├── .clang-format ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── CMakePresets.json ├── LICENSE ├── ProjectGen.py ├── README.md ├── cmake ├── Version.h.in ├── common.cmake ├── headerlist.cmake ├── ports │ ├── clib-util │ │ ├── portfile.cmake │ │ └── vcpkg.json │ └── mergemapper │ │ ├── portfile.cmake │ │ └── vcpkg.json ├── sourcelist.cmake └── version.rc.in ├── include ├── Cache.h ├── Defs.h ├── DependencyResolver.h ├── Distribute.h ├── ExclusiveGroups.h ├── Hooks.h ├── KeywordData.h ├── KeywordDependencies.h ├── LogBuffer.h ├── LookupConfigs.h ├── LookupFilters.h ├── LookupForms.h ├── PCH.h └── Traits.h ├── src ├── Cache.cpp ├── Distribute.cpp ├── ExclusiveGroups.cpp ├── Hooks.cpp ├── KeywordData.cpp ├── LogBuffer.cpp ├── LookupConfigs.cpp ├── LookupFilters.cpp ├── LookupForms.cpp ├── PCH.cpp └── main.cpp └── vcpkg.json /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AccessModifierOffset: -4 3 | AlignAfterOpenBracket: DontAlign 4 | AlignConsecutiveAssignments: 'false' 5 | AlignConsecutiveBitFields: 'false' 6 | AlignConsecutiveDeclarations: 'true' 7 | AlignConsecutiveMacros: 'false' 8 | AlignEscapedNewlines: Left 9 | AlignOperands: Align 10 | AlignTrailingComments: 'true' 11 | AllowAllArgumentsOnNextLine: 'false' 12 | AllowAllConstructorInitializersOnNextLine: 'false' 13 | AllowAllParametersOfDeclarationOnNextLine: 'false' 14 | AllowShortBlocksOnASingleLine: Empty 15 | AllowShortCaseLabelsOnASingleLine: 'false' 16 | AllowShortEnumsOnASingleLine: 'true' 17 | AllowShortFunctionsOnASingleLine: All 18 | AllowShortIfStatementsOnASingleLine: Never 19 | AllowShortLambdasOnASingleLine: All 20 | AllowShortLoopsOnASingleLine: 'true' 21 | AlwaysBreakAfterReturnType: None 22 | AlwaysBreakBeforeMultilineStrings: 'true' 23 | AlwaysBreakTemplateDeclarations: 'Yes' 24 | BinPackArguments: 'true' 25 | BinPackParameters: 'true' 26 | BitFieldColonSpacing: After 27 | BraceWrapping: 28 | AfterCaseLabel: 'true' 29 | AfterClass: 'true' 30 | AfterControlStatement: 'false' 31 | AfterEnum: 'true' 32 | AfterFunction: 'true' 33 | AfterNamespace: 'true' 34 | AfterStruct: 'true' 35 | AfterUnion: 'true' 36 | AfterExternBlock: 'true' 37 | BeforeCatch: 'false' 38 | BeforeElse: 'false' 39 | BeforeLambdaBody: 'false' 40 | BeforeWhile: 'false' 41 | IndentBraces: 'false' 42 | SplitEmptyFunction: 'false' 43 | SplitEmptyRecord: 'false' 44 | SplitEmptyNamespace: 'false' 45 | BreakBeforeBinaryOperators: None 46 | BreakBeforeBraces: Custom 47 | BreakBeforeTernaryOperators: 'false' 48 | BreakConstructorInitializers: AfterColon 49 | BreakInheritanceList: AfterColon 50 | BreakStringLiterals: 'true' 51 | ColumnLimit: 0 52 | CompactNamespaces: 'false' 53 | ConstructorInitializerAllOnOneLineOrOnePerLine: 'false' 54 | ConstructorInitializerIndentWidth: 4 55 | ContinuationIndentWidth: 4 56 | Cpp11BracedListStyle: 'false' 57 | DeriveLineEnding: 'true' 58 | DerivePointerAlignment: 'false' 59 | DisableFormat: 'false' 60 | FixNamespaceComments: 'false' 61 | IncludeBlocks: Preserve 62 | IndentCaseBlocks: 'true' 63 | IndentCaseLabels: 'false' 64 | IndentExternBlock: Indent 65 | IndentGotoLabels: 'false' 66 | IndentPPDirectives: AfterHash 67 | IndentWidth: 4 68 | IndentWrappedFunctionNames: 'true' 69 | KeepEmptyLinesAtTheStartOfBlocks: 'false' 70 | Language: Cpp 71 | MaxEmptyLinesToKeep: 1 72 | NamespaceIndentation: All 73 | PointerAlignment: Left 74 | ReflowComments : 'false' 75 | SortIncludes: 'true' 76 | SortUsingDeclarations: 'true' 77 | SpaceAfterCStyleCast: 'false' 78 | SpaceAfterLogicalNot: 'false' 79 | SpaceAfterTemplateKeyword: 'true' 80 | SpaceBeforeAssignmentOperators: 'true' 81 | SpaceBeforeCpp11BracedList: 'false' 82 | SpaceBeforeCtorInitializerColon: 'true' 83 | SpaceBeforeInheritanceColon: 'true' 84 | SpaceBeforeParens: ControlStatements 85 | SpaceBeforeRangeBasedForLoopColon: 'true' 86 | SpaceBeforeSquareBrackets: 'false' 87 | SpaceInEmptyBlock: 'false' 88 | SpaceInEmptyParentheses: 'false' 89 | SpacesBeforeTrailingComments: 2 90 | SpacesInAngles: 'false' 91 | SpacesInCStyleCastParentheses: 'false' 92 | SpacesInConditionalStatement: 'false' 93 | SpacesInContainerLiterals: 'true' 94 | SpacesInParentheses: 'false' 95 | SpacesInSquareBrackets: 'false' 96 | Standard: Latest 97 | TabWidth: 4 98 | UseCRLF: 'true' 99 | UseTab: AlignWithSpaces 100 | 101 | ... 102 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | insert_final_newline = true 4 | 5 | [*.{c,cmake,cpp,cxx,h,hpp,hxx}] 6 | indent_style = tab 7 | indent_size = 4 8 | 9 | [*.json] 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | - 'v[0-9]+.[0-9]+.[0-9]+.rc[0-9]+' 8 | 9 | concurrency: 10 | group: ${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | run: 15 | uses: adya/pack-skse-mod/.github/workflows/pack.yml@main 16 | with: 17 | FOMOD_INCLUDE_PDB: true 18 | FOMOD_MOD_NAME: "Keyword Item Distributor" 19 | FOMOD_MOD_AUTHOR: "powerofthree" 20 | FOMOD_MOD_NEXUS_ID: "55728" 21 | FOMOD_SE_MIN_GAME_VERSION: '1.5' 22 | FOMOD_AE_NAME: 'SSE v1.6+ ("Anniversary Edition")' 23 | FOMOD_AE_DESCR: 'Select this if you are using Skyrim Anniversary Edition v1.6 or higher.' 24 | FOMOD_AE_MIN_GAME_VERSION: '1.6' 25 | PUBLISH_ARCHIVE_TYPE: '7z' 26 | VCPKG_COMMIT_ID: '43d81795a513e2ca6648354786178714f33c8b6f' 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build*/ 2 | /.vs 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "extern/CommonLibSSE"] 2 | path = extern/CommonLibSSE 3 | url = https://github.com/powerof3/CommonLibSSE 4 | [submodule "extern/CommonLibVR"] 5 | path = extern/CommonLibVR 6 | url = https://github.com/alandtse/CommonLibVR 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | set(NAME "po3_KeywordItemDistributor" CACHE STRING "") 3 | set(VERSION 3.5.0 CACHE STRING "") 4 | set(AE_VERSION 1) 5 | set(VR_VERSION 1) 6 | 7 | # ---- Options ---- 8 | 9 | option(COPY_BUILD "Copy the build output to the Skyrim directory." TRUE) 10 | option(BUILD_SKYRIMVR "Build for Skyrim VR" OFF) 11 | option(BUILD_SKYRIMAE "Build for Skyrim AE" OFF) 12 | 13 | # ---- Cache build vars ---- 14 | 15 | macro(set_from_environment VARIABLE) 16 | if (NOT DEFINED ${VARIABLE} AND DEFINED ENV{${VARIABLE}}) 17 | set(${VARIABLE} $ENV{${VARIABLE}}) 18 | endif () 19 | endmacro() 20 | 21 | macro(find_commonlib_path) 22 | if (CommonLibName AND NOT ${CommonLibName} STREQUAL "") 23 | # Check extern 24 | find_path(CommonLibPath 25 | include/REL/Relocation.h 26 | PATHS extern/${CommonLibName}) 27 | if (${CommonLibPath} STREQUAL "CommonLibPath-NOTFOUND") 28 | #Check path 29 | set_from_environment(${CommonLibName}Path) 30 | set(CommonLibPath ${${CommonLibName}Path}) 31 | endif() 32 | endif() 33 | endmacro() 34 | 35 | set_from_environment(VCPKG_ROOT) 36 | if(BUILD_SKYRIMAE) 37 | add_compile_definitions(SKYRIM_AE) 38 | add_compile_definitions(SKYRIM_SUPPORT_AE) 39 | set(CommonLibName "CommonLibSSE") 40 | set_from_environment(SkyrimAEPath) 41 | set(SkyrimPath ${SkyrimAEPath}) 42 | set(SkyrimVersion "Skyrim AE") 43 | set(VERSION ${VERSION}.${AE_VERSION}) 44 | elseif(BUILD_SKYRIMVR) 45 | add_compile_definitions(SKYRIMVR) 46 | add_compile_definitions(_CRT_SECURE_NO_WARNINGS) 47 | set(CommonLibName "CommonLibVR") 48 | set_from_environment(SkyrimVRPath) 49 | set(SkyrimPath ${SkyrimVRPath}) 50 | set(SkyrimVersion "Skyrim VR") 51 | set(VERSION ${VERSION}.${VR_VERSION}) 52 | else() 53 | set(CommonLibName "CommonLibSSE") 54 | set_from_environment(Skyrim64Path) 55 | set(SkyrimPath ${Skyrim64Path}) 56 | set(SkyrimVersion "Skyrim SSE") 57 | endif() 58 | find_commonlib_path() 59 | message( 60 | STATUS 61 | "Building ${NAME} ${VERSION} for ${SkyrimVersion} at ${SkyrimPath} with ${CommonLibName} at ${CommonLibPath}." 62 | ) 63 | 64 | if (DEFINED VCPKG_ROOT) 65 | set(CMAKE_TOOLCHAIN_FILE "${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "") 66 | set(VCPKG_TARGET_TRIPLET "x64-windows-static" CACHE STRING "") 67 | else () 68 | message( 69 | WARNING 70 | "Variable VCPKG_ROOT is not set. Continuing without vcpkg." 71 | ) 72 | endif () 73 | 74 | set(Boost_USE_STATIC_RUNTIME OFF CACHE BOOL "") 75 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" CACHE STRING "") 76 | 77 | # ---- Project ---- 78 | 79 | project( 80 | ${NAME} 81 | VERSION ${VERSION} 82 | LANGUAGES CXX 83 | ) 84 | 85 | configure_file( 86 | ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Version.h.in 87 | ${CMAKE_CURRENT_BINARY_DIR}/include/Version.h 88 | @ONLY 89 | ) 90 | 91 | configure_file( 92 | ${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.rc.in 93 | ${CMAKE_CURRENT_BINARY_DIR}/version.rc 94 | @ONLY 95 | ) 96 | 97 | # ---- Include guards ---- 98 | 99 | if(PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR) 100 | message( 101 | FATAL_ERROR 102 | "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there." 103 | ) 104 | endif() 105 | 106 | # ---- Globals ---- 107 | 108 | add_compile_definitions( 109 | SKSE_SUPPORT_XBYAK 110 | ) 111 | 112 | if (MSVC) 113 | if (NOT ${CMAKE_GENERATOR} STREQUAL "Ninja") 114 | add_compile_options( 115 | /MP # Build with Multiple Processes 116 | ) 117 | endif () 118 | endif () 119 | 120 | set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) 121 | set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_DEBUG OFF) 122 | 123 | set(Boost_USE_STATIC_LIBS ON) 124 | 125 | # ---- Dependencies ---- 126 | if (DEFINED CommonLibPath AND NOT ${CommonLibPath} STREQUAL "" AND IS_DIRECTORY ${CommonLibPath}) 127 | add_subdirectory(${CommonLibPath} ${CommonLibName}) 128 | else () 129 | message( 130 | FATAL_ERROR 131 | "Variable ${CommonLibName}Path is not set or in extern/." 132 | ) 133 | endif() 134 | 135 | find_package(unordered_dense CONFIG REQUIRED) 136 | find_package(tsl-ordered-map CONFIG REQUIRED) 137 | 138 | find_path(SRELL_INCLUDE_DIRS "srell.hpp") 139 | find_path(MERGEMAPPER_INCLUDE_DIRS "MergeMapperPluginAPI.h") 140 | find_path(CLIB_UTIL_INCLUDE_DIRS "ClibUtil/utils.hpp") 141 | 142 | # ---- Add source files ---- 143 | 144 | include(cmake/headerlist.cmake) 145 | include(cmake/sourcelist.cmake) 146 | 147 | source_group( 148 | TREE 149 | ${CMAKE_CURRENT_SOURCE_DIR} 150 | FILES 151 | ${headers} 152 | ${sources} 153 | ) 154 | 155 | source_group( 156 | TREE 157 | ${CMAKE_CURRENT_BINARY_DIR} 158 | FILES 159 | ${CMAKE_CURRENT_BINARY_DIR}/include/Version.h 160 | ) 161 | 162 | # ---- Create DLL ---- 163 | 164 | add_library( 165 | ${PROJECT_NAME} 166 | SHARED 167 | ${headers} 168 | ${sources} 169 | ${CMAKE_CURRENT_BINARY_DIR}/include/Version.h 170 | ${CMAKE_CURRENT_BINARY_DIR}/version.rc 171 | .clang-format 172 | .editorconfig 173 | ${MERGEMAPPER_INCLUDE_DIRS}/MergeMapperPluginAPI.cpp 174 | ) 175 | 176 | target_compile_features( 177 | ${PROJECT_NAME} 178 | PRIVATE 179 | cxx_std_23 180 | ) 181 | 182 | target_compile_definitions( 183 | ${PROJECT_NAME} 184 | PRIVATE 185 | _UNICODE 186 | ) 187 | 188 | target_include_directories( 189 | ${PROJECT_NAME} 190 | PRIVATE 191 | ${CMAKE_CURRENT_BINARY_DIR}/include 192 | ${CMAKE_CURRENT_SOURCE_DIR}/include 193 | ${SRELL_INCLUDE_DIRS} 194 | ${MERGEMAPPER_INCLUDE_DIRS} 195 | ${CLIB_UTIL_INCLUDE_DIRS} 196 | ) 197 | 198 | target_link_libraries( 199 | ${PROJECT_NAME} 200 | PRIVATE 201 | ${CommonLibName}::${CommonLibName} 202 | unordered_dense::unordered_dense 203 | tsl::ordered_map 204 | ) 205 | 206 | target_precompile_headers( 207 | ${PROJECT_NAME} 208 | PRIVATE 209 | include/PCH.h 210 | ) 211 | 212 | if (MSVC) 213 | target_compile_options( 214 | ${PROJECT_NAME} 215 | PRIVATE 216 | /sdl # Enable Additional Security Checks 217 | /utf-8 # Set Source and Executable character sets to UTF-8 218 | /Zi # Debug Information Format 219 | 220 | /permissive- # Standards conformance 221 | /Zc:preprocessor # Enable preprocessor conformance mode 222 | 223 | /wd4200 # nonstandard extension used : zero-sized array in struct/union 224 | 225 | "$<$:>" 226 | "$<$:/Zc:inline;/JMC-;/Ob3>" 227 | ) 228 | 229 | target_link_options( 230 | ${PROJECT_NAME} 231 | PRIVATE 232 | "$<$:/INCREMENTAL;/OPT:NOREF;/OPT:NOICF>" 233 | "$<$:/INCREMENTAL:NO;/OPT:REF;/OPT:ICF;/DEBUG:FULL>" 234 | ) 235 | endif () 236 | 237 | # ---- Post build ---- 238 | 239 | if (COPY_BUILD) 240 | if (DEFINED SkyrimPath) 241 | add_custom_command( 242 | TARGET ${PROJECT_NAME} 243 | POST_BUILD 244 | COMMAND ${CMAKE_COMMAND} -E copy $ ${SkyrimPath}/SKSE/Plugins/ 245 | COMMAND ${CMAKE_COMMAND} -E copy $ ${SkyrimPath}/SKSE/Plugins/ 246 | ) 247 | else () 248 | message( 249 | WARNING 250 | "Variable ${SkyrimPath} is not defined. Skipping post-build copy command." 251 | ) 252 | endif () 253 | endif () 254 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 21, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "cmake-dev", 11 | "hidden": true, 12 | "cacheVariables": { 13 | "CMAKE_CONFIGURATION_TYPES": "Debug;Release", 14 | "CMAKE_CXX_FLAGS": "/EHsc /MP /W4 /external:anglebrackets /external:W0 $penv{CXXFLAGS}" 15 | }, 16 | "errors": { 17 | "deprecated": true 18 | }, 19 | "warnings": { 20 | "deprecated": true, 21 | "dev": true 22 | } 23 | }, 24 | { 25 | "name": "vcpkg", 26 | "hidden": true, 27 | "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", 28 | "cacheVariables": { 29 | "VCPKG_OVERLAY_PORTS": "${sourceDir}/cmake/ports/" 30 | } 31 | }, 32 | { 33 | "name": "windows", 34 | "hidden": true, 35 | "cacheVariables": { 36 | "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded$<$:Debug>", 37 | "VCPKG_TARGET_TRIPLET": "x64-windows-static" 38 | } 39 | }, 40 | { 41 | "name": "vs2022", 42 | "hidden": true, 43 | "generator": "Visual Studio 17 2022", 44 | "toolset": "v143" 45 | }, 46 | { 47 | "name": "se", 48 | "hidden": true, 49 | "binaryDir": "${sourceDir}/build" 50 | }, 51 | { 52 | "name": "ae", 53 | "hidden": true, 54 | "binaryDir": "${sourceDir}/buildae", 55 | "cacheVariables": { 56 | "BUILD_SKYRIMAE": true 57 | } 58 | }, 59 | { 60 | "name": "vr", 61 | "hidden": true, 62 | "binaryDir": "${sourceDir}/buildvr", 63 | "cacheVariables": { 64 | "BUILD_SKYRIMVR": true 65 | } 66 | }, 67 | { 68 | "name": "vs2022-windows-vcpkg-se", 69 | "inherits": [ 70 | "cmake-dev", 71 | "vcpkg", 72 | "windows", 73 | "vs2022", 74 | "se" 75 | ] 76 | }, 77 | { 78 | "name": "vs2022-windows-vcpkg-ae", 79 | "inherits": [ 80 | "cmake-dev", 81 | "vcpkg", 82 | "windows", 83 | "vs2022", 84 | "ae" 85 | ] 86 | }, 87 | { 88 | "name": "vs2022-windows-vcpkg-vr", 89 | "inherits": [ 90 | "cmake-dev", 91 | "vcpkg", 92 | "windows", 93 | "vs2022", 94 | "vr" 95 | ] 96 | } 97 | ], 98 | "buildPresets": [ 99 | { 100 | "name": "vs2022-windows-vcpkg-ae", 101 | "configurePreset": "vs2022-windows-vcpkg-ae", 102 | "configuration": "Release" 103 | }, 104 | { 105 | "name": "vs2022-windows-vcpkg-se", 106 | "configurePreset": "vs2022-windows-vcpkg-se", 107 | "configuration": "Release" 108 | }, 109 | { 110 | "name": "vs2022-windows-vcpkg-vr", 111 | "configurePreset": "vs2022-windows-vcpkg-vr", 112 | "configuration": "Release" 113 | } 114 | ] 115 | } 116 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 powerofthree 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ProjectGen.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | HEADER_TYPES = (".h", ".hpp", ".hxx") 4 | SOURCE_TYPES = (".c", ".cpp", ".cxx") 5 | ALL_TYPES = HEADER_TYPES + SOURCE_TYPES 6 | 7 | def make_cmake(): 8 | tmp = list() 9 | directories = ("src","include") 10 | for directory in directories: 11 | for dirpath, dirnames, filenames in os.walk(directory): 12 | for filename in filenames: 13 | if filename.endswith(ALL_TYPES): 14 | path = os.path.join(dirpath, filename) 15 | tmp.append(os.path.normpath(path)) 16 | 17 | headers = list() 18 | sources = list() 19 | for file in tmp: 20 | name = file.replace("\\", "/") 21 | if name.endswith(HEADER_TYPES): 22 | headers.append(name) 23 | elif name.endswith(SOURCE_TYPES): 24 | sources.append(name) 25 | headers.sort() 26 | sources.sort() 27 | 28 | def do_make(a_filename, a_varname, a_files): 29 | out = open("cmake/" + a_filename + ".cmake", "w", encoding="utf-8") 30 | out.write("set(" + a_varname + " ${" + a_varname + "}\n") 31 | 32 | for file in a_files: 33 | out.write("\t" + file + "\n") 34 | 35 | out.write(")\n") 36 | 37 | do_make("headerlist", "headers", headers) 38 | do_make("sourcelist", "sources", sources) 39 | 40 | def main(): 41 | cur = os.path.dirname(os.path.realpath(__file__)) 42 | os.chdir(cur) 43 | make_cmake() 44 | 45 | if __name__ == "__main__": 46 | main() 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keyword Item Distributor 2 | 3 | Distributes keywords to items (armor/weapons/ammo) 4 | * [Nexus link](https://www.nexusmods.com/skyrimspecialedition/mods/55728) 5 | 6 | ## Requirements 7 | * [CMake](https://cmake.org/) 8 | * Add this to your `PATH` 9 | * [PowerShell](https://github.com/PowerShell/PowerShell/releases/latest) 10 | * [Vcpkg](https://github.com/microsoft/vcpkg) 11 | * Add the environment variable `VCPKG_ROOT` with the value as the path to the folder containing vcpkg 12 | * [Visual Studio Community 2019](https://visualstudio.microsoft.com/) 13 | * Desktop development with C++ 14 | * [CommonLibSSE](https://github.com/powerof3/CommonLibSSE/tree/dev) 15 | * You need to build from the powerof3/dev branch 16 | * Add this as as an environment variable `CommonLibSSEPath` 17 | 18 | ## User Requirements 19 | * [Address Library for SKSE](https://www.nexusmods.com/skyrimspecialedition/mods/32444) 20 | * Needed for SSE 21 | * [VR Address Library for SKSEVR](https://www.nexusmods.com/skyrimspecialedition/mods/58101) 22 | * Needed for VR 23 | ## Register Visual Studio as a Generator 24 | * Open `x64 Native Tools Command Prompt` 25 | * Run `cmake` 26 | * Close the cmd window 27 | 28 | ## Building 29 | ``` 30 | git clone https://github.com/powerof3/Keyword-Item-Distributor.git 31 | cd Keyword-Item-Distributor 32 | # pull commonlib /extern to override the path settings 33 | git submodule init 34 | # to update submodules to checked in build 35 | git submodule update 36 | ``` 37 | 38 | ### SSE 39 | ``` 40 | cmake --preset vs2022-windows-vcpkg-se 41 | cmake --build build --config Release 42 | ``` 43 | ### AE 44 | ``` 45 | cmake --preset vs2022-windows-vcpkg-ae 46 | cmake --build buildae --config Release 47 | ``` 48 | ### VR 49 | ``` 50 | cmake --preset vs2022-windows-vcpkg-vr 51 | cmake --build buildvr --config Release 52 | ``` 53 | 54 | ## License 55 | [MIT](LICENSE) 56 | -------------------------------------------------------------------------------- /cmake/Version.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Version 4 | { 5 | inline constexpr std::size_t MAJOR = @PROJECT_VERSION_MAJOR@; 6 | inline constexpr std::size_t MINOR = @PROJECT_VERSION_MINOR@; 7 | inline constexpr std::size_t PATCH = @PROJECT_VERSION_PATCH@; 8 | inline constexpr auto NAME = "@PROJECT_VERSION@"sv; 9 | inline constexpr auto PROJECT = "@PROJECT_NAME@"sv; 10 | } 11 | -------------------------------------------------------------------------------- /cmake/common.cmake: -------------------------------------------------------------------------------- 1 | macro(set_from_environment VARIABLE) 2 | if(NOT DEFINED "${VARIABLE}" AND DEFINED "ENV{${VARIABLE}}") 3 | set("${VARIABLE}" "$ENV{${VARIABLE}}") 4 | endif() 5 | endmacro() 6 | 7 | macro(add_project) 8 | set(_PREFIX add_project) 9 | 10 | set(_OPTIONS) 11 | set(_ONE_VALUE_ARGS 12 | LIBRARY_TYPE 13 | PROJECT 14 | TARGET_TYPE 15 | VERSION 16 | ) 17 | set(_MULTI_VALUE_ARGS 18 | COMPILE_DEFINITIONS 19 | GROUPED_FILES 20 | INCLUDE_DIRECTORIES 21 | MISC_FILES 22 | PRECOMPILED_HEADERS 23 | ) 24 | 25 | set(_REQUIRED 26 | PROJECT 27 | TARGET_TYPE 28 | ) 29 | 30 | cmake_parse_arguments( 31 | "${_PREFIX}" 32 | "${_OPTIONS}" 33 | "${_ONE_VALUE_ARGS}" 34 | "${_MULTI_VALUE_ARGS}" 35 | "${ARGN}" 36 | ) 37 | 38 | foreach(_ARG ${_REQUIRED}) 39 | if(NOT DEFINED "${_PREFIX}_${_ARG}") 40 | message(FATAL_ERROR "Argument is required to be defined: ${_ARG}") 41 | endif() 42 | endforeach() 43 | 44 | set(_CLEANUP 45 | _PREFIX 46 | _OPTIONS 47 | _ONE_VALUE_ARGS 48 | _MULTI_VALUE_ARGS 49 | _REQUIRED 50 | ${_PREFIX}_UNPARSED_ARGUMENTS 51 | ${_PREFIX}_KEYWORDS_MISSING_VALUES 52 | ) 53 | 54 | foreach(_ARG IN LISTS _OPTIONS _ONE_VALUE_ARGS _MULTI_VALUE_ARGS) 55 | list(APPEND _CLEANUP "${_PREFIX}_${_ARG}") 56 | endforeach() 57 | 58 | # ---- Argument validation 59 | 60 | string(TOLOWER "${${_PREFIX}_TARGET_TYPE}" "${_PREFIX}_TARGET_TYPE") 61 | 62 | if(DEFINED "${_PREFIX}_UNPARSED_ARGUMENTS") 63 | foreach(_ARG "${_PREFIX}_UNPARSED_ARGUMENTS") 64 | message(WARNING "Unused argument: ${_ARG}") 65 | endforeach() 66 | endif() 67 | 68 | set(_TARGET_TYPES executable library) 69 | list(APPEND _CLEANUP _TARGET_TYPES) 70 | if(NOT "${${_PREFIX}_TARGET_TYPE}" IN_LIST _TARGET_TYPES) 71 | message(FATAL_ERROR "TARGET_TYPE \"${${_PREFIX}_TARGET_TYPE}\" must be one of: ${_TARGET_TYPES}") 72 | endif() 73 | 74 | set(_LIBRARY_TYPES STATIC SHARED MODULE) 75 | list(APPEND _CLEANUP _LIBRARY_TYPES) 76 | if(DEFINED "${_PREFIX}_LIBRARY_TYPE" AND 77 | NOT "${${_PREFIX}_LIBRARY_TYPE}" IN_LIST _LIBRARY_TYPES) 78 | message(FATAL_ERROR "LIBRARY_TYPE \"${${_PREFIX}_LIBRARY_TYPE}\" must be one of: ${_LIBRARY_TYPES}") 79 | endif() 80 | 81 | if("${_PREFIX}_TARGET_TYPE" STREQUAL "library" AND 82 | NOT DEFINED "${_PREFIX}_LIBRARY_TYPE") 83 | message(FATAL_ERROR "LIBRARY_TYPE must be set for \"library\" targets") 84 | elseif("${_PREFIX}_TARGET_TYPE" STREQUAL "executable" AND 85 | DEFINED "${_PREFIX}_LIBRARY_TYPE") 86 | message(FATAL_ERROR "LIBRARY_TYPE must not be set for \"executable\" targets") 87 | endif() 88 | 89 | # ---- Project ---- 90 | 91 | if(DEFINED "${_PREFIX}_VERSION") 92 | set(_VERSION_PREFIX "VERSION") 93 | list(APPEND _CLEANUP _VERSION_PREFIX) 94 | else() 95 | unset(_VERSION_PREFIX) 96 | endif() 97 | 98 | project( 99 | "${${_PREFIX}_PROJECT}" 100 | ${_VERSION_PREFIX} ${${_PREFIX}_VERSION} 101 | LANGUAGES CXX 102 | ) 103 | 104 | # ---- Include guards ---- 105 | 106 | if("${PROJECT_SOURCE_DIR}" STREQUAL "${PROJECT_BINARY_DIR}") 107 | message(FATAL_ERROR "in-source builds are not allowed") 108 | endif() 109 | 110 | # ---- Add target ---- 111 | 112 | cmake_language( 113 | CALL 114 | "add_${${_PREFIX}_TARGET_TYPE}" # add_executable/add_library 115 | "${PROJECT_NAME}" 116 | "${${_PREFIX}_LIBRARY_TYPE}" 117 | ${${_PREFIX}_GROUPED_FILES} 118 | ${${_PREFIX}_MISC_FILES} 119 | ) 120 | 121 | set_target_properties( 122 | "${PROJECT_NAME}" 123 | PROPERTIES 124 | CXX_STANDARD 20 125 | CXX_STANDARD_REQUIRED ON 126 | INTERPROCEDURAL_OPTIMIZATION ON 127 | INTERPROCEDURAL_OPTIMIZATION_DEBUG OFF 128 | ) 129 | 130 | target_compile_definitions( 131 | "${PROJECT_NAME}" 132 | PRIVATE 133 | _UNICODE 134 | ${${_PREFIX}_COMPILE_DEFINITIONS} 135 | ) 136 | 137 | target_include_directories( 138 | "${PROJECT_NAME}" 139 | PRIVATE 140 | ${${_PREFIX}_INCLUDE_DIRECTORIES} 141 | ) 142 | 143 | target_precompile_headers( 144 | "${PROJECT_NAME}" 145 | PRIVATE 146 | ${${_PREFIX}_PRECOMPILED_HEADERS} 147 | ) 148 | 149 | if(MSVC) 150 | target_compile_options( 151 | "${PROJECT_NAME}" 152 | PRIVATE 153 | "/sdl" # Enable Additional Security Checks 154 | "/utf-8" # Set Source and Executable character sets to UTF-8 155 | "/Zi" # Debug Information Format 156 | 157 | "/permissive-" # Standards conformance 158 | ) 159 | 160 | target_link_options( 161 | "${PROJECT_NAME}" 162 | PRIVATE 163 | /WX # Treat Linker Warnings as Errors 164 | 165 | "$<$:/INCREMENTAL;/OPT:NOREF;/OPT:NOICF>" 166 | "$<$:/INCREMENTAL:NO;/OPT:REF;/OPT:ICF;/DEBUG:FULL>" 167 | ) 168 | endif() 169 | 170 | source_group( 171 | TREE "${CMAKE_CURRENT_SOURCE_DIR}" 172 | FILES 173 | ${${_PREFIX}_GROUPED_FILES} 174 | ) 175 | 176 | # ---- Cleanup local variables ---- 177 | 178 | foreach(_VAR "${_CLEANUP}") 179 | unset("${_VAR}") 180 | endforeach() 181 | unset(_CLEANUP) 182 | endmacro() 183 | 184 | macro(copy_files) 185 | if(NOT "${ARGC}" GREATER 0) 186 | message(FATAL_ERROR "Invalid number of arguments.") 187 | endif() 188 | 189 | math(EXPR _REMAINDER "${ARGC} % 2") 190 | if(NOT _REMAINDER EQUAL 0) 191 | message(FATAL_ERROR "Arguments must be paired as a file + path spec.") 192 | endif() 193 | 194 | option(COPY_BUILD "Copy the build output to the Skyrim directory." OFF) 195 | 196 | set(_ARGS "${ARGN}") 197 | 198 | if(COPY_BUILD) 199 | set_from_environment(Skyrim64Path) 200 | if(DEFINED Skyrim64Path) 201 | math(EXPR _PAIRS "${ARGC} / 2 - 1") 202 | foreach(_IDX RANGE "${_PAIRS}") 203 | math(EXPR _IDX "${_IDX} * 2") 204 | math(EXPR _IDXN "${_IDX} + 1") 205 | 206 | list(GET _ARGS "${_IDX}" _FROM) 207 | list(GET _ARGS "${_IDXN}" _TO) 208 | 209 | cmake_path(SET _TO NORMALIZE "${Skyrim64Path}/${_TO}") 210 | 211 | add_custom_command( 212 | TARGET "${PROJECT_NAME}" 213 | POST_BUILD 214 | COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${_FROM}" "${_TO}" 215 | ) 216 | endforeach() 217 | else() 218 | message(WARNING "Variable Skyrim64Path is not defined. Skipping post-build copy command.") 219 | endif() 220 | endif() 221 | 222 | unset(_ARGS) 223 | unset(_REMAINDER) 224 | unset(_IDX) 225 | unset(_IDXN) 226 | unset(_FROM) 227 | unset(_TO) 228 | endmacro() 229 | -------------------------------------------------------------------------------- /cmake/headerlist.cmake: -------------------------------------------------------------------------------- 1 | set(headers ${headers} 2 | include/Cache.h 3 | include/Defs.h 4 | include/DependencyResolver.h 5 | include/Distribute.h 6 | include/ExclusiveGroups.h 7 | include/Hooks.h 8 | include/KeywordData.h 9 | include/KeywordDependencies.h 10 | include/LogBuffer.h 11 | include/LookupConfigs.h 12 | include/LookupFilters.h 13 | include/LookupForms.h 14 | include/PCH.h 15 | include/Traits.h 16 | ) 17 | -------------------------------------------------------------------------------- /cmake/ports/clib-util/portfile.cmake: -------------------------------------------------------------------------------- 1 | # header-only library 2 | vcpkg_from_github( 3 | OUT_SOURCE_PATH SOURCE_PATH 4 | REPO powerof3/CLibUtil 5 | REF 88d78d94464a04e582669beac56346edbbc4a662 6 | SHA512 960cf62e5317356f7c0d994e49f56effb89c415377e9c865e801c5ec28b57e9ec0fd2a9fd54136cd2382addedb6745cd5cc062c46cab5cccb1f634999491c9e1 7 | HEAD_REF master 8 | ) 9 | 10 | # Install codes 11 | set(CLIBUTIL_SOURCE ${SOURCE_PATH}/include/ClibUtil) 12 | file(INSTALL ${CLIBUTIL_SOURCE} DESTINATION ${CURRENT_PACKAGES_DIR}/include) 13 | 14 | vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE") 15 | -------------------------------------------------------------------------------- /cmake/ports/clib-util/vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clib-util", 3 | "version-string": "1.4.2", 4 | "port-version": 1, 5 | "description": "", 6 | "homepage": "https://github.com/powerof3/CLibUtil" 7 | } 8 | -------------------------------------------------------------------------------- /cmake/ports/mergemapper/portfile.cmake: -------------------------------------------------------------------------------- 1 | # header-only library 2 | vcpkg_from_github( 3 | OUT_SOURCE_PATH SOURCE_PATH 4 | REPO alandtse/MergeMapper 5 | REF ad758592e2c4fa77f0c0e985348f17af8fa24371 6 | SHA512 010f2e5de88c41b5b614af5e7347f7f08293b190e89bd761664ee1f2bcfa70007f87b4e0bb219a5ad7ee06921db9f6df061c875d6723f6068d776e651f729374 7 | HEAD_REF main 8 | ) 9 | 10 | # Install codes 11 | set(MERGEMAPPER_SOURCE ${SOURCE_PATH}/src/MergeMapperPluginAPI.cpp ${SOURCE_PATH}/include/MergeMapperPluginAPI.h) 12 | 13 | file(INSTALL ${MERGEMAPPER_SOURCE} DESTINATION ${CURRENT_PACKAGES_DIR}/include) 14 | 15 | file(INSTALL ${SOURCE_PATH}/LICENSE DESTINATION ${CURRENT_PACKAGES_DIR}/share/${PORT} RENAME copyright) 16 | -------------------------------------------------------------------------------- /cmake/ports/mergemapper/vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mergemapper", 3 | "version-string": "1.5.0", 4 | "port-version": 1, 5 | "description": "A SKSE plugin to dynamically map zmerges.", 6 | "homepage": "https://github.com/alandtse/MergeMapper" 7 | } 8 | -------------------------------------------------------------------------------- /cmake/sourcelist.cmake: -------------------------------------------------------------------------------- 1 | set(sources ${sources} 2 | src/Cache.cpp 3 | src/Distribute.cpp 4 | src/ExclusiveGroups.cpp 5 | src/Hooks.cpp 6 | src/KeywordData.cpp 7 | src/LogBuffer.cpp 8 | src/LookupConfigs.cpp 9 | src/LookupFilters.cpp 10 | src/LookupForms.cpp 11 | src/PCH.cpp 12 | src/main.cpp 13 | ) 14 | -------------------------------------------------------------------------------- /cmake/version.rc.in: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | 1 VERSIONINFO 4 | FILEVERSION @PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@, @PROJECT_VERSION_PATCH@, 0 5 | PRODUCTVERSION @PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@, @PROJECT_VERSION_PATCH@, 0 6 | FILEFLAGSMASK 0x17L 7 | #ifdef _DEBUG 8 | FILEFLAGS 0x1L 9 | #else 10 | FILEFLAGS 0x0L 11 | #endif 12 | FILEOS 0x4L 13 | FILETYPE 0x1L 14 | FILESUBTYPE 0x0L 15 | BEGIN 16 | BLOCK "StringFileInfo" 17 | BEGIN 18 | BLOCK "040904b0" 19 | BEGIN 20 | VALUE "FileDescription", "@PROJECT_NAME@" 21 | VALUE "FileVersion", "@PROJECT_VERSION@.0" 22 | VALUE "InternalName", "@PROJECT_NAME@" 23 | VALUE "LegalCopyright", "MIT License" 24 | VALUE "ProductName", "@PROJECT_NAME@" 25 | VALUE "ProductVersion", "@PROJECT_VERSION@.0" 26 | END 27 | END 28 | BLOCK "VarFileInfo" 29 | BEGIN 30 | VALUE "Translation", 0x409, 1200 31 | END 32 | END 33 | -------------------------------------------------------------------------------- /include/Cache.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Cache 4 | { 5 | namespace Item 6 | { 7 | enum TYPE : std::uint32_t 8 | { 9 | kNone = static_cast>(-1), 10 | kArmor = 0, 11 | kWeapon, 12 | kAmmo, 13 | kMagicEffect, 14 | kPotion, 15 | kScroll, 16 | kLocation, 17 | kIngredient, 18 | kBook, 19 | kMiscItem, 20 | kKey, 21 | kSoulGem, 22 | kSpell, 23 | kActivator, 24 | kFlora, 25 | kFurniture, 26 | kRace, 27 | kTalkingActivator, 28 | kEnchantmentItem, 29 | 30 | kTotal 31 | }; 32 | 33 | inline constexpr std::array itemTypes{ 34 | std::pair{ "Armor"sv, "armors"sv }, 35 | std::pair{ "Weapon"sv, "weapons"sv }, 36 | std::pair{ "Ammo"sv, "ammo"sv }, 37 | std::pair{ "Magic Effect"sv, "magic effects"sv }, 38 | std::pair{ "Potion"sv, "potions"sv }, 39 | std::pair{ "Scroll"sv, "scrolls"sv }, 40 | std::pair{ "Location"sv, "location"sv }, 41 | std::pair{ "Ingredient"sv, "ingredients"sv }, 42 | std::pair{ "Book"sv, "books"sv }, 43 | std::pair{ "Misc Item"sv, "misc items"sv }, 44 | std::pair{ "Key"sv, "keys"sv }, 45 | std::pair{ "Soul Gem"sv, "soul gems"sv }, 46 | std::pair{ "Spell"sv, "spells"sv }, 47 | std::pair{ "Activator"sv, "activators"sv }, 48 | std::pair{ "Flora"sv, "flora"sv }, 49 | std::pair{ "Furniture"sv, "furniture"sv }, 50 | std::pair{ "Race"sv, "races"sv }, 51 | std::pair{ "Talking Activator"sv, "talking activators"sv }, 52 | std::pair{ "Enchantment"sv, "enchantments"sv } 53 | }; 54 | 55 | TYPE GetType(const std::string& a_type); 56 | std::string_view GetType(TYPE a_type); 57 | } 58 | 59 | namespace FormType 60 | { 61 | inline constexpr std::array set{ 62 | //types 63 | RE::FormType::Armor, 64 | RE::FormType::Weapon, 65 | RE::FormType::Ammo, 66 | RE::FormType::MagicEffect, 67 | RE::FormType::AlchemyItem, 68 | RE::FormType::Scroll, 69 | RE::FormType::Location, 70 | RE::FormType::Ingredient, 71 | RE::FormType::Book, 72 | RE::FormType::Misc, 73 | RE::FormType::KeyMaster, 74 | RE::FormType::SoulGem, 75 | RE::FormType::Activator, 76 | RE::FormType::Flora, 77 | RE::FormType::Furniture, 78 | RE::FormType::Race, 79 | RE::FormType::TalkingActivator, 80 | RE::FormType::Enchantment, 81 | //filters 82 | RE::FormType::EffectShader, 83 | RE::FormType::ReferenceEffect, 84 | RE::FormType::ArtObject, 85 | RE::FormType::MusicType, 86 | RE::FormType::Faction, 87 | RE::FormType::Keyword, 88 | RE::FormType::Spell, // also type 89 | RE::FormType::Projectile, 90 | RE::FormType::EquipSlot, 91 | RE::FormType::VoiceType, 92 | RE::FormType::LeveledItem, 93 | RE::FormType::Water, 94 | RE::FormType::Perk, 95 | RE::FormType::FormList 96 | }; 97 | 98 | bool IsFilter(RE::FormType a_type); 99 | } 100 | 101 | namespace Archetype 102 | { 103 | inline const StringMap map{ 104 | { "None"sv, RE::EffectArchetype::kNone }, 105 | { "ValueMod"sv, RE::EffectArchetype::kValueModifier }, 106 | { "Script"sv, RE::EffectArchetype::kScript }, 107 | { "Dispel"sv, RE::EffectArchetype::kDispel }, 108 | { "CureDisease"sv, RE::EffectArchetype::kCureDisease }, 109 | { "Absorb"sv, RE::EffectArchetype::kAbsorb }, 110 | { "DualValueMod"sv, RE::EffectArchetype::kDualValueModifier }, 111 | { "Calm"sv, RE::EffectArchetype::kCalm }, 112 | { "Demoralize"sv, RE::EffectArchetype::kDemoralize }, 113 | { "Frenzy"sv, RE::EffectArchetype::kFrenzy }, 114 | { "Disarm"sv, RE::EffectArchetype::kDisarm }, 115 | { "CommandSummoned"sv, RE::EffectArchetype::kCommandSummoned }, 116 | { "Invisibility"sv, RE::EffectArchetype::kInvisibility }, 117 | { "Light"sv, RE::EffectArchetype::kLight }, 118 | { "Darkness"sv, RE::EffectArchetype::kDarkness }, 119 | { "NightEye"sv, RE::EffectArchetype::kNightEye }, 120 | { "Lock"sv, RE::EffectArchetype::kLock }, 121 | { "Open"sv, RE::EffectArchetype::kOpen }, 122 | { "BoundWeapon"sv, RE::EffectArchetype::kBoundWeapon }, 123 | { "SummonCreature"sv, RE::EffectArchetype::kSummonCreature }, 124 | { "DetectLife"sv, RE::EffectArchetype::kDetectLife }, 125 | { "Telekinesis"sv, RE::EffectArchetype::kTelekinesis }, 126 | { "Paralysis"sv, RE::EffectArchetype::kParalysis }, 127 | { "Reanimate"sv, RE::EffectArchetype::kReanimate }, 128 | { "SoulTrap"sv, RE::EffectArchetype::kSoulTrap }, 129 | { "TurnUndead"sv, RE::EffectArchetype::kTurnUndead }, 130 | { "Guide"sv, RE::EffectArchetype::kGuide }, 131 | { "WerewolfFeed"sv, RE::EffectArchetype::kWerewolfFeed }, 132 | { "CureParalysis"sv, RE::EffectArchetype::kCureParalysis }, 133 | { "CureAddiction"sv, RE::EffectArchetype::kCureAddiction }, 134 | { "CurePoison"sv, RE::EffectArchetype::kCurePoison }, 135 | { "Concussion"sv, RE::EffectArchetype::kConcussion }, 136 | { "ValueAndParts"sv, RE::EffectArchetype::kValueAndParts }, 137 | { "AccumulateMagnitude"sv, RE::EffectArchetype::kAccumulateMagnitude }, 138 | { "Stagger"sv, RE::EffectArchetype::kStagger }, 139 | { "PeakValueMod"sv, RE::EffectArchetype::kPeakValueModifier }, 140 | { "Cloak"sv, RE::EffectArchetype::kCloak }, 141 | { "Werewolf"sv, RE::EffectArchetype::kWerewolf }, 142 | { "SlowTime"sv, RE::EffectArchetype::kSlowTime }, 143 | { "Rally"sv, RE::EffectArchetype::kRally }, 144 | { "EnhanceWeapon"sv, RE::EffectArchetype::kEnhanceWeapon }, 145 | { "SpawnHazard"sv, RE::EffectArchetype::kSpawnHazard }, 146 | { "Etherealize"sv, RE::EffectArchetype::kEtherealize }, 147 | { "Banish"sv, RE::EffectArchetype::kBanish }, 148 | { "SpawnScriptedRef"sv, RE::EffectArchetype::kSpawnScriptedRef }, 149 | { "Disguise"sv, RE::EffectArchetype::kDisguise }, 150 | { "GrabActor"sv, RE::EffectArchetype::kGrabActor }, 151 | { "VampireLord"sv, RE::EffectArchetype::kVampireLord } 152 | }; 153 | } 154 | 155 | namespace ActorValue 156 | { 157 | inline const StringMap map{ 158 | { "Aggression"sv, RE::ActorValue::kAggression }, 159 | { "Confidence"sv, RE::ActorValue::kConfidence }, 160 | { "Energy"sv, RE::ActorValue::kEnergy }, 161 | { "Morality"sv, RE::ActorValue::kMorality }, 162 | { "Mood"sv, RE::ActorValue::kMood }, 163 | { "Assistance"sv, RE::ActorValue::kAssistance }, 164 | { "OneHanded"sv, RE::ActorValue::kOneHanded }, 165 | { "TwoHanded"sv, RE::ActorValue::kTwoHanded }, 166 | { "Marksman"sv, RE::ActorValue::kArchery }, 167 | { "Block"sv, RE::ActorValue::kBlock }, 168 | { "Smithing"sv, RE::ActorValue::kSmithing }, 169 | { "HeavyArmor"sv, RE::ActorValue::kHeavyArmor }, 170 | { "LightArmor"sv, RE::ActorValue::kLightArmor }, 171 | { "Pickpocket"sv, RE::ActorValue::kPickpocket }, 172 | { "Lockpicking"sv, RE::ActorValue::kLockpicking }, 173 | { "Sneak"sv, RE::ActorValue::kSneak }, 174 | { "Alchemy"sv, RE::ActorValue::kAlchemy }, 175 | { "Speechcraft"sv, RE::ActorValue::kSpeech }, 176 | { "Alteration"sv, RE::ActorValue::kAlteration }, 177 | { "Conjuration"sv, RE::ActorValue::kConjuration }, 178 | { "Destruction"sv, RE::ActorValue::kDestruction }, 179 | { "Illusion"sv, RE::ActorValue::kIllusion }, 180 | { "Restoration"sv, RE::ActorValue::kRestoration }, 181 | { "Enchanting"sv, RE::ActorValue::kEnchanting }, 182 | { "Health"sv, RE::ActorValue::kHealth }, 183 | { "Magicka"sv, RE::ActorValue::kMagicka }, 184 | { "Stamina"sv, RE::ActorValue::kStamina }, 185 | { "HealRate"sv, RE::ActorValue::kHealRate }, 186 | { "MagickaRate"sv, RE::ActorValue::kMagickaRate }, 187 | { "StaminaRate"sv, RE::ActorValue::kStaminaRate }, 188 | { "SpeedMult"sv, RE::ActorValue::kSpeedMult }, 189 | { "InventoryWeight"sv, RE::ActorValue::kInventoryWeight }, 190 | { "CarryWeight"sv, RE::ActorValue::kCarryWeight }, 191 | { "CritChance"sv, RE::ActorValue::kCriticalChance }, 192 | { "MeleeDamage"sv, RE::ActorValue::kMeleeDamage }, 193 | { "UnarmedDamage"sv, RE::ActorValue::kUnarmedDamage }, 194 | { "Mass"sv, RE::ActorValue::kMass }, 195 | { "VoicePoints"sv, RE::ActorValue::kVoicePoints }, 196 | { "VoiceRate"sv, RE::ActorValue::kVoiceRate }, 197 | { "DamageResist"sv, RE::ActorValue::kDamageResist }, 198 | { "PoisonResist"sv, RE::ActorValue::kPoisonResist }, 199 | { "FireResist"sv, RE::ActorValue::kResistFire }, 200 | { "ElectricResist"sv, RE::ActorValue::kResistShock }, 201 | { "FrostResist"sv, RE::ActorValue::kResistFrost }, 202 | { "MagicResist"sv, RE::ActorValue::kResistMagic }, 203 | { "DiseaseResist"sv, RE::ActorValue::kResistDisease }, 204 | { "PerceptionCondition"sv, RE::ActorValue::kPerceptionCondition }, 205 | { "EnduranceCondition"sv, RE::ActorValue::kEnduranceCondition }, 206 | { "LeftAttackCondition"sv, RE::ActorValue::kLeftAttackCondition }, 207 | { "RightAttackCondition"sv, RE::ActorValue::kRightAttackCondition }, 208 | { "LeftMobilityCondition"sv, RE::ActorValue::kLeftMobilityCondition }, 209 | { "RightMobilityCondition"sv, RE::ActorValue::kRightMobilityCondition }, 210 | { "BrainCondition"sv, RE::ActorValue::kBrainCondition }, 211 | { "Paralysis"sv, RE::ActorValue::kParalysis }, 212 | { "Invisibility"sv, RE::ActorValue::kInvisibility }, 213 | { "NightEye"sv, RE::ActorValue::kNightEye }, 214 | { "DetectLifeRange"sv, RE::ActorValue::kDetectLifeRange }, 215 | { "WaterBreathing"sv, RE::ActorValue::kWaterBreathing }, 216 | { "WaterWalking"sv, RE::ActorValue::kWaterWalking }, 217 | { "IgnoreCrippledLimbs"sv, RE::ActorValue::kIgnoreCrippledLimbs }, 218 | { "Fame"sv, RE::ActorValue::kFame }, 219 | { "Infamy"sv, RE::ActorValue::kInfamy }, 220 | { "JumpingBonus"sv, RE::ActorValue::kJumpingBonus }, 221 | { "WardPower"sv, RE::ActorValue::kWardPower }, 222 | { "RightItemCharge"sv, RE::ActorValue::kRightItemCharge }, 223 | { "ArmorPerks"sv, RE::ActorValue::kArmorPerks }, 224 | { "ShieldPerks"sv, RE::ActorValue::kShieldPerks }, 225 | { "WardDeflection"sv, RE::ActorValue::kWardDeflection }, 226 | { "Variable01"sv, RE::ActorValue::kVariable01 }, 227 | { "Variable02"sv, RE::ActorValue::kVariable02 }, 228 | { "Variable03"sv, RE::ActorValue::kVariable03 }, 229 | { "Variable04"sv, RE::ActorValue::kVariable04 }, 230 | { "Variable05"sv, RE::ActorValue::kVariable05 }, 231 | { "Variable06"sv, RE::ActorValue::kVariable06 }, 232 | { "Variable07"sv, RE::ActorValue::kVariable07 }, 233 | { "Variable08"sv, RE::ActorValue::kVariable08 }, 234 | { "Variable09"sv, RE::ActorValue::kVariable09 }, 235 | { "Variable10"sv, RE::ActorValue::kVariable10 }, 236 | { "BowSpeedBonus"sv, RE::ActorValue::kBowSpeedBonus }, 237 | { "FavorActive"sv, RE::ActorValue::kFavorActive }, 238 | { "FavorsPerDay"sv, RE::ActorValue::kFavorsPerDay }, 239 | { "FavorsPerDayTimer"sv, RE::ActorValue::kFavorsPerDayTimer }, 240 | { "LeftItemCharge"sv, RE::ActorValue::kLeftItemCharge }, 241 | { "AbsorbChance"sv, RE::ActorValue::kAbsorbChance }, 242 | { "Blindness"sv, RE::ActorValue::kBlindness }, 243 | { "WeaponSpeedMult"sv, RE::ActorValue::kWeaponSpeedMult }, 244 | { "ShoutRecoveryMult"sv, RE::ActorValue::kShoutRecoveryMult }, 245 | { "BowStaggerBonus"sv, RE::ActorValue::kBowStaggerBonus }, 246 | { "Telekinesis"sv, RE::ActorValue::kTelekinesis }, 247 | { "FavorPointsBonus"sv, RE::ActorValue::kFavorPointsBonus }, 248 | { "LastBribedIntimidated"sv, RE::ActorValue::kLastBribedIntimidated }, 249 | { "LastFlattered"sv, RE::ActorValue::kLastFlattered }, 250 | { "MovementNoiseMult"sv, RE::ActorValue::kMovementNoiseMult }, 251 | { "BypassVendorStolenCheck"sv, RE::ActorValue::kBypassVendorStolenCheck }, 252 | { "BypassVendorKeywordCheck"sv, RE::ActorValue::kBypassVendorKeywordCheck }, 253 | { "WaitingForPlayer"sv, RE::ActorValue::kWaitingForPlayer }, 254 | { "OneHandedMod"sv, RE::ActorValue::kOneHandedModifier }, 255 | { "TwoHandedMod"sv, RE::ActorValue::kTwoHandedModifier }, 256 | { "MarksmanMod"sv, RE::ActorValue::kMarksmanModifier }, 257 | { "BlockMod"sv, RE::ActorValue::kBlockModifier }, 258 | { "SmithingMod"sv, RE::ActorValue::kSmithingModifier }, 259 | { "HeavyArmorMod"sv, RE::ActorValue::kHeavyArmorModifier }, 260 | { "LightArmorMod"sv, RE::ActorValue::kLightArmorModifier }, 261 | { "PickPocketMod"sv, RE::ActorValue::kPickpocketModifier }, 262 | { "LockpickingMod"sv, RE::ActorValue::kLockpickingModifier }, 263 | { "SneakMod"sv, RE::ActorValue::kSneakingModifier }, 264 | { "AlchemyMod"sv, RE::ActorValue::kAlchemyModifier }, 265 | { "SpeechcraftMod"sv, RE::ActorValue::kSpeechcraftModifier }, 266 | { "AlterationMod"sv, RE::ActorValue::kAlterationModifier }, 267 | { "ConjurationMod"sv, RE::ActorValue::kConjurationModifier }, 268 | { "DestructionMod"sv, RE::ActorValue::kDestructionModifier }, 269 | { "IllusionMod"sv, RE::ActorValue::kIllusionModifier }, 270 | { "RestorationMod"sv, RE::ActorValue::kRestorationModifier }, 271 | { "EnchantingMod"sv, RE::ActorValue::kEnchantingModifier }, 272 | { "OneHandedSkillAdvance"sv, RE::ActorValue::kOneHandedSkillAdvance }, 273 | { "TwoHandedSkillAdvance"sv, RE::ActorValue::kTwoHandedSkillAdvance }, 274 | { "MarksmanSkillAdvance"sv, RE::ActorValue::kMarksmanSkillAdvance }, 275 | { "BlockSkillAdvance"sv, RE::ActorValue::kBlockSkillAdvance }, 276 | { "SmithingSkillAdvance"sv, RE::ActorValue::kSmithingSkillAdvance }, 277 | { "HeavyArmorSkillAdvance"sv, RE::ActorValue::kHeavyArmorSkillAdvance }, 278 | { "LightArmorSkillAdvance"sv, RE::ActorValue::kLightArmorSkillAdvance }, 279 | { "PickPocketSkillAdvance"sv, RE::ActorValue::kPickpocketSkillAdvance }, 280 | { "LockpickingSkillAdvance"sv, RE::ActorValue::kLockpickingSkillAdvance }, 281 | { "SneakSkillAdvance"sv, RE::ActorValue::kSneakingSkillAdvance }, 282 | { "AlchemySkillAdvance"sv, RE::ActorValue::kAlchemySkillAdvance }, 283 | { "SpeechcraftSkillAdvance"sv, RE::ActorValue::kSpeechcraftSkillAdvance }, 284 | { "AlterationSkillAdvance"sv, RE::ActorValue::kAlterationSkillAdvance }, 285 | { "ConjurationSkillAdvance"sv, RE::ActorValue::kConjurationSkillAdvance }, 286 | { "DestructionSkillAdvance"sv, RE::ActorValue::kDestructionSkillAdvance }, 287 | { "IllusionSkillAdvance"sv, RE::ActorValue::kIllusionSkillAdvance }, 288 | { "RestorationSkillAdvance"sv, RE::ActorValue::kRestorationSkillAdvance }, 289 | { "EnchantingSkillAdvance"sv, RE::ActorValue::kEnchantingSkillAdvance }, 290 | { "LeftWeaponSpeedMult"sv, RE::ActorValue::kLeftWeaponSpeedMultiply }, 291 | { "DragonSouls"sv, RE::ActorValue::kDragonSouls }, 292 | { "CombatHealthRegenMult"sv, RE::ActorValue::kCombatHealthRegenMultiply }, 293 | { "OneHandedPowerMod"sv, RE::ActorValue::kOneHandedPowerModifier }, 294 | { "TwoHandedPowerMod"sv, RE::ActorValue::kTwoHandedPowerModifier }, 295 | { "MarksmanPowerMod"sv, RE::ActorValue::kMarksmanPowerModifier }, 296 | { "BlockPowerMod"sv, RE::ActorValue::kBlockPowerModifier }, 297 | { "SmithingPowerMod"sv, RE::ActorValue::kSmithingPowerModifier }, 298 | { "HeavyArmorPowerMod"sv, RE::ActorValue::kHeavyArmorPowerModifier }, 299 | { "LightArmorPowerMod"sv, RE::ActorValue::kLightArmorPowerModifier }, 300 | { "PickPocketPowerMod"sv, RE::ActorValue::kPickpocketPowerModifier }, 301 | { "LockpickingPowerMod"sv, RE::ActorValue::kLockpickingPowerModifier }, 302 | { "SneakPowerMod"sv, RE::ActorValue::kSneakingPowerModifier }, 303 | { "AlchemyPowerMod"sv, RE::ActorValue::kAlchemyPowerModifier }, 304 | { "SpeechcraftPowerMod"sv, RE::ActorValue::kSpeechcraftPowerModifier }, 305 | { "AlterationPowerMod"sv, RE::ActorValue::kAlterationPowerModifier }, 306 | { "ConjurationPowerMod"sv, RE::ActorValue::kConjurationPowerModifier }, 307 | { "DestructionPowerMod"sv, RE::ActorValue::kDestructionPowerModifier }, 308 | { "IllusionPowerMod"sv, RE::ActorValue::kIllusionPowerModifier }, 309 | { "RestorationPowerMod"sv, RE::ActorValue::kRestorationPowerModifier }, 310 | { "EnchantingPowerMod"sv, RE::ActorValue::kEnchantingPowerModifier }, 311 | { "DragonRend"sv, RE::ActorValue::kDragonRend }, 312 | { "AttackDamageMult"sv, RE::ActorValue::kAttackDamageMult }, 313 | { "HealRateMult"sv, RE::ActorValue::kHealRateMult }, 314 | { "MagickaRateMult"sv, RE::ActorValue::kMagickaRateMult }, 315 | { "StaminaRateMult"sv, RE::ActorValue::kStaminaRateMult }, 316 | { "WerewolfPerks"sv, RE::ActorValue::kWerewolfPerks }, 317 | { "VampirePerks"sv, RE::ActorValue::kVampirePerks }, 318 | { "GrabActorOffset"sv, RE::ActorValue::kGrabActorOffset }, 319 | { "Grabbed"sv, RE::ActorValue::kGrabbed }, 320 | { "DEPRECATED05"sv, RE::ActorValue::kDEPRECATED05 }, 321 | { "ReflectDamage"sv, RE::ActorValue::kReflectDamage }, 322 | }; 323 | 324 | inline const Map r_map{ 325 | { RE::ActorValue::kAggression, "Aggression"sv }, 326 | { RE::ActorValue::kConfidence, "Confidence"sv }, 327 | { RE::ActorValue::kEnergy, "Energy"sv }, 328 | { RE::ActorValue::kMorality, "Morality"sv }, 329 | { RE::ActorValue::kMood, "Mood"sv }, 330 | { RE::ActorValue::kAssistance, "Assistance"sv }, 331 | { RE::ActorValue::kOneHanded, "OneHanded"sv }, 332 | { RE::ActorValue::kTwoHanded, "TwoHanded"sv }, 333 | { RE::ActorValue::kArchery, "Marksman"sv }, 334 | { RE::ActorValue::kBlock, "Block"sv }, 335 | { RE::ActorValue::kSmithing, "Smithing"sv }, 336 | { RE::ActorValue::kHeavyArmor, "HeavyArmor"sv }, 337 | { RE::ActorValue::kLightArmor, "LightArmor"sv }, 338 | { RE::ActorValue::kPickpocket, "Pickpocket"sv }, 339 | { RE::ActorValue::kLockpicking, "Lockpicking"sv }, 340 | { RE::ActorValue::kSneak, "Sneak"sv }, 341 | { RE::ActorValue::kAlchemy, "Alchemy"sv }, 342 | { RE::ActorValue::kSpeech, "Speechcraft"sv }, 343 | { RE::ActorValue::kAlteration, "Alteration"sv }, 344 | { RE::ActorValue::kConjuration, "Conjuration"sv }, 345 | { RE::ActorValue::kDestruction, "Destruction"sv }, 346 | { RE::ActorValue::kIllusion, "Illusion"sv }, 347 | { RE::ActorValue::kRestoration, "Restoration"sv }, 348 | { RE::ActorValue::kEnchanting, "Enchanting"sv }, 349 | { RE::ActorValue::kHealth, "Health"sv }, 350 | { RE::ActorValue::kMagicka, "Magicka"sv }, 351 | { RE::ActorValue::kStamina, "Stamina"sv }, 352 | { RE::ActorValue::kHealRate, "HealRate"sv }, 353 | { RE::ActorValue::kMagickaRate, "MagickaRate"sv }, 354 | { RE::ActorValue::kStaminaRate, "StaminaRate"sv }, 355 | { RE::ActorValue::kSpeedMult, "SpeedMult"sv }, 356 | { RE::ActorValue::kInventoryWeight, "InventoryWeight"sv }, 357 | { RE::ActorValue::kCarryWeight, "CarryWeight"sv }, 358 | { RE::ActorValue::kCriticalChance, "CritChance"sv }, 359 | { RE::ActorValue::kMeleeDamage, "MeleeDamage"sv }, 360 | { RE::ActorValue::kUnarmedDamage, "UnarmedDamage"sv }, 361 | { RE::ActorValue::kMass, "Mass"sv }, 362 | { RE::ActorValue::kVoicePoints, "VoicePoints"sv }, 363 | { RE::ActorValue::kVoiceRate, "VoiceRate"sv }, 364 | { RE::ActorValue::kDamageResist, "DamageResist"sv }, 365 | { RE::ActorValue::kPoisonResist, "PoisonResist"sv }, 366 | { RE::ActorValue::kResistFire, "FireResist"sv }, 367 | { RE::ActorValue::kResistShock, "ElectricResist"sv }, 368 | { RE::ActorValue::kResistFrost, "FrostResist"sv }, 369 | { RE::ActorValue::kResistMagic, "MagicResist"sv }, 370 | { RE::ActorValue::kResistDisease, "DiseaseResist"sv }, 371 | { RE::ActorValue::kPerceptionCondition, "PerceptionCondition"sv }, 372 | { RE::ActorValue::kEnduranceCondition, "EnduranceCondition"sv }, 373 | { RE::ActorValue::kLeftAttackCondition, "LeftAttackCondition"sv }, 374 | { RE::ActorValue::kRightAttackCondition, "RightAttackCondition"sv }, 375 | { RE::ActorValue::kLeftMobilityCondition, "LeftMobilityCondition"sv }, 376 | { RE::ActorValue::kRightMobilityCondition, "RightMobilityCondition"sv }, 377 | { RE::ActorValue::kBrainCondition, "BrainCondition"sv }, 378 | { RE::ActorValue::kParalysis, "Paralysis"sv }, 379 | { RE::ActorValue::kInvisibility, "Invisibility"sv }, 380 | { RE::ActorValue::kNightEye, "NightEye"sv }, 381 | { RE::ActorValue::kDetectLifeRange, "DetectLifeRange"sv }, 382 | { RE::ActorValue::kWaterBreathing, "WaterBreathing"sv }, 383 | { RE::ActorValue::kWaterWalking, "WaterWalking"sv }, 384 | { RE::ActorValue::kIgnoreCrippledLimbs, "IgnoreCrippledLimbs"sv }, 385 | { RE::ActorValue::kFame, "Fame"sv }, 386 | { RE::ActorValue::kInfamy, "Infamy"sv }, 387 | { RE::ActorValue::kJumpingBonus, "JumpingBonus"sv }, 388 | { RE::ActorValue::kWardPower, "WardPower"sv }, 389 | { RE::ActorValue::kRightItemCharge, "RightItemCharge"sv }, 390 | { RE::ActorValue::kArmorPerks, "ArmorPerks"sv }, 391 | { RE::ActorValue::kShieldPerks, "ShieldPerks"sv }, 392 | { RE::ActorValue::kWardDeflection, "WardDeflection"sv }, 393 | { RE::ActorValue::kVariable01, "Variable01"sv }, 394 | { RE::ActorValue::kVariable02, "Variable02"sv }, 395 | { RE::ActorValue::kVariable03, "Variable03"sv }, 396 | { RE::ActorValue::kVariable04, "Variable04"sv }, 397 | { RE::ActorValue::kVariable05, "Variable05"sv }, 398 | { RE::ActorValue::kVariable06, "Variable06"sv }, 399 | { RE::ActorValue::kVariable07, "Variable07"sv }, 400 | { RE::ActorValue::kVariable08, "Variable08"sv }, 401 | { RE::ActorValue::kVariable09, "Variable09"sv }, 402 | { RE::ActorValue::kVariable10, "Variable10"sv }, 403 | { RE::ActorValue::kBowSpeedBonus, "BowSpeedBonus"sv }, 404 | { RE::ActorValue::kFavorActive, "FavorActive"sv }, 405 | { RE::ActorValue::kFavorsPerDay, "FavorsPerDay"sv }, 406 | { RE::ActorValue::kFavorsPerDayTimer, "FavorsPerDayTimer"sv }, 407 | { RE::ActorValue::kLeftItemCharge, "LeftItemCharge"sv }, 408 | { RE::ActorValue::kAbsorbChance, "AbsorbChance"sv }, 409 | { RE::ActorValue::kBlindness, "Blindness"sv }, 410 | { RE::ActorValue::kWeaponSpeedMult, "WeaponSpeedMult"sv }, 411 | { RE::ActorValue::kShoutRecoveryMult, "ShoutRecoveryMult"sv }, 412 | { RE::ActorValue::kBowStaggerBonus, "BowStaggerBonus"sv }, 413 | { RE::ActorValue::kTelekinesis, "Telekinesis"sv }, 414 | { RE::ActorValue::kFavorPointsBonus, "FavorPointsBonus"sv }, 415 | { RE::ActorValue::kLastBribedIntimidated, "LastBribedIntimidated"sv }, 416 | { RE::ActorValue::kLastFlattered, "LastFlattered"sv }, 417 | { RE::ActorValue::kMovementNoiseMult, "MovementNoiseMult"sv }, 418 | { RE::ActorValue::kBypassVendorStolenCheck, "BypassVendorStolenCheck"sv }, 419 | { RE::ActorValue::kBypassVendorKeywordCheck, "BypassVendorKeywordCheck"sv }, 420 | { RE::ActorValue::kWaitingForPlayer, "WaitingForPlayer"sv }, 421 | { RE::ActorValue::kOneHandedModifier, "OneHandedMod"sv }, 422 | { RE::ActorValue::kTwoHandedModifier, "TwoHandedMod"sv }, 423 | { RE::ActorValue::kMarksmanModifier, "MarksmanMod"sv }, 424 | { RE::ActorValue::kBlockModifier, "BlockMod"sv }, 425 | { RE::ActorValue::kSmithingModifier, "SmithingMod"sv }, 426 | { RE::ActorValue::kHeavyArmorModifier, "HeavyArmorMod"sv }, 427 | { RE::ActorValue::kLightArmorModifier, "LightArmorMod"sv }, 428 | { RE::ActorValue::kPickpocketModifier, "PickPocketMod"sv }, 429 | { RE::ActorValue::kLockpickingModifier, "LockpickingMod"sv }, 430 | { RE::ActorValue::kSneakingModifier, "SneakMod"sv }, 431 | { RE::ActorValue::kAlchemyModifier, "AlchemyMod"sv }, 432 | { RE::ActorValue::kSpeechcraftModifier, "SpeechcraftMod"sv }, 433 | { RE::ActorValue::kAlterationModifier, "AlterationMod"sv }, 434 | { RE::ActorValue::kConjurationModifier, "ConjurationMod"sv }, 435 | { RE::ActorValue::kDestructionModifier, "DestructionMod"sv }, 436 | { RE::ActorValue::kIllusionModifier, "IllusionMod"sv }, 437 | { RE::ActorValue::kRestorationModifier, "RestorationMod"sv }, 438 | { RE::ActorValue::kEnchantingModifier, "EnchantingMod"sv }, 439 | { RE::ActorValue::kOneHandedSkillAdvance, "OneHandedSkillAdvance"sv }, 440 | { RE::ActorValue::kTwoHandedSkillAdvance, "TwoHandedSkillAdvance"sv }, 441 | { RE::ActorValue::kMarksmanSkillAdvance, "MarksmanSkillAdvance"sv }, 442 | { RE::ActorValue::kBlockSkillAdvance, "BlockSkillAdvance"sv }, 443 | { RE::ActorValue::kSmithingSkillAdvance, "SmithingSkillAdvance"sv }, 444 | { RE::ActorValue::kHeavyArmorSkillAdvance, "HeavyArmorSkillAdvance"sv }, 445 | { RE::ActorValue::kLightArmorSkillAdvance, "LightArmorSkillAdvance"sv }, 446 | { RE::ActorValue::kPickpocketSkillAdvance, "PickPocketSkillAdvance"sv }, 447 | { RE::ActorValue::kLockpickingSkillAdvance, "LockpickingSkillAdvance"sv }, 448 | { RE::ActorValue::kSneakingSkillAdvance, "SneakSkillAdvance"sv }, 449 | { RE::ActorValue::kAlchemySkillAdvance, "AlchemySkillAdvance"sv }, 450 | { RE::ActorValue::kSpeechcraftSkillAdvance, "SpeechcraftSkillAdvance"sv }, 451 | { RE::ActorValue::kAlterationSkillAdvance, "AlterationSkillAdvance"sv }, 452 | { RE::ActorValue::kConjurationSkillAdvance, "ConjurationSkillAdvance"sv }, 453 | { RE::ActorValue::kDestructionSkillAdvance, "DestructionSkillAdvance"sv }, 454 | { RE::ActorValue::kIllusionSkillAdvance, "IllusionSkillAdvance"sv }, 455 | { RE::ActorValue::kRestorationSkillAdvance, "RestorationSkillAdvance"sv }, 456 | { RE::ActorValue::kEnchantingSkillAdvance, "EnchantingSkillAdvance"sv }, 457 | { RE::ActorValue::kLeftWeaponSpeedMultiply, "LeftWeaponSpeedMult"sv }, 458 | { RE::ActorValue::kDragonSouls, "DragonSouls"sv }, 459 | { RE::ActorValue::kCombatHealthRegenMultiply, "CombatHealthRegenMult"sv }, 460 | { RE::ActorValue::kOneHandedPowerModifier, "OneHandedPowerMod"sv }, 461 | { RE::ActorValue::kTwoHandedPowerModifier, "TwoHandedPowerMod"sv }, 462 | { RE::ActorValue::kMarksmanPowerModifier, "MarksmanPowerMod"sv }, 463 | { RE::ActorValue::kBlockPowerModifier, "BlockPowerMod"sv }, 464 | { RE::ActorValue::kSmithingPowerModifier, "SmithingPowerMod"sv }, 465 | { RE::ActorValue::kHeavyArmorPowerModifier, "HeavyArmorPowerMod"sv }, 466 | { RE::ActorValue::kLightArmorPowerModifier, "LightArmorPowerMod"sv }, 467 | { RE::ActorValue::kPickpocketPowerModifier, "PickPocketPowerMod"sv }, 468 | { RE::ActorValue::kLockpickingPowerModifier, "LockpickingPowerMod"sv }, 469 | { RE::ActorValue::kSneakingPowerModifier, "SneakPowerMod"sv }, 470 | { RE::ActorValue::kAlchemyPowerModifier, "AlchemyPowerMod"sv }, 471 | { RE::ActorValue::kSpeechcraftPowerModifier, "SpeechcraftPowerMod"sv }, 472 | { RE::ActorValue::kAlterationPowerModifier, "AlterationPowerMod"sv }, 473 | { RE::ActorValue::kConjurationPowerModifier, "ConjurationPowerMod"sv }, 474 | { RE::ActorValue::kDestructionPowerModifier, "DestructionPowerMod"sv }, 475 | { RE::ActorValue::kIllusionPowerModifier, "IllusionPowerMod"sv }, 476 | { RE::ActorValue::kRestorationPowerModifier, "RestorationPowerMod"sv }, 477 | { RE::ActorValue::kEnchantingPowerModifier, "EnchantingPowerMod"sv }, 478 | { RE::ActorValue::kDragonRend, "DragonRend"sv }, 479 | { RE::ActorValue::kAttackDamageMult, "AttackDamageMult"sv }, 480 | { RE::ActorValue::kHealRateMult, "HealRateMult"sv }, 481 | { RE::ActorValue::kMagickaRateMult, "MagickaRateMult"sv }, 482 | { RE::ActorValue::kStaminaRateMult, "StaminaRateMult"sv }, 483 | { RE::ActorValue::kWerewolfPerks, "WerewolfPerks"sv }, 484 | { RE::ActorValue::kVampirePerks, "VampirePerks"sv }, 485 | { RE::ActorValue::kGrabActorOffset, "GrabActorOffset"sv }, 486 | { RE::ActorValue::kGrabbed, "Grabbed"sv }, 487 | { RE::ActorValue::kDEPRECATED05, "DEPRECATED05"sv }, 488 | { RE::ActorValue::kReflectDamage, "ReflectDamage"sv }, 489 | }; 490 | 491 | std::string_view GetActorValue(RE::ActorValue a_av); 492 | RE::ActorValue GetActorValue(std::string_view a_av); 493 | 494 | RE::ActorValue GetAssociatedSkill(RE::MagicItem* a_spell); 495 | } 496 | } 497 | namespace EDID = clib_util::editorID; 498 | namespace ITEM = Cache::Item; 499 | namespace AV = Cache::ActorValue; 500 | namespace ARCHETYPE = Cache::Archetype; 501 | -------------------------------------------------------------------------------- /include/Defs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Keyword = formID~esp(OR)keywordEditorID|type|strings,formIDs(OR)editorIDs|traits|chance 4 | 5 | // for visting variants 6 | template 7 | struct overload : Ts... 8 | { 9 | using Ts::operator()...; 10 | }; 11 | 12 | using FormModPair = distribution::formid_pair; 13 | // std::variant 14 | using FormIDOrString = distribution::record; 15 | 16 | template 17 | struct Filters 18 | { 19 | std::vector ALL{}; 20 | std::vector NOT{}; 21 | std::vector MATCH{}; 22 | std::vector ANY{}; 23 | }; 24 | 25 | using RawVec = std::vector; 26 | using RawFilters = Filters; 27 | 28 | using FormOrString = std::variant< 29 | RE::TESForm*, // form 30 | const RE::TESFile*, // mod 31 | std::string>; // string 32 | using ProcessedVec = std::vector; 33 | using ProcessedFilters = Filters; 34 | 35 | using Chance = float; 36 | 37 | /// A standardized way of converting any object to string. 38 | /// 39 | ///

40 | /// Overload `operator<<` to provide custom formatting for your value. 41 | /// Alternatively, specialize this method and provide your own implementation. 42 | ///

43 | template 44 | std::string describe(Value value) 45 | { 46 | std::ostringstream os; 47 | os << value; 48 | return os.str(); 49 | } 50 | 51 | inline std::ostream& operator<<(std::ostream& os, const RE::TESFile* file) 52 | { 53 | os << file->fileName; 54 | return os; 55 | } 56 | 57 | inline std::ostream& operator<<(std::ostream& os, const RE::TESForm* form) 58 | { 59 | if (const auto& edid = EDID::get_editorID(form); !edid.empty()) { 60 | os << edid << " "; 61 | } 62 | os << "[" 63 | << std::to_string(form->GetFormType()) 64 | << ":" 65 | << std::setfill('0') 66 | << std::setw(sizeof(RE::FormID) * 2) 67 | << std::uppercase 68 | << std::hex 69 | << form->GetFormID() 70 | << "]"; 71 | 72 | return os; 73 | } 74 | -------------------------------------------------------------------------------- /include/DependencyResolver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | /// DependencyResolver builds a dependency graph for any arbitrary Values 6 | /// and resolves it into a vector of the Values in the order 7 | /// that ensures that dependent Values are placed after those they depend on. 8 | ///

9 | /// Note: If custom Value type does not define overloaded operator `<` you may provide a Comparator functor, 10 | /// that will be used to control the order in which Values will be processed. 11 | /// This Comparator should indicate whether one value is less than the other. 12 | ///

13 | template > 14 | class DependencyResolver 15 | { 16 | /// An object that represents each unique value passed to DependencyResolver 17 | /// and carries information about it's dependencies. 18 | struct Node 19 | { 20 | const Value value; 21 | 22 | /// Comparator functor that is used to compare Value of two Nodes. 23 | const Comparator& comparator; 24 | 25 | /// A list of all other nodes that current node depends on. 26 | /// 27 | ///

28 | /// Use dependsOn() method to determine whether current node depends on another. 29 | ///

30 | std::set dependencies{}; 31 | 32 | /// Flag that is used by DependencyResolver during resolution process 33 | /// to detect whether given Node was already resolved. 34 | /// 35 | ///

36 | /// This helps avoid unnecessary iterations over the same nodes, 37 | /// which might occur when it is a part of several dependencies lists. 38 | ///

39 | bool isResolved; 40 | 41 | Node(Value value, const Comparator& comparator) : 42 | value(std::move(value)), comparator(comparator), isResolved(false) {} 43 | ~Node() = default; 44 | 45 | bool dependsOn(Node* const node, std::stack& path) const 46 | { 47 | if (dependencies.empty()) { 48 | return false; 49 | } 50 | 51 | if (dependencies.contains(node)) { 52 | path.push(node->value); 53 | path.push(this->value); 54 | return true; 55 | } 56 | 57 | if (auto nextNode = std::find_if(dependencies.begin(), dependencies.end(), [&](const auto& dependency) { return dependency->dependsOn(node, path); }); nextNode != dependencies.end()) { 58 | path.push(this->value); 59 | return true; 60 | } 61 | 62 | return false; 63 | } 64 | 65 | /// May throw an exception if Directed Acyclic Graph rules will be violated. 66 | void addDependency(Node* node) 67 | { 68 | assert(node != nullptr); 69 | 70 | if (this == node) { 71 | throw SelfReferenceDependencyException(this->value); 72 | } 73 | 74 | std::stack cyclicPath; 75 | 76 | if (node->dependsOn(this, cyclicPath)) { 77 | cyclicPath.push(this->value); 78 | throw CyclicDependencyException(this->value, node->value, cyclicPath); 79 | } 80 | 81 | std::stack superfluousPath; 82 | 83 | if (this->dependsOn(node, superfluousPath)) { 84 | throw SuperfluousDependencyException(this->value, node->value, superfluousPath); 85 | } 86 | 87 | if (!dependencies.emplace(node).second) { 88 | // this->dependsOn(node) should always detect when duplicated dependency is added, but just to be safe.. :) 89 | throw SuperfluousDependencyException(this->value, node->value, {}); 90 | } 91 | } 92 | 93 | bool operator==(const Node& other) const 94 | { 95 | return this->value == other.value; 96 | } 97 | 98 | bool operator<(const Node& other) const 99 | { 100 | if (this->dependencies.size() < other.dependencies.size()) { 101 | return true; 102 | } 103 | if (this->dependencies.size() == other.dependencies.size()) { 104 | return comparator(this->value, other.value); 105 | } 106 | 107 | return false; 108 | } 109 | }; 110 | 111 | /// Custom functor that invokes Node's overloaded operator `<`. 112 | struct node_less 113 | { 114 | bool operator()(const Node* lhs, const Node* rhs) const 115 | { 116 | return *lhs < *rhs; 117 | } 118 | }; 119 | 120 | /// A comparator object that will be used to determine whether one value is less than the other. 121 | /// This comparator is used to determine ordering in which nodes should be processed for the optimal resolution. 122 | const Comparator comparator; 123 | 124 | /// A container that holds nodes associated with each value that was added to DependencyResolver. 125 | Map nodes{}; 126 | 127 | /// Looks up dependencies of a single node and places it into the result vector afterwards. 128 | void resolveNode(Node* const node, std::vector& result) const 129 | { 130 | if (node->isResolved) { 131 | return; 132 | } 133 | for (const auto& dependency : node->dependencies) { 134 | resolveNode(dependency, result); 135 | } 136 | result.push_back(node->value); 137 | node->isResolved = true; 138 | } 139 | 140 | public: 141 | DependencyResolver(const Comparator comparator = Comparator()) : 142 | comparator(std::move(comparator)) {} 143 | 144 | DependencyResolver(const std::vector& values, 145 | const Comparator comparator = Comparator()) : 146 | DependencyResolver(comparator) 147 | { 148 | for (const auto& value : values) { 149 | nodes.try_emplace(value, new Node(value, comparator)); 150 | } 151 | } 152 | 153 | ~DependencyResolver() 154 | { 155 | for (const auto& pair : nodes) { 156 | delete pair.second; 157 | } 158 | } 159 | 160 | /// Attempts to create a dependency rule between `parent` and `dependency` objects. 161 | /// If either of those objects were not present in the original vector they'll be added in-place. 162 | /// 163 | ///

164 | /// May throw one of the following exceptions when Directed Acyclic Graph rules are being violated. 165 | /// 166 | /// CyclicDependencyException 167 | /// SuperfluousDependencyException 168 | /// SelfReferenceDependencyException 169 | /// 170 | ///

171 | void addDependency(const Value& parent, const Value& dependency) 172 | { 173 | Node* parentNode; 174 | Node* dependencyNode; 175 | 176 | if (const auto it = nodes.find(parent); it != nodes.end()) { 177 | parentNode = it->second; 178 | } else { 179 | parentNode = new Node(parent, comparator); 180 | nodes.try_emplace(parent, parentNode); 181 | } 182 | 183 | if (const auto it = nodes.find(dependency); it != nodes.end()) { 184 | dependencyNode = it->second; 185 | } else { 186 | dependencyNode = new Node(dependency, comparator); 187 | nodes.try_emplace(dependency, dependencyNode); 188 | } 189 | 190 | if (parentNode && dependencyNode) { 191 | parentNode->addDependency(dependencyNode); 192 | } 193 | } 194 | 195 | /// Wrapper function for addDependency 196 | void AddDependency(const Value& lhs, const Value& rhs) 197 | { 198 | try { 199 | addDependency(lhs, rhs); 200 | } catch (SelfReferenceDependencyException& e) { 201 | buffered_logger::warn("\t\tINFO - {} is referencing itself", describe(e.current)); 202 | } catch (CyclicDependencyException& e) { 203 | std::ostringstream os; 204 | os << e.path.top(); 205 | auto path = e.path; 206 | path.pop(); 207 | while (!path.empty()) { 208 | os << " -> " << path.top(); 209 | path.pop(); 210 | } 211 | buffered_logger::warn("\t\tINFO - {} and {} may depend on each other. Distribution might not work as expected. Ignore if using names as wildcards", describe(e.first), describe(e.second)); 212 | buffered_logger::warn("\t\t\tFull path: {}", os.str()); 213 | } catch (...) { 214 | // we'll ignore other exceptions 215 | } 216 | } 217 | 218 | /// Add an isolated object to the resolver's graph. 219 | /// 220 | /// Isolated object is the one that doesn't have any dependencies on the others. 221 | /// However, dependencies can be added later using addDependency() method. 222 | void AddIsolated(const Value& value) 223 | { 224 | if (!nodes.contains(value)) { 225 | nodes.try_emplace(value, new Node(value, comparator)); 226 | } 227 | } 228 | 229 | /// Creates a vector that contains all values sorted topologically according to dependencies provided with addDependency method. 230 | [[nodiscard]] std::vector Resolve() const 231 | { 232 | std::vector result; 233 | 234 | /// A vector of nodes that are ordered in a way that would make resolution the most efficient 235 | /// by reducing number of lookups for all nodes to resolved the graph. 236 | std::vector orderedNodes; 237 | 238 | std::transform(nodes.begin(), nodes.end(), std::back_inserter(orderedNodes), [](const auto& pair) { 239 | return pair.second; 240 | }); 241 | // Sort nodes in correct order of processing. 242 | std::sort(orderedNodes.begin(), orderedNodes.end(), node_less()); 243 | 244 | for (const auto& node : orderedNodes) { 245 | node->isResolved = false; 246 | } 247 | 248 | for (const auto& node : orderedNodes) { 249 | resolveNode(node, result); 250 | } 251 | 252 | return result; 253 | } 254 | 255 | /// An exception thrown when DependencyResolver attempts to add current value as a dependency of itself. 256 | struct SelfReferenceDependencyException : std::exception 257 | { 258 | SelfReferenceDependencyException(const Value& current) : 259 | current(current) 260 | {} 261 | 262 | const Value& current; 263 | }; 264 | 265 | /// An exception thrown when DependencyResolver attempts to add a dependency that will create a cycle (e.g. A -> B -> A) 266 | struct CyclicDependencyException : std::exception 267 | { 268 | CyclicDependencyException(Value first, Value second, std::stack path) : 269 | first(std::move(first)), 270 | second(std::move(second)), 271 | path(std::move(path)) 272 | {} 273 | 274 | const Value first; 275 | const Value second; 276 | const std::stack path; 277 | }; 278 | 279 | /// An exception thrown when DependencyResolver attempts to add a dependency that can be inferred implicitly. 280 | /// For example if (A -> B) and (B -> C) then (A -> C) is a superfluous dependency that is inferred from the first two. 281 | struct SuperfluousDependencyException : std::exception 282 | { 283 | SuperfluousDependencyException(Value current, Value superfluous, std::stack path) : 284 | current(std::move(current)), 285 | superfluous(std::move(superfluous)), 286 | path(std::move(path)) 287 | {} 288 | 289 | const Value current; 290 | const Value superfluous; 291 | const std::stack path; 292 | }; 293 | }; 294 | -------------------------------------------------------------------------------- /include/Distribute.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "KeywordData.h" 4 | 5 | namespace Distribute 6 | { 7 | using namespace Keyword; 8 | 9 | template 10 | void distribute(T* a_item, KeywordDataVec& a_keywords) 11 | { 12 | Item::Data itemData(a_item); 13 | 14 | std::vector processedKeywords; 15 | processedKeywords.reserve(a_keywords.size()); 16 | for (auto& [count, keyword, filters] : a_keywords) { 17 | if (itemData.PassedFilters(keyword, filters)) { 18 | processedKeywords.emplace_back(keyword); 19 | ++count; 20 | } 21 | } 22 | 23 | if (!processedKeywords.empty()) { 24 | a_item->AddKeywords(processedKeywords); 25 | } 26 | } 27 | 28 | template 29 | void distribute(Distributable& a_keywords) 30 | { 31 | if (a_keywords) { 32 | auto& keywords = a_keywords.GetKeywords(); 33 | for (auto& item : RE::TESDataHandler::GetSingleton()->GetFormArray()) { 34 | if (item) { 35 | distribute(item, keywords); 36 | } 37 | } 38 | } 39 | } 40 | 41 | template 42 | void log_keyword_count(const Distributable& a_keywords) 43 | { 44 | if (a_keywords) { 45 | logger::info("{}", a_keywords.GetTypeString()); 46 | 47 | const auto formArraySize = RE::TESDataHandler::GetSingleton()->GetFormArray().size(); 48 | 49 | // Group the same entries together to show total number of distributed records in the log.t 50 | tsl::ordered_map sums{}; 51 | for (auto& keywordData : a_keywords.GetKeywords()) { 52 | auto it = sums.find(keywordData.keyword->GetFormID()); 53 | if (it != sums.end()) { 54 | it.value().count += keywordData.count; 55 | } else { 56 | sums.insert({ keywordData.keyword->GetFormID(), keywordData }); 57 | } 58 | } 59 | 60 | for (auto& entry : sums | std::views::values) { 61 | auto& [count, keyword, filters] = entry; 62 | if (const auto file = keyword->GetFile(0)) { 63 | logger::info("\t{} [0x{:X}~{}] added to {}/{}", keyword->GetFormEditorID(), keyword->GetLocalFormID(), file->GetFilename(), count, formArraySize); 64 | } else { 65 | logger::info("\t{} [0x{:X}] added to {}/{}", keyword->GetFormEditorID(), keyword->GetFormID(), count, formArraySize); 66 | } 67 | } 68 | sums.clear(); 69 | } 70 | } 71 | 72 | void AddKeywords(); 73 | } 74 | -------------------------------------------------------------------------------- /include/ExclusiveGroups.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "LookupConfigs.h" 4 | 5 | namespace ExclusiveGroups 6 | { 7 | namespace INI 8 | { 9 | struct RawExclusiveGroup 10 | { 11 | std::string name{}; 12 | 13 | /// Raw filters in RawExclusiveGroup only use NOT and MATCH, there is no meaning for ALL, so it's ignored. 14 | Filters formIDs{}; 15 | std::string path{}; 16 | }; 17 | 18 | using ExclusiveGroupsVec = std::vector; 19 | 20 | /// 21 | /// A list of RawExclusiveGroups that will be processed along with configs. 22 | /// 23 | inline ExclusiveGroupsVec exclusiveGroups{}; 24 | 25 | bool TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path); 26 | } 27 | 28 | using Group = std::string; 29 | using KeywordGroupMap = Map>; 30 | using GroupKeywordsMap = Map>; 31 | 32 | class Manager : public ISingleton 33 | { 34 | 35 | public: 36 | /// 37 | /// Does a forms lookup similar to what Filters do. 38 | /// 39 | /// As a result this method configures Manager with discovered valid exclusive groups. 40 | /// 41 | /// A raw exclusive group entries that should be processed. 42 | void LookupExclusiveGroups(INI::ExclusiveGroupsVec& rawExclusiveGroups = INI::exclusiveGroups); 43 | 44 | void LogExclusiveGroupsLookup(); 45 | 46 | /// 47 | /// Gets a set of all keywords that are in the same exclusive group as the given keyword. 48 | /// Note that a keyword can appear in multiple exclusive groups, all of those groups are returned. 49 | /// 50 | /// A keyword for which mutually exclusive keywords will be returned. 51 | /// A union of all groups that contain a given keyword. 52 | Set MutuallyExclusiveKeywordsForKeyword(RE::BGSKeyword*) const; 53 | 54 | /// 55 | /// Retrieves all exclusive groups. 56 | /// 57 | /// A reference to discovered exclusive groups 58 | const GroupKeywordsMap& GetGroups() const; 59 | 60 | private: 61 | /// 62 | /// A map of exclusive group names related to each keyword in the exclusive groups. 63 | /// Provides a quick and easy way to get names of all groups that need to be checked. 64 | /// 65 | KeywordGroupMap linkedGroups{}; 66 | 67 | /// 68 | /// A map of exclusive groups names and the keywords that are part of each exclusive group. 69 | /// 70 | GroupKeywordsMap groups{}; 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /include/Hooks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Hooks 4 | { 5 | void Install(); 6 | } 7 | -------------------------------------------------------------------------------- /include/KeywordData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "LookupConfigs.h" 4 | #include "LookupFilters.h" 5 | 6 | namespace Keyword 7 | { 8 | namespace detail 9 | { 10 | inline RE::BGSKeyword* find_existing_keyword(const RE::BSTArray& a_keywordArray, const std::string& a_edid, bool a_skipEDID = false) 11 | { 12 | if (!a_skipEDID) { 13 | if (const auto keyword = RE::TESForm::LookupByEditorID(a_edid)) { 14 | return keyword; 15 | } 16 | } 17 | 18 | // iterate from last added (most likely to be runtime) 19 | const auto rBegin = std::reverse_iterator(a_keywordArray.end()); 20 | const auto rEnd = std::reverse_iterator(a_keywordArray.begin()); 21 | 22 | const auto result = std::find_if(rBegin, rEnd, [&](const auto& keywordInArray) { 23 | return keywordInArray->formEditorID == a_edid.c_str(); 24 | }); 25 | 26 | return result != rEnd ? *result : nullptr; 27 | } 28 | 29 | inline void get_merged_IDs(std::optional& a_formID, std::optional& a_modName) 30 | { 31 | const auto [mergedModName, mergedFormID] = g_mergeMapperInterface->GetNewFormID(a_modName.value_or("").c_str(), a_formID.value_or(0)); 32 | std::string conversion_log{}; 33 | if (a_formID.value_or(0) && mergedFormID && a_formID.value_or(0) != mergedFormID) { 34 | conversion_log = std::format("0x{:X}->0x{:X}", a_formID.value_or(0), mergedFormID); 35 | a_formID.emplace(mergedFormID); 36 | } 37 | const std::string mergedModString{ mergedModName }; 38 | if (!a_modName.value_or("").empty() && !mergedModString.empty() && a_modName.value_or("") != mergedModString) { 39 | if (conversion_log.empty()) { 40 | conversion_log = std::format("{}->{}", a_modName.value_or(""), mergedModString); 41 | } else { 42 | conversion_log = std::format("{}~{}->{}", conversion_log, a_modName.value_or(""), mergedModString); 43 | } 44 | a_modName.emplace(mergedModName); 45 | } 46 | if (!conversion_log.empty()) { 47 | buffered_logger::info("\t\tFound merged: {}", conversion_log); 48 | } 49 | } 50 | 51 | inline bool formID_to_form(RawVec& a_rawFormVec, ProcessedVec& a_formVec, bool a_all = false) 52 | { 53 | if (a_rawFormVec.empty()) { 54 | return true; 55 | } 56 | 57 | const auto dataHandler = RE::TESDataHandler::GetSingleton(); 58 | const auto& keywordArray = dataHandler->GetFormArray(); 59 | 60 | for (auto& formOrEditorID : a_rawFormVec) { 61 | std::visit(overload{ 62 | [&](FormModPair& formMod) { 63 | auto& [formID, modName] = formMod; 64 | if (g_mergeMapperInterface) { 65 | get_merged_IDs(formID, modName); 66 | } 67 | if (modName && !formID) { 68 | if (const RE::TESFile* filterMod = dataHandler->LookupModByName(*modName)) { 69 | a_formVec.push_back(filterMod); 70 | } else { 71 | buffered_logger::error("\t\tFilter ({}) SKIP - mod doesn't exist", *modName); 72 | } 73 | } else if (formID) { 74 | if (auto filterForm = modName ? 75 | dataHandler->LookupForm(*formID, *modName) : 76 | RE::TESForm::LookupByID(*formID)) { 77 | const auto formType = filterForm->GetFormType(); 78 | if (Cache::FormType::IsFilter(formType)) { 79 | a_formVec.push_back(filterForm); 80 | } else { 81 | buffered_logger::error("\t\tFilter [0x{:X}] ({}) SKIP - invalid formtype ({})", *formID, modName.value_or(""), formType); 82 | } 83 | } else { 84 | buffered_logger::error("\t\tFilter [0x{:X}] ({}) SKIP - form doesn't exist", *formID, modName.value_or("")); 85 | } 86 | } 87 | }, 88 | [&](std::string& editorID) { 89 | if (auto filterForm = RE::TESForm::LookupByEditorID(editorID)) { 90 | const auto formType = filterForm->GetFormType(); 91 | if (Cache::FormType::IsFilter(formType)) { 92 | a_formVec.push_back(filterForm); 93 | } else { 94 | buffered_logger::error("\t\tFilter ({}) SKIP - invalid formtype ({})", editorID, formType); 95 | } 96 | } else { 97 | if (auto keyword = find_existing_keyword(keywordArray, editorID, true)) { 98 | a_formVec.push_back(keyword); 99 | } else { 100 | if (string::icontains(editorID, ".nif")) { 101 | Filter::SanitizePath(editorID); 102 | } 103 | a_formVec.push_back(editorID); 104 | } 105 | } 106 | } }, 107 | formOrEditorID); 108 | } 109 | 110 | return !a_all && !a_formVec.empty() || a_formVec.size() == a_rawFormVec.size(); 111 | } 112 | } 113 | 114 | struct Data 115 | { 116 | bool operator==(const Data& a_rhs) const; 117 | 118 | // members 119 | std::uint32_t count{ 0 }; 120 | RE::BGSKeyword* keyword{ nullptr }; 121 | FilterData filters{}; 122 | }; 123 | 124 | using DataVec = std::vector; 125 | using CountMap = std::map; 126 | 127 | template 128 | class Distributable 129 | { 130 | public: 131 | Distributable(ITEM::TYPE a_type); 132 | 133 | explicit operator bool() const; 134 | bool empty() const; 135 | [[nodiscard]] std::size_t size() const; 136 | void clear(); 137 | 138 | [[nodiscard]] ITEM::TYPE GetType() const; 139 | [[nodiscard]] std::string_view GetTypeString() const; 140 | [[nodiscard]] const DataVec& GetKeywords() const; 141 | [[nodiscard]] DataVec& GetKeywords(); 142 | void LookupForms(); 143 | 144 | private: 145 | ITEM::TYPE type; 146 | DataVec keywords{}; 147 | }; 148 | 149 | inline Distributable armors{ ITEM::kArmor }; 150 | inline Distributable weapons{ ITEM::kWeapon }; 151 | inline Distributable ammo{ ITEM::kAmmo }; 152 | inline Distributable magicEffects{ ITEM::kMagicEffect }; 153 | inline Distributable potions{ ITEM::kPotion }; 154 | inline Distributable scrolls{ ITEM::kScroll }; 155 | inline Distributable locations{ ITEM::kLocation }; 156 | inline Distributable ingredients{ ITEM::kIngredient }; 157 | inline Distributable books{ ITEM::kBook }; 158 | inline Distributable miscItems{ ITEM::kMiscItem }; 159 | inline Distributable keys{ ITEM::kKey }; 160 | inline Distributable soulGems{ ITEM::kSoulGem }; 161 | inline Distributable spells{ ITEM::kSpell }; 162 | inline Distributable activators{ ITEM::kActivator }; 163 | inline Distributable flora{ ITEM::kFlora }; 164 | inline Distributable furniture{ ITEM::kFurniture }; 165 | inline Distributable races{ ITEM::kRace }; 166 | inline Distributable talkingActivators{ ITEM::kTalkingActivator }; 167 | inline Distributable enchantments{ ITEM::kEnchantmentItem }; 168 | 169 | template 170 | void ForEachDistributable(Func&& a_func, Args&&... args) 171 | { 172 | const auto process = [&](auto&& container) { 173 | a_func(container, std::forward(args)...); 174 | }; 175 | 176 | process(magicEffects); 177 | 178 | process(armors); 179 | process(weapons); 180 | process(ammo); 181 | process(potions); 182 | process(scrolls); 183 | process(locations); 184 | process(ingredients); 185 | process(books); 186 | process(miscItems); 187 | process(keys); 188 | process(soulGems); 189 | process(spells); 190 | process(activators); 191 | process(flora); 192 | process(furniture); 193 | process(races); 194 | process(talkingActivators); 195 | process(enchantments); 196 | } 197 | 198 | template 199 | void ForEachDistributable_MT(Func&& a_func, Args&&... args) 200 | { 201 | std::vector threads; 202 | 203 | const auto process = [&](auto&& container) { 204 | a_func(container, std::forward(args)...); 205 | }; 206 | 207 | process(magicEffects); 208 | 209 | threads.emplace_back([&] { process(armors); }); 210 | threads.emplace_back([&] { process(weapons); }); 211 | threads.emplace_back([&] { process(ammo); }); 212 | threads.emplace_back([&] { process(potions); }); 213 | threads.emplace_back([&] { process(scrolls); }); 214 | threads.emplace_back([&] { process(locations); }); 215 | threads.emplace_back([&] { process(ingredients); }); 216 | threads.emplace_back([&] { process(books); }); 217 | threads.emplace_back([&] { process(miscItems); }); 218 | threads.emplace_back([&] { process(keys); }); 219 | threads.emplace_back([&] { process(soulGems); }); 220 | threads.emplace_back([&] { process(spells); }); 221 | threads.emplace_back([&] { process(activators); }); 222 | threads.emplace_back([&] { process(flora); }); 223 | threads.emplace_back([&] { process(furniture); }); 224 | threads.emplace_back([&] { process(races); }); 225 | threads.emplace_back([&] { process(talkingActivators); }); 226 | threads.emplace_back([&] { process(enchantments); }); 227 | 228 | for (auto& t : threads) { 229 | t.join(); 230 | } 231 | } 232 | } 233 | using KeywordData = Keyword::Data; 234 | using KeywordDataVec = Keyword::DataVec; 235 | 236 | template 237 | Keyword::Distributable::Distributable(ITEM::TYPE a_type) : 238 | type(a_type) 239 | {} 240 | 241 | template 242 | Keyword::Distributable::operator bool() const 243 | { 244 | return !empty(); 245 | } 246 | 247 | template 248 | bool Keyword::Distributable::empty() const 249 | { 250 | return keywords.empty(); 251 | } 252 | 253 | template 254 | std::size_t Keyword::Distributable::size() const 255 | { 256 | return keywords.size(); 257 | } 258 | 259 | template 260 | void Keyword::Distributable::clear() 261 | { 262 | keywords.clear(); 263 | } 264 | 265 | template 266 | ITEM::TYPE Keyword::Distributable::GetType() const 267 | { 268 | return type; 269 | } 270 | 271 | template 272 | std::string_view Keyword::Distributable::GetTypeString() const 273 | { 274 | return ITEM::GetType(type); 275 | } 276 | 277 | template 278 | const KeywordDataVec& Keyword::Distributable::GetKeywords() const 279 | { 280 | return keywords; 281 | } 282 | 283 | template 284 | KeywordDataVec& Keyword::Distributable::GetKeywords() 285 | { 286 | return keywords; 287 | } 288 | 289 | template 290 | void Keyword::Distributable::LookupForms() 291 | { 292 | auto& INIDataVec = INI::INIs[type]; 293 | if (INIDataVec.empty()) { 294 | return; 295 | } 296 | 297 | logger::info("{}", GetTypeString()); 298 | 299 | keywords.reserve(INIDataVec.size()); 300 | 301 | const auto dataHandler = RE::TESDataHandler::GetSingleton(); 302 | auto& keywordArray = dataHandler->GetFormArray(); 303 | 304 | // INIDataVec index, keyword 305 | std::map processedKeywords{}; 306 | 307 | // Process keywords to be distributed first. 308 | std::uint32_t index = 0; 309 | 310 | for (auto& [rawForm, rawFilters, traits, chance, path] : INIDataVec) { 311 | RE::BGSKeyword* keyword = nullptr; 312 | 313 | std::visit(overload{ 314 | [&](FormModPair& a_formMod) { 315 | auto& [formID, modName] = a_formMod; 316 | if (g_mergeMapperInterface) { 317 | detail::get_merged_IDs(formID, modName); 318 | } 319 | keyword = modName ? 320 | dataHandler->LookupForm(*formID, *modName) : 321 | RE::TESForm::LookupByID(*formID); 322 | if (!keyword) { 323 | buffered_logger::error("\t[{}] [0x{:X}]({}) FAIL - keyword doesn't exist", path, *formID, modName.value_or("")); 324 | } else if (keyword->formEditorID.empty()) { 325 | keyword = nullptr; 326 | buffered_logger::error("\t[{}] [0x{:X}]({}) FAIL - keyword editorID is empty!", path, *formID, modName.value_or("")); 327 | } 328 | }, 329 | [&](const std::string& a_edid) { 330 | keyword = detail::find_existing_keyword(keywordArray, a_edid); 331 | if (!keyword) { 332 | const auto factory = RE::IFormFactory::GetConcreteFormFactoryByType(); 333 | if (keyword = factory ? factory->Create() : nullptr; keyword) { 334 | keyword->formEditorID = a_edid; 335 | keywordArray.push_back(keyword); 336 | } else { 337 | buffered_logger::critical("\t[{}] {} FAIL - couldn't create keyword", path, a_edid); 338 | } 339 | } 340 | }, 341 | }, 342 | rawForm); 343 | 344 | if (keyword) { 345 | processedKeywords.emplace(index, keyword); 346 | } 347 | index++; 348 | } 349 | 350 | // Get Filters 351 | for (auto& [vecIdx, keyword] : processedKeywords) { 352 | auto& [rawForm, rawFilters, traits, chance, path] = INIDataVec[vecIdx]; 353 | 354 | buffered_logger::info("\t[{}] {}", path, keyword->GetFormEditorID()); 355 | 356 | ProcessedFilters processedFilters{}; 357 | 358 | bool validEntry = detail::formID_to_form(rawFilters.ALL, processedFilters.ALL, true); 359 | if (validEntry) { 360 | validEntry = detail::formID_to_form(rawFilters.NOT, processedFilters.NOT); 361 | } 362 | if (validEntry) { 363 | validEntry = detail::formID_to_form(rawFilters.MATCH, processedFilters.MATCH); 364 | } 365 | if (validEntry) { 366 | std::ranges::for_each(rawFilters.ANY, [](std::string& str) { 367 | if (string::icontains(str, ".nif")) { 368 | Filter::SanitizePath(str); 369 | } 370 | }); 371 | processedFilters.ANY = rawFilters.ANY; 372 | } 373 | 374 | if (!validEntry) { 375 | logger::error("\t\tInvalid/missing filters, skipping distribution"); 376 | continue; 377 | } 378 | 379 | keywords.emplace_back(Data{ 0, keyword, { processedFilters, traits, chance } }); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /include/KeywordDependencies.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "DependencyResolver.h" 4 | #include "KeywordData.h" 5 | 6 | namespace Keyword 7 | { 8 | inline std::once_flag init; 9 | 10 | inline StringMap allKeywords{}; 11 | 12 | namespace Dependencies 13 | { 14 | using KYWD = RE::BGSKeyword*; 15 | 16 | /// Comparator that preserves relative order at which Keywords appeared in config files. 17 | /// If that order is undefined it falls back to alphabetical order of EditorIDs. 18 | struct keyword_less 19 | { 20 | using RelativeOrderMap = Map; 21 | 22 | const RelativeOrderMap relativeOrder; 23 | 24 | bool operator()(const KYWD& a, const KYWD& b) const 25 | { 26 | const auto aIdx = getIndex(a); 27 | const auto bIdx = getIndex(b); 28 | if (aIdx >= 0 && bIdx >= 0) { 29 | if (aIdx < bIdx) { 30 | return true; 31 | } 32 | if (aIdx > bIdx) { 33 | return false; 34 | } 35 | } 36 | return a->GetFormEditorID() < b->GetFormEditorID(); 37 | } 38 | 39 | [[nodiscard]] int getIndex(const KYWD& kwd) const 40 | { 41 | if (relativeOrder.contains(kwd)) { 42 | return relativeOrder.at(kwd); 43 | } 44 | return -1; 45 | } 46 | }; 47 | 48 | using Resolver = DependencyResolver; 49 | 50 | /// Reads Forms::keywords and sorts them based on their relationship or alphabetical order. 51 | /// This must be called after initial Lookup was performed. 52 | 53 | template 54 | void ResolveKeywords(Distributable& keywords) 55 | { 56 | if (!keywords) { 57 | return; 58 | } 59 | 60 | auto& keywordForms = keywords.GetKeywords(); 61 | 62 | std::call_once(init, []() { 63 | const auto dataHandler = RE::TESDataHandler::GetSingleton(); 64 | for (const auto& kwd : dataHandler->GetFormArray()) { 65 | if (kwd) { 66 | if (const auto edid = kwd->GetFormEditorID(); !string::is_empty(edid)) { 67 | allKeywords[edid] = kwd; 68 | } else { 69 | if (const auto file = kwd->GetFile(0)) { 70 | const auto modname = file->GetFilename(); 71 | const auto formID = kwd->GetLocalFormID(); 72 | std::string mergeDetails; 73 | if (g_mergeMapperInterface && g_mergeMapperInterface->isMerge(modname.data())) { 74 | const auto [mergedModName, mergedFormID] = g_mergeMapperInterface->GetOriginalFormID( 75 | modname.data(), 76 | formID); 77 | mergeDetails = std::format("->0x{:X}~{}", mergedFormID, mergedModName); 78 | } 79 | logger::error("\tWARN : [0x{:X}~{}{}] keyword has an empty editorID!", formID, modname, mergeDetails); 80 | } 81 | } 82 | } 83 | } 84 | }); 85 | 86 | keyword_less::RelativeOrderMap orderMap; 87 | 88 | for (std::int32_t index = 0; index < keywordForms.size(); ++index) { 89 | orderMap.emplace(keywordForms[index].keyword, index); 90 | } 91 | 92 | Resolver resolver{ keyword_less(orderMap) }; 93 | 94 | /// A map that will be used to map back keywords to their data wrappers. 95 | std::unordered_multimap dataKeywords; 96 | 97 | logger::info("\tSorting keywords..."); 98 | for (const auto& keywordData : keywordForms) { 99 | dataKeywords.emplace(keywordData.keyword, keywordData); 100 | resolver.AddIsolated(keywordData.keyword); 101 | 102 | const auto addDependencies = [&](const ProcessedVec& a_processed) { 103 | for (const auto& filter : a_processed) { 104 | if (const auto formPtr = std::get_if(&filter)) { 105 | if (const auto& kwd = (*formPtr)->As()) { 106 | resolver.AddDependency(keywordData.keyword, kwd); 107 | } 108 | } 109 | } 110 | }; 111 | 112 | const auto containsKeyword = [&](const std::string& name) -> RE::BGSKeyword* { 113 | for (const auto& [keywordName, keyword] : allKeywords) { 114 | if (keywordName.contains(name)) { 115 | return keyword; 116 | } 117 | } 118 | return nullptr; 119 | }; 120 | 121 | const auto addANYDependencies = [&](const std::vector& a_strings) { 122 | for (const auto& filter : a_strings) { 123 | if (const auto& kwd = containsKeyword(filter); kwd) { 124 | resolver.AddDependency(keywordData.keyword, kwd); 125 | } 126 | } 127 | }; 128 | 129 | const auto& filters = keywordData.filters.processedFilters; 130 | 131 | addDependencies(filters.ALL); 132 | addDependencies(filters.NOT); 133 | addDependencies(filters.MATCH); 134 | 135 | addANYDependencies(filters.ANY); 136 | } 137 | 138 | const auto result = resolver.Resolve(); 139 | 140 | keywordForms.clear(); 141 | logger::info("\tSorted keywords: "); 142 | for (const auto& keyword : result) { 143 | const auto& [begin, end] = dataKeywords.equal_range(keyword); 144 | if (begin != end) { 145 | logger::info("\t\t{}", describe(begin->second.keyword)); 146 | } 147 | for (auto it = begin; it != end; ++it) { 148 | keywordForms.push_back(it->second); 149 | } 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /include/LogBuffer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define MAKE_BUFFERED_LOG(a_func, a_type) \ 4 | \ 5 | template \ 6 | struct [[maybe_unused]] a_func \ 7 | { \ 8 | a_func() = delete; \ 9 | \ 10 | explicit a_func( \ 11 | fmt::format_string a_fmt, \ 12 | Args&&... a_args, \ 13 | std::source_location a_loc = std::source_location::current()) \ 14 | { \ 15 | if (buffer.insert({ a_loc, fmt::format(a_fmt, std::forward(a_args)...) }).second) { \ 16 | spdlog::log( \ 17 | spdlog::source_loc{ \ 18 | a_loc.file_name(), \ 19 | static_cast(a_loc.line()), \ 20 | a_loc.function_name() }, \ 21 | spdlog::level::a_type, \ 22 | a_fmt, \ 23 | std::forward(a_args)...); \ 24 | } \ 25 | } \ 26 | }; \ 27 | template \ 28 | a_func(fmt::format_string, Args&&...) -> a_func; 29 | 30 | namespace LogBuffer 31 | { 32 | struct Entry 33 | { 34 | std::source_location loc; 35 | std::string message; 36 | 37 | bool operator==(const Entry& other) const 38 | { 39 | return strcmp(loc.file_name(), other.loc.file_name()) == 0 && loc.line() == other.loc.line() && message == other.message; 40 | } 41 | }; 42 | } 43 | 44 | /// Add hashing for custom log entries. 45 | template <> 46 | struct ankerl::unordered_dense::hash 47 | { 48 | using is_avalanching = void; 49 | 50 | [[nodiscard]] std::uint64_t operator()(const LogBuffer::Entry& entry) const noexcept 51 | { 52 | return detail::wyhash::hash(entry.message.c_str(), entry.message.size()); 53 | } 54 | }; 55 | 56 | /// LogBuffer proxies typical logging calls and buffers received entries to avoid duplication. 57 | /// 58 | /// Main log proxy functions. 59 | /// Each log function checks whether given message was already logged and skips the log. 60 | namespace LogBuffer 61 | { 62 | inline ankerl::unordered_dense::set buffer{}; 63 | 64 | /// Clears already buffered messages to allow them to be logged once again. 65 | inline void clear() 66 | { 67 | buffer.clear(); 68 | } 69 | 70 | MAKE_BUFFERED_LOG(trace, trace); 71 | MAKE_BUFFERED_LOG(debug, debug); 72 | MAKE_BUFFERED_LOG(info, info); 73 | MAKE_BUFFERED_LOG(warn, warn); 74 | MAKE_BUFFERED_LOG(error, err); 75 | MAKE_BUFFERED_LOG(critical, critical); 76 | } 77 | 78 | #undef MAKE_BUFFERED_LOG 79 | -------------------------------------------------------------------------------- /include/LookupConfigs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "LookupFilters.h" 4 | 5 | namespace INI 6 | { 7 | enum TYPE : std::uint32_t 8 | { 9 | kFormIDPair = 0, 10 | kFormID = kFormIDPair, 11 | kType, 12 | kESP = kType, 13 | kFilters, 14 | kTraits, 15 | kChance 16 | }; 17 | 18 | struct Data 19 | { 20 | FormIDOrString rawForm{}; 21 | RawFilters rawFilters{}; 22 | TraitsPtr traits{}; 23 | Chance chance{ 100 }; 24 | std::string path{}; 25 | }; 26 | using DataVec = std::vector; 27 | 28 | inline std::map INIs{}; 29 | 30 | std::pair GetConfigs(); 31 | } 32 | using INIDataVec = INI::DataVec; 33 | -------------------------------------------------------------------------------- /include/LookupFilters.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Traits.h" 4 | 5 | namespace Filter 6 | { 7 | void SanitizeString(std::string& a_string); 8 | void SanitizePath(std::string& a_string); 9 | 10 | struct Data 11 | { 12 | Data() = default; 13 | Data(ProcessedFilters a_processedFilters, TraitsPtr a_traits, Chance a_chance); 14 | 15 | // members 16 | ProcessedFilters processedFilters{}; 17 | TraitsPtr traits{}; 18 | Chance chance{ 100 }; 19 | }; 20 | } 21 | using FilterData = Filter::Data; 22 | 23 | namespace Item 24 | { 25 | struct Data 26 | { 27 | Data(RE::TESForm* a_item); 28 | 29 | [[nodiscard]] bool PassedFilters(RE::BGSKeyword* a_keyword, const FilterData& a_filters); 30 | 31 | private: 32 | RE::TESForm* item{ nullptr }; 33 | std::string edid{}; 34 | std::string name{}; 35 | std::string model{}; 36 | 37 | Set keywords{}; 38 | 39 | [[nodiscard]] bool HasFormOrStringFilter(const ProcessedVec& a_processed, bool a_all = false) const; 40 | [[nodiscard]] bool HasFormFilter(RE::TESForm* a_formFilter) const; 41 | [[nodiscard]] bool HasStringFilter(const std::string& a_str) const; 42 | [[nodiscard]] bool ContainsStringFilter(const std::vector& a_strings) const; 43 | 44 | /// 45 | /// Checks whether given Item already has another Keyword that is mutually exclusive with the given keyword, 46 | /// according to the exclusive groups configuration. 47 | /// 48 | /// A Keyword that needs to be checked. 49 | [[nodiscard]] bool HasMutuallyExclusiveKeyword(RE::BGSKeyword* otherKeyword) const; 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /include/LookupForms.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Forms 4 | { 5 | bool LookupForms(); 6 | void LogFormLookup(); 7 | } 8 | -------------------------------------------------------------------------------- /include/PCH.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | #define NOMINMAX 5 | 6 | #include 7 | #include 8 | 9 | #include "RE/Skyrim.h" 10 | #include "SKSE/SKSE.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include 27 | 28 | #include "LogBuffer.h" 29 | 30 | #define DLLEXPORT __declspec(dllexport) 31 | 32 | namespace logger = SKSE::log; 33 | namespace buffered_logger = LogBuffer; 34 | 35 | using namespace clib_util; 36 | using namespace clib_util::singleton; 37 | using namespace std::literals; 38 | using namespace string::literals; 39 | 40 | template 41 | using nullable = std::optional; 42 | 43 | template 44 | using Map = ankerl::unordered_dense::map; 45 | template 46 | using Set = ankerl::unordered_dense::set; 47 | 48 | struct string_hash 49 | { 50 | using is_transparent = void; // enable heterogeneous overloads 51 | using is_avalanching = void; // mark class as high quality avalanching hash 52 | 53 | [[nodiscard]] std::uint64_t operator()(std::string_view str) const noexcept 54 | { 55 | return ankerl::unordered_dense::hash{}(str); 56 | } 57 | }; 58 | 59 | template 60 | using StringMap = ankerl::unordered_dense::map>; 61 | using StringSet = ankerl::unordered_dense::set>; 62 | 63 | namespace stl 64 | { 65 | using namespace SKSE::stl; 66 | 67 | template 68 | void write_vfunc() 69 | { 70 | REL::Relocation vtbl{ F::VTABLE[0] }; 71 | T::func = vtbl.write_vfunc(T::idx, T::thunk); 72 | } 73 | } 74 | 75 | #include "Cache.h" 76 | #include "Defs.h" 77 | #include "Version.h" 78 | -------------------------------------------------------------------------------- /include/Traits.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace TRAITS 4 | { 5 | namespace detail 6 | { 7 | template 8 | nullable get_single_value(std::string& a_str) 9 | { 10 | if (const auto values = string::split(string::remove_non_numeric(a_str), " "); !values.empty()) { 11 | return string::to_num(values[0]); 12 | } 13 | return std::nullopt; 14 | } 15 | } 16 | 17 | template 18 | struct Range 19 | { 20 | Range(T a_min) : 21 | min(a_min) 22 | {} 23 | Range(T a_min, T a_max) : 24 | min(a_min), 25 | max(a_max) 26 | {} 27 | Range(std::string& a_str) 28 | { 29 | if (const auto values = string::split(string::remove_non_numeric(a_str), " "); !values.empty()) { 30 | if (values.size() > 1) { 31 | min = string::to_num(values[0]); 32 | max = string::to_num(values[1]); 33 | } else { 34 | min = string::to_num(values[0]); 35 | } 36 | } 37 | } 38 | 39 | [[nodiscard]] bool IsInRange(T value) const 40 | { 41 | return value >= min && value <= max; 42 | } 43 | 44 | // members 45 | T min{ std::numeric_limits::min() }; 46 | T max{ std::numeric_limits::max() }; 47 | }; 48 | 49 | class Traits 50 | { 51 | public: 52 | virtual ~Traits() = default; 53 | 54 | [[nodiscard]] virtual bool PassFilter([[maybe_unused]] RE::TESForm* a_item) const 55 | { 56 | return false; 57 | } 58 | }; 59 | 60 | class ArmorTraits : public Traits 61 | { 62 | public: 63 | ArmorTraits(const std::string& a_traits) 64 | { 65 | auto traits = distribution::split_entry(a_traits); 66 | for (auto& trait : traits) { 67 | if (trait.contains("AR(")) { 68 | armorRating = Range(trait); 69 | } else if (trait.contains("W(")) { 70 | weight = Range(trait); 71 | } else if (string::is_only_digit(trait)) { 72 | slot = static_cast(1 << (string::to_num(trait) - 30)); 73 | } else { 74 | switch (string::const_hash(trait)) { 75 | case "HEAVY"_h: 76 | armorType = RE::BIPED_MODEL::ArmorType::kHeavyArmor; 77 | break; 78 | case "LIGHT"_h: 79 | armorType = RE::BIPED_MODEL::ArmorType::kLightArmor; 80 | break; 81 | case "CLOTHING"_h: 82 | armorType = RE::BIPED_MODEL::ArmorType::kClothing; 83 | break; 84 | case "E"_h: 85 | enchanted = true; 86 | break; 87 | case "-E"_h: 88 | enchanted = false; 89 | break; 90 | case "T"_h: 91 | templated = true; 92 | break; 93 | case "-T"_h: 94 | templated = false; 95 | break; 96 | default: 97 | break; 98 | } 99 | } 100 | } 101 | } 102 | ~ArmorTraits() override = default; 103 | 104 | [[nodiscard]] bool PassFilter(RE::TESForm* a_item) const override 105 | { 106 | const auto armor = a_item->As(); 107 | 108 | if (enchanted && (armor->formEnchanting != nullptr) != *enchanted) { 109 | return false; 110 | } 111 | if (templated && (armor->templateArmor != nullptr) != *templated) { 112 | return false; 113 | } 114 | if (armorRating && !armorRating->IsInRange(armor->GetArmorRating())) { 115 | return false; 116 | } 117 | if (armorType && armor->GetArmorType() != *armorType) { 118 | return false; 119 | } 120 | if (weight && !weight->IsInRange(armor->weight)) { 121 | return false; 122 | } 123 | if (slot && !armor->HasPartOf(*slot)) { 124 | return false; 125 | } 126 | return true; 127 | } 128 | 129 | private: 130 | // members 131 | nullable enchanted{}; 132 | nullable templated{}; 133 | nullable> armorRating{}; 134 | nullable armorType{}; 135 | nullable> weight; 136 | nullable slot{}; 137 | }; 138 | 139 | class WeaponTraits : public Traits 140 | { 141 | public: 142 | WeaponTraits(const std::string& a_traits) 143 | { 144 | auto traits = distribution::split_entry(a_traits); 145 | for (auto& trait : traits) { 146 | if (trait.contains("W(")) { 147 | weight = Range(trait); 148 | } else if (trait.contains("D(")) { 149 | damage = Range(trait); 150 | } else { 151 | switch (string::const_hash(trait)) { 152 | case "HandToHandMelee"_h: 153 | animationType = RE::WEAPON_TYPE::kHandToHandMelee; 154 | break; 155 | case "OneHandSword"_h: 156 | animationType = RE::WEAPON_TYPE::kOneHandSword; 157 | break; 158 | case "OneHandDagger"_h: 159 | animationType = RE::WEAPON_TYPE::kOneHandDagger; 160 | break; 161 | case "OneHandAxe"_h: 162 | animationType = RE::WEAPON_TYPE::kOneHandAxe; 163 | break; 164 | case "OneHandMace"_h: 165 | animationType = RE::WEAPON_TYPE::kOneHandMace; 166 | break; 167 | case "TwoHandSword"_h: 168 | animationType = RE::WEAPON_TYPE::kTwoHandSword; 169 | break; 170 | case "TwoHandAxe"_h: 171 | animationType = RE::WEAPON_TYPE::kTwoHandAxe; 172 | break; 173 | case "Bow"_h: 174 | animationType = RE::WEAPON_TYPE::kBow; 175 | break; 176 | case "Staff"_h: 177 | animationType = RE::WEAPON_TYPE::kStaff; 178 | break; 179 | case "Crossbow"_h: 180 | animationType = RE::WEAPON_TYPE::kCrossbow; 181 | break; 182 | case "E"_h: 183 | enchanted = true; 184 | break; 185 | case "-E"_h: 186 | enchanted = false; 187 | break; 188 | case "T"_h: 189 | templated = true; 190 | break; 191 | case "-T"_h: 192 | templated = false; 193 | break; 194 | default: 195 | break; 196 | } 197 | } 198 | } 199 | } 200 | ~WeaponTraits() override = default; 201 | 202 | bool PassFilter(RE::TESForm* a_item) const override 203 | { 204 | const auto weapon = a_item->As(); 205 | 206 | if (enchanted && (weapon->formEnchanting != nullptr) != *enchanted) { 207 | return false; 208 | } 209 | if (templated && (weapon->templateWeapon != nullptr) != *templated) { 210 | return false; 211 | } 212 | if (weight && !weight->IsInRange(weapon->weight)) { 213 | return false; 214 | } 215 | if (animationType && weapon->GetWeaponType() != *animationType) { 216 | return false; 217 | } 218 | if (damage && !damage->IsInRange(weapon->GetAttackDamage())) { 219 | return false; 220 | } 221 | return true; 222 | } 223 | 224 | private: 225 | // members 226 | nullable enchanted{}; 227 | nullable templated{}; 228 | nullable animationType{}; 229 | nullable> damage{}; 230 | nullable> weight{}; 231 | }; 232 | 233 | class AmmoTraits : public Traits 234 | { 235 | public: 236 | AmmoTraits(const std::string& a_traits) 237 | { 238 | auto traits = distribution::split_entry(a_traits); 239 | for (auto& trait : traits) { 240 | if (trait.contains("D(")) { 241 | damage = Range(trait); 242 | } else { 243 | switch (string::const_hash(a_traits)) { 244 | case "B"_h: 245 | isBolt = true; 246 | break; 247 | case "-B"_h: 248 | isBolt = false; 249 | break; 250 | default: 251 | break; 252 | } 253 | } 254 | } 255 | } 256 | 257 | ~AmmoTraits() override = default; 258 | 259 | [[nodiscard]] bool PassFilter(RE::TESForm* a_item) const override 260 | { 261 | const auto ammo = a_item->As(); 262 | if (isBolt && ammo->IsBolt() != *isBolt) { 263 | return false; 264 | } 265 | if (damage && !damage->IsInRange(ammo->data.damage)) { 266 | return false; 267 | } 268 | return true; 269 | } 270 | 271 | private: 272 | // members 273 | nullable isBolt; 274 | nullable> damage; 275 | }; 276 | 277 | class MagicEffectTraits : public Traits 278 | { 279 | public: 280 | MagicEffectTraits(const std::string& a_traits) 281 | { 282 | auto traits = distribution::split_entry(a_traits); 283 | for (auto& trait : traits) { 284 | if (trait.contains("D(")) { 285 | deliveryType = detail::get_single_value(trait); 286 | } else if (trait.contains("CT(")) { 287 | castingType = detail::get_single_value(trait); 288 | } else if (trait.contains("R(")) { 289 | resistance = detail::get_single_value(trait); 290 | } else if (trait.contains('(')) { 291 | if (auto value = string::split(string::remove_non_numeric(trait), " "); !value.empty()) { 292 | auto skillType = string::to_num(value[0]); 293 | auto min = string::to_num(value[1]); 294 | if (value.size() > 2) { 295 | auto max = string::to_num(value[2]); 296 | skill = { skillType, { min, max } }; 297 | } else { 298 | skill = { skillType, { min } }; 299 | } 300 | } 301 | } else if (trait == "H") { 302 | isHostile = true; 303 | } else if (trait == "-H") { 304 | isHostile = false; 305 | } else if (trait == "DISPEL") { 306 | dispelWithKeywords = true; 307 | } else if (trait == "-DISPEL") { 308 | dispelWithKeywords = false; 309 | } 310 | } 311 | } 312 | ~MagicEffectTraits() override = default; 313 | 314 | bool PassFilter(RE::TESForm* a_item) const override 315 | { 316 | const auto mgef = a_item->As(); 317 | 318 | if (isHostile && mgef->IsHostile() != *isHostile) { 319 | return false; 320 | } 321 | if (castingType && mgef->data.castingType != *castingType) { 322 | return false; 323 | } 324 | if (deliveryType && mgef->data.delivery != *deliveryType) { 325 | return false; 326 | } 327 | if (skill) { 328 | auto& [skillType, minMax] = *skill; 329 | if (skillType != mgef->GetMagickSkill()) { 330 | return false; 331 | } 332 | if (!minMax.IsInRange(mgef->GetMinimumSkillLevel())) { 333 | return false; 334 | } 335 | } 336 | if (resistance && mgef->data.resistVariable != *resistance) { 337 | return false; 338 | } 339 | if (dispelWithKeywords && mgef->data.flags.all(RE::EffectSetting::EffectSettingData::Flag::kDispelWithKeywords) != *dispelWithKeywords) { 340 | return false; 341 | } 342 | return true; 343 | } 344 | 345 | private: 346 | // members 347 | nullable isHostile; 348 | nullable castingType; 349 | nullable deliveryType; 350 | nullable>> skill; 351 | nullable resistance; 352 | nullable dispelWithKeywords; 353 | }; 354 | 355 | class PotionTraits : public Traits 356 | { 357 | public: 358 | PotionTraits(const std::string& a_traits) 359 | { 360 | const auto traits = distribution::split_entry(a_traits); 361 | for (auto& trait : traits) { 362 | switch (string::const_hash(trait)) { 363 | case "P"_h: 364 | isPoison = true; 365 | break; 366 | case "-P"_h: 367 | isPoison = false; 368 | break; 369 | case "F"_h: 370 | isFood = true; 371 | break; 372 | case "-F"_h: 373 | isFood = false; 374 | break; 375 | default: 376 | break; 377 | } 378 | } 379 | } 380 | ~PotionTraits() override = default; 381 | 382 | bool PassFilter(RE::TESForm* a_item) const override 383 | { 384 | const auto potion = a_item->As(); 385 | 386 | if (isPoison && potion->IsPoison() != *isPoison) { 387 | return false; 388 | } 389 | if (isFood && potion->IsFood() != *isFood) { 390 | return false; 391 | } 392 | return true; 393 | } 394 | 395 | private: 396 | // members 397 | nullable isPoison; 398 | nullable isFood; 399 | }; 400 | 401 | class IngredientTraits : public Traits 402 | { 403 | public: 404 | IngredientTraits(const std::string& a_traits) 405 | { 406 | switch (string::const_hash(a_traits)) { 407 | case "F"_h: 408 | isFood = true; 409 | break; 410 | case "-F"_h: 411 | isFood = false; 412 | break; 413 | default: 414 | break; 415 | } 416 | } 417 | ~IngredientTraits() override = default; 418 | 419 | bool PassFilter(RE::TESForm* a_item) const override 420 | { 421 | if (isFood && a_item->As()->IsFood() != *isFood) { 422 | return false; 423 | } 424 | return true; 425 | } 426 | 427 | private: 428 | // members 429 | nullable isFood; 430 | }; 431 | 432 | class BookTraits : public Traits 433 | { 434 | public: 435 | BookTraits(const std::string& a_traits) 436 | { 437 | const auto traits = distribution::split_entry(a_traits); 438 | for (auto& trait : traits) { 439 | switch (string::const_hash(trait)) { 440 | case "S"_h: 441 | teachesSpell = true; 442 | break; 443 | case "-S"_h: 444 | teachesSpell = false; 445 | break; 446 | case "AV"_h: 447 | teachesSkill = true; 448 | break; 449 | case "-AV"_h: 450 | teachesSkill = false; 451 | break; 452 | default: 453 | actorValue = string::to_num(trait); 454 | break; 455 | } 456 | } 457 | } 458 | ~BookTraits() override = default; 459 | 460 | bool PassFilter(RE::TESForm* a_item) const override 461 | { 462 | const auto book = a_item->As(); 463 | 464 | if (teachesSpell && book->TeachesSpell() != *teachesSpell) { 465 | return false; 466 | } 467 | if (teachesSkill && book->TeachesSkill() != *teachesSkill) { 468 | return false; 469 | } 470 | if (actorValue) { 471 | const auto taughtSpell = book->GetSpell(); 472 | if (book->GetSkill() != *actorValue && (taughtSpell && AV::GetAssociatedSkill(taughtSpell) != *actorValue)) { 473 | return false; 474 | } 475 | } 476 | return true; 477 | } 478 | 479 | private: 480 | // members 481 | nullable teachesSpell; 482 | nullable teachesSkill; 483 | nullable actorValue; 484 | }; 485 | 486 | class SoulGemTraits : public Traits 487 | { 488 | public: 489 | SoulGemTraits(const std::string& a_traits) 490 | { 491 | auto traits = distribution::split_entry(a_traits); 492 | for (auto& trait : traits) { 493 | if (trait == "BLACK") { 494 | black = true; 495 | } else if (trait == "-BLACK") { 496 | black = false; 497 | } else if (trait.contains("SOUL(")) { 498 | soulSize = detail::get_single_value(trait); 499 | } else { // GEM 500 | gemSize = detail::get_single_value(trait); 501 | } 502 | } 503 | } 504 | ~SoulGemTraits() override = default; 505 | 506 | bool PassFilter(RE::TESForm* a_item) const override 507 | { 508 | const auto soulGem = a_item->As(); 509 | 510 | if (black && soulGem->CanHoldNPCSoul() != *black) { 511 | return false; 512 | } 513 | if (soulSize && soulGem->GetContainedSoul() != *soulSize) { 514 | return false; 515 | } 516 | if (gemSize && soulGem->GetMaximumCapacity() != *gemSize) { 517 | return false; 518 | } 519 | 520 | return true; 521 | } 522 | 523 | private: 524 | // members 525 | nullable black; 526 | nullable soulSize; 527 | nullable gemSize; 528 | }; 529 | 530 | class SpellTraits : public Traits 531 | { 532 | public: 533 | SpellTraits(const std::string& a_traits) 534 | { 535 | auto traits = distribution::split_entry(a_traits); 536 | for (auto& trait : traits) { 537 | if (trait.contains("ST(")) { 538 | spellType = detail::get_single_value(trait); 539 | } else if (trait.contains("D(")) { 540 | deliveryType = detail::get_single_value(trait); 541 | } else if (trait.contains("CT(")) { 542 | castingType = detail::get_single_value(trait); 543 | } else if (trait == "H") { 544 | isHostile = true; 545 | } else if (trait == "-H") { 546 | isHostile = false; 547 | } else { 548 | skill = detail::get_single_value(trait); 549 | } 550 | } 551 | } 552 | ~SpellTraits() override = default; 553 | 554 | bool PassFilter(RE::TESForm* a_item) const override 555 | { 556 | const auto spell = a_item->As(); 557 | 558 | if (spellType && spell->GetSpellType() != *spellType) { 559 | return false; 560 | } 561 | if (castingType && spell->GetCastingType() != *castingType) { 562 | return false; 563 | } 564 | if (deliveryType && spell->GetDelivery() != *deliveryType) { 565 | return false; 566 | } 567 | if (skill && AV::GetAssociatedSkill(spell) != *skill) { 568 | return false; 569 | } 570 | if (isHostile && spell->IsHostile() != *isHostile) { 571 | return false; 572 | } 573 | 574 | return true; 575 | } 576 | 577 | private: 578 | // members 579 | nullable spellType{}; 580 | nullable castingType{}; 581 | nullable deliveryType{}; 582 | nullable skill{}; 583 | nullable isHostile{}; 584 | }; 585 | 586 | class FurnitureTraits : public Traits 587 | { 588 | public: 589 | FurnitureTraits(const std::string& a_traits) 590 | { 591 | auto traits = distribution::split_entry(a_traits); 592 | for (auto& trait : traits) { 593 | if (trait.contains("BT(")) { 594 | benchType = detail::get_single_value(trait); 595 | } else if (trait.contains("T(")) { 596 | furnitureType = detail::get_single_value(trait); 597 | } else if (trait.contains("US(")) { 598 | useSkill = detail::get_single_value(trait); 599 | } 600 | } 601 | } 602 | ~FurnitureTraits() override = default; 603 | 604 | bool PassFilter(RE::TESForm* a_item) const override 605 | { 606 | const auto furniture = a_item->As(); 607 | 608 | if (benchType && furniture->workBenchData.benchType != *benchType) { 609 | return false; 610 | } 611 | 612 | if (furnitureType && GetFurnitureType(furniture) != *furnitureType) { 613 | return false; 614 | } 615 | 616 | if (useSkill && furniture->workBenchData.usesSkill != *useSkill) { 617 | return false; 618 | } 619 | 620 | return true; 621 | } 622 | 623 | private: 624 | static std::int32_t GetFurnitureType(const RE::TESFurniture* a_furniture) 625 | { 626 | using FLAGS = RE::TESFurniture::ActiveMarker; 627 | 628 | const auto flags = a_furniture->furnFlags; 629 | if (flags.any(FLAGS::kIsPerch)) { 630 | return 0; 631 | } 632 | if (flags.any(FLAGS::kCanLean)) { 633 | return 1; 634 | } 635 | if (flags.any(FLAGS::kCanSit)) { 636 | return 2; 637 | } 638 | if (flags.any(FLAGS::kCanSleep)) { 639 | return 3; 640 | } 641 | 642 | return -1; 643 | } 644 | 645 | // members 646 | nullable furnitureType{}; 647 | nullable benchType{}; 648 | nullable useSkill{}; 649 | }; 650 | } 651 | using TraitsPtr = std::shared_ptr; 652 | -------------------------------------------------------------------------------- /src/Cache.cpp: -------------------------------------------------------------------------------- 1 | #include "Cache.h" 2 | 3 | namespace Cache 4 | { 5 | bool FormType::IsFilter(RE::FormType a_type) 6 | { 7 | return std::ranges::find(set, a_type) != set.end(); 8 | } 9 | 10 | Item::TYPE Item::GetType(const std::string& a_type) 11 | { 12 | const auto it = std::ranges::find_if(itemTypes, 13 | [&](const auto& element) { return element.first == a_type; }); 14 | return it != itemTypes.end() ? 15 | static_cast(it - std::begin(itemTypes)) : 16 | kNone; 17 | } 18 | 19 | std::string_view Item::GetType(TYPE a_type) 20 | { 21 | return itemTypes[a_type].first; 22 | } 23 | 24 | std::string_view ActorValue::GetActorValue(RE::ActorValue a_av) 25 | { 26 | const auto it = r_map.find(a_av); 27 | return it != r_map.end() ? it->second : "None"; 28 | } 29 | 30 | RE::ActorValue ActorValue::GetActorValue(std::string_view a_av) 31 | { 32 | const auto it = map.find(a_av); 33 | return it != map.end() ? it->second : RE::ActorValue::kNone; 34 | } 35 | 36 | RE::ActorValue ActorValue::GetAssociatedSkill(RE::MagicItem* a_spell) 37 | { 38 | if (auto effect = a_spell->GetCostliestEffectItem(); effect && effect->baseEffect) { 39 | return effect->baseEffect->data.associatedSkill; 40 | } 41 | return RE::ActorValue::kNone; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Distribute.cpp: -------------------------------------------------------------------------------- 1 | #include "Distribute.h" 2 | 3 | void Distribute::AddKeywords() 4 | { 5 | ForEachDistributable_MT([](Distributable& a_distributable) { 6 | distribute(a_distributable); 7 | }); 8 | 9 | logger::info("{:*^50}", "RESULT"); 10 | 11 | ForEachDistributable([](Distributable& a_distributable) { 12 | log_keyword_count(a_distributable); 13 | }); 14 | 15 | ForEachDistributable([](Distributable& a_distributable) { 16 | if (a_distributable.GetType() != ITEM::kBook) { 17 | a_distributable.clear(); 18 | } 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/ExclusiveGroups.cpp: -------------------------------------------------------------------------------- 1 | #include "ExclusiveGroups.h" 2 | #include "KeywordData.h" 3 | 4 | namespace ExclusiveGroups 5 | { 6 | bool INI::TryParse(const std::string& a_key, const std::string& a_value, const std::string& a_path) 7 | { 8 | if (a_key != "ExclusiveGroup") { 9 | return false; 10 | } 11 | 12 | const auto sections = string::split(a_value, "|"); 13 | const auto size = sections.size(); 14 | 15 | if (size < 2) { 16 | logger::warn("IGNORED: ExclusiveGroup must have a name and at least one Form Filter: {} = {}"sv, a_key, a_value); 17 | return true; 18 | } 19 | 20 | auto split_IDs = distribution::split_entry(sections[1]); 21 | 22 | if (split_IDs.empty()) { 23 | logger::warn("ExclusiveGroup must have at least one Form Filter : {} = {}"sv, a_key, a_value); 24 | return true; 25 | } 26 | 27 | RawExclusiveGroup group{}; 28 | group.name = sections[0]; 29 | group.path = a_path; 30 | 31 | for (auto& IDs : split_IDs) { 32 | if (IDs.at(0) == '-') { 33 | IDs.erase(0, 1); 34 | group.formIDs.NOT.push_back(distribution::get_record(IDs)); 35 | } else { 36 | group.formIDs.MATCH.push_back(distribution::get_record(IDs)); 37 | } 38 | } 39 | 40 | exclusiveGroups.emplace_back(group); 41 | 42 | return true; 43 | } 44 | 45 | void formID_to_form(RE::TESDataHandler* const dataHandler, const std::string& groupName, RawVec& a_rawFormVec, ProcessedVec& a_formVec) 46 | { 47 | if (a_rawFormVec.empty()) { 48 | return; 49 | } 50 | 51 | const auto& keywordArray = dataHandler->GetFormArray(); 52 | 53 | for (auto& formOrEditorID : a_rawFormVec) { 54 | std::visit(overload{ 55 | [&](FormModPair& formMod) { 56 | auto& [formID, modName] = formMod; 57 | if (g_mergeMapperInterface) { 58 | Keyword::detail::get_merged_IDs(formID, modName); 59 | } 60 | if (modName && !formID) { 61 | buffered_logger::error("\t\tExclusive Group ({}): Attempted to add plugin ({}) to the group.", groupName, *modName); 62 | } else if (formID) { 63 | if (auto filterForm = modName ? 64 | dataHandler->LookupForm(*formID, *modName) : 65 | RE::TESForm::LookupByID(*formID)) { 66 | const auto formType = filterForm->GetFormType(); 67 | if (const auto keyword = filterForm->As(); keyword && formType == RE::FormType::Keyword) { 68 | a_formVec.push_back(keyword); 69 | } else { 70 | buffered_logger::error("\t\tExclusive Group ({}): Attempted to add invalid Form {} [0x{:X}] ({}) to the group.", groupName, formType, *formID, modName.value_or("")); 71 | } 72 | } else { 73 | buffered_logger::error("\t\tExclusive Group ({}): Form doesn't exist", groupName, *formID, modName.value_or("")); 74 | } 75 | } 76 | }, 77 | [&](std::string& editorID) { 78 | if (auto filterForm = RE::TESForm::LookupByEditorID(editorID)) { 79 | const auto formType = filterForm->GetFormType(); 80 | if (const auto keyword = filterForm->As(); keyword && formType == RE::FormType::Keyword) { 81 | a_formVec.push_back(keyword); 82 | } else { 83 | buffered_logger::error("\t\tExclusive Group ({}): Attempted to add invalid Form {} to the group. Expected {}, but got {}", groupName, editorID, RE::FormType::Keyword, formType); 84 | } 85 | } else { 86 | if (auto keyword = Keyword::detail::find_existing_keyword(keywordArray, editorID, true)) { 87 | a_formVec.push_back(keyword); 88 | } else { 89 | buffered_logger::error("\t\tExclusive Group ({}): Attempted to add unknown Keyword {} to the group.", groupName, editorID); 90 | } 91 | } 92 | } }, 93 | formOrEditorID); 94 | } 95 | } 96 | 97 | void Manager::LookupExclusiveGroups(INI::ExclusiveGroupsVec& exclusiveGroups) 98 | { 99 | groups.clear(); 100 | linkedGroups.clear(); 101 | 102 | const auto dataHandler = RE::TESDataHandler::GetSingleton(); 103 | 104 | for (auto& [name, filterIDs, path] : exclusiveGroups) { 105 | auto& forms = groups[name]; 106 | ProcessedVec match{}; 107 | ProcessedVec formsNot{}; 108 | 109 | formID_to_form(dataHandler, name, filterIDs.MATCH, match); 110 | formID_to_form(dataHandler, name, filterIDs.NOT, formsNot); 111 | 112 | for (const auto& form : match) { 113 | if (std::holds_alternative(form)) { 114 | if (const auto keyword = std::get(form)->As(); keyword) { 115 | forms.insert(keyword); 116 | } 117 | } 118 | } 119 | 120 | for (auto& form : formsNot) { 121 | if (std::holds_alternative(form)) { 122 | if (const auto keyword = std::get(form)->As(); keyword) { 123 | forms.erase(keyword); 124 | } 125 | } 126 | } 127 | } 128 | 129 | // Remove empty groups 130 | std::erase_if(groups, [](const auto& pair) { return pair.second.empty(); }); 131 | 132 | for (auto& [name, forms] : groups) { 133 | for (auto& form : forms) { 134 | linkedGroups[form].insert(name); 135 | } 136 | } 137 | } 138 | 139 | void Manager::LogExclusiveGroupsLookup() 140 | { 141 | if (groups.empty()) { 142 | return; 143 | } 144 | 145 | logger::info("{:*^50}", "EXCLUSIVE GROUPS"); 146 | 147 | for (const auto& [group, forms] : groups) { 148 | logger::info("Adding '{}' exclusive group", group); 149 | for (const auto& form : forms) { 150 | logger::info("\t{}", describe(form)); 151 | } 152 | } 153 | } 154 | 155 | Set Manager::MutuallyExclusiveKeywordsForKeyword(RE::BGSKeyword* form) const 156 | { 157 | Set forms{}; 158 | if (auto it = linkedGroups.find(form); it != linkedGroups.end()) { 159 | std::ranges::for_each(it->second, [&](const Group& name) { 160 | const auto& group = groups.at(name); 161 | forms.insert(group.begin(), group.end()); 162 | }); 163 | } 164 | 165 | // Remove self from the list. 166 | forms.erase(form); 167 | 168 | return forms; 169 | } 170 | 171 | const GroupKeywordsMap& ExclusiveGroups::Manager::GetGroups() const 172 | { 173 | return groups; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Hooks.cpp: -------------------------------------------------------------------------------- 1 | #include "Hooks.h" 2 | #include "Distribute.h" 3 | 4 | namespace Hooks 5 | { 6 | struct InitItemImpl 7 | { 8 | static void thunk(RE::TESObjectBOOK* a_this) 9 | { 10 | func(a_this); 11 | 12 | // if InitItem fires after DataLoaded 13 | if (Keyword::books) { 14 | Distribute::distribute(a_this, Keyword::books.GetKeywords()); 15 | } 16 | } 17 | static inline REL::Relocation func; 18 | static inline std::size_t idx = 0x13; 19 | }; 20 | 21 | void Install() 22 | { 23 | logger::info("{:*^50}", "HOOKS"); 24 | 25 | stl::write_vfunc(); 26 | 27 | logger::info("Installed TESObjectBOOK InitItemImpl hook"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/KeywordData.cpp: -------------------------------------------------------------------------------- 1 | #include "KeywordData.h" 2 | 3 | namespace Keyword 4 | { 5 | bool Data::operator==(const Data& a_rhs) const 6 | { 7 | if (!keyword || !a_rhs.keyword) { 8 | return false; 9 | } 10 | return keyword->GetFormID() == a_rhs.keyword->GetFormID(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/LogBuffer.cpp: -------------------------------------------------------------------------------- 1 | #include "LogBuffer.h" 2 | -------------------------------------------------------------------------------- /src/LookupConfigs.cpp: -------------------------------------------------------------------------------- 1 | #include "LookupConfigs.h" 2 | #include "ExclusiveGroups.h" 3 | 4 | namespace INI 5 | { 6 | namespace detail 7 | { 8 | inline std::pair parse_config(std::string& a_value, const std::string& a_path) 9 | { 10 | #ifdef SKYRIMVR 11 | // swap dawnguard and dragonborn forms 12 | // VR apparently does not load masters in order so the lookup fails 13 | static const srell::regex re_dawnguard(R"((0x0*2)([0-9a-f]{6}))", srell::regex_constants::optimize | srell::regex::icase); 14 | a_value = regex_replace(a_value, re_dawnguard, "0x$2~Dawnguard.esm"); 15 | 16 | static const srell::regex re_dragonborn(R"((0x0*4)([0-9a-f]{6}))", srell::regex_constants::optimize | srell::regex::icase); 17 | a_value = regex_replace(a_value, re_dragonborn, "0x$2~Dragonborn.esm"); 18 | #endif 19 | 20 | Data data{}; 21 | ITEM::TYPE type{ ITEM::kNone }; 22 | 23 | const auto sections = string::split(a_value, "|"); 24 | const auto size = sections.size(); 25 | 26 | //[FORMID/ESP] / string 27 | if (kFormID < size) { 28 | data.rawForm = distribution::get_record(sections[kFormID]); 29 | } 30 | 31 | //TYPE 32 | if (kType < size) { 33 | if (const auto& typeStr = sections[kType]; !typeStr.empty()) { 34 | type = ITEM::GetType(typeStr); 35 | } 36 | } 37 | 38 | //FILTERS 39 | if (kFilters < size) { 40 | auto filterStr = distribution::split_entry(sections[kFilters]); 41 | 42 | for (auto& filter : filterStr) { 43 | if (filter.contains('+')) { 44 | auto ALL = distribution::split_entry(filter, "+"); 45 | std::ranges::transform(ALL, std::back_inserter(data.rawFilters.ALL), [](const auto& filter_str) { 46 | return distribution::get_record(filter_str); 47 | }); 48 | } else if (filter.at(0) == '-') { 49 | filter.erase(0, 1); 50 | 51 | data.rawFilters.NOT.emplace_back(distribution::get_record(filter)); 52 | 53 | } else if (filter.at(0) == '*') { 54 | filter.erase(0, 1); 55 | data.rawFilters.ANY.emplace_back(filter); 56 | 57 | } else { 58 | data.rawFilters.MATCH.emplace_back(distribution::get_record(filter)); 59 | } 60 | } 61 | } 62 | 63 | //TRAITS 64 | if (kTraits < size) { 65 | auto& traitsStr = sections[kTraits]; 66 | switch (type) { 67 | case ITEM::kArmor: 68 | data.traits = std::make_unique(traitsStr); 69 | break; 70 | case ITEM::kWeapon: 71 | data.traits = std::make_unique(traitsStr); 72 | break; 73 | case ITEM::kAmmo: 74 | data.traits = std::make_unique(traitsStr); 75 | break; 76 | case ITEM::kMagicEffect: 77 | data.traits = std::make_unique(traitsStr); 78 | break; 79 | case ITEM::kPotion: 80 | data.traits = std::make_unique(traitsStr); 81 | break; 82 | case ITEM::kIngredient: 83 | data.traits = std::make_unique(traitsStr); 84 | break; 85 | case ITEM::kBook: 86 | data.traits = std::make_unique(traitsStr); 87 | break; 88 | case ITEM::kSoulGem: 89 | data.traits = std::make_unique(traitsStr); 90 | break; 91 | case ITEM::kSpell: 92 | case ITEM::kEnchantmentItem: 93 | case ITEM::kScroll: 94 | data.traits = std::make_unique(traitsStr); 95 | break; 96 | case ITEM::kFurniture: 97 | data.traits = std::make_unique(traitsStr); 98 | break; 99 | default: 100 | break; 101 | } 102 | } 103 | 104 | //CHANCE 105 | if (INI::kChance < size) { 106 | const auto& chanceStr = sections[kChance]; 107 | if (distribution::is_valid_entry(chanceStr)) { 108 | data.chance = string::to_num(chanceStr); 109 | } 110 | } 111 | 112 | //PATH 113 | data.path = a_path; 114 | 115 | return std::make_pair(std::move(data), type); 116 | } 117 | } 118 | 119 | std::pair GetConfigs() 120 | { 121 | logger::info("{:*^50}", "INI"); 122 | 123 | std::vector configs = distribution::get_configs(R"(Data\)", "_KID"sv); 124 | 125 | if (configs.empty()) { 126 | logger::warn("No .ini files with _KID suffix were found within the Data folder, aborting..."); 127 | return { false, false }; 128 | } 129 | 130 | logger::info("{} matching inis found", configs.size()); 131 | 132 | std::ranges::sort(configs); 133 | 134 | bool shouldLogErrors{ false }; 135 | 136 | for (auto& path : configs) { 137 | logger::info("\tINI : {}", path); 138 | 139 | CSimpleIniA ini; 140 | ini.SetUnicode(); 141 | ini.SetMultiKey(); 142 | 143 | if (const auto rc = ini.LoadFile(path.c_str()); rc < 0) { 144 | logger::error("\t\tcouldn't read INI"); 145 | continue; 146 | } 147 | 148 | string::replace_first_instance(path, "Data\\", ""); 149 | 150 | if (const auto values = ini.GetSection(""); values) { 151 | for (auto& [key, entry] : *values) { 152 | try { 153 | std::string entryStr{ entry }; 154 | 155 | if (ExclusiveGroups::INI::TryParse(key.pItem, entryStr, path)) { 156 | continue; 157 | } 158 | 159 | auto [data, type] = detail::parse_config(entryStr, path); 160 | INIs[type].emplace_back(std::move(data)); 161 | } catch (...) { 162 | logger::error("\t\tFailed to parse entry [Keyword = {}]", entry); 163 | shouldLogErrors = true; 164 | } 165 | } 166 | } 167 | } 168 | 169 | return { true, shouldLogErrors }; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/LookupFilters.cpp: -------------------------------------------------------------------------------- 1 | #include "LookupFilters.h" 2 | #include "ExclusiveGroups.h" 3 | 4 | namespace Filter 5 | { 6 | void SanitizeString(std::string& a_string) 7 | { 8 | std::ranges::transform(a_string, a_string.begin(), 9 | [](unsigned char c) { return static_cast(std::tolower(c)); }); 10 | } 11 | 12 | void SanitizePath(std::string& a_string) 13 | { 14 | SanitizeString(a_string); 15 | 16 | a_string = srell::regex_replace(a_string, srell::regex(R"(/+|\\+)"), R"(\)"); 17 | a_string = srell::regex_replace(a_string, srell::regex(R"(^\\+)"), ""); 18 | a_string = srell::regex_replace(a_string, srell::regex(R"(.*?[^\s]meshes\\|^meshes\\)", srell::regex::icase), ""); 19 | } 20 | 21 | Data::Data(ProcessedFilters a_processedFilters, TraitsPtr a_traits, Chance a_chance) : 22 | processedFilters(std::move(a_processedFilters)), 23 | traits(std::move(a_traits)), 24 | chance(a_chance) 25 | {} 26 | } 27 | 28 | namespace Item 29 | { 30 | Data::Data(RE::TESForm* a_item) 31 | { 32 | item = a_item; 33 | edid = EDID::get_editorID(a_item); 34 | name = a_item->GetName(); 35 | 36 | if (const auto tesModel = a_item->As()) { 37 | model = tesModel->GetModel(); 38 | Filter::SanitizeString(model); 39 | } 40 | 41 | const auto kywdForm = a_item->As(); 42 | keywords = { kywdForm->keywords, kywdForm->keywords + kywdForm->numKeywords }; 43 | } 44 | 45 | bool Data::PassedFilters(RE::BGSKeyword* a_keyword, const FilterData& a_filters) 46 | { 47 | // Skip if keyword exists 48 | if (keywords.contains(a_keyword)) { 49 | return false; 50 | } 51 | 52 | // Skip if keyword from related exclusive groups is already present. 53 | if (HasMutuallyExclusiveKeyword(a_keyword)) { 54 | return false; 55 | } 56 | 57 | // Fail chance first to avoid running unnecessary checks 58 | if (a_filters.chance < 100) { 59 | // create unique seed based on keyword editorID (can't use formID because it can be dynamic) and item formID 60 | // item formID alone would result in same RNG chance for different keywords 61 | const auto seed = hash::szudzik_pair( 62 | hash::fnv1a_32(a_keyword->GetFormEditorID()), 63 | item->GetFormID()); 64 | 65 | const auto randNum = RNG(seed).generate(0, 100); 66 | if (randNum > a_filters.chance) { 67 | return false; 68 | } 69 | } 70 | 71 | // STRING,FORM 72 | if (!a_filters.processedFilters.ALL.empty() && !HasFormOrStringFilter(a_filters.processedFilters.ALL, true)) { 73 | return false; 74 | } 75 | if (!a_filters.processedFilters.NOT.empty() && HasFormOrStringFilter(a_filters.processedFilters.NOT)) { 76 | return false; 77 | } 78 | if (!a_filters.processedFilters.MATCH.empty() && !HasFormOrStringFilter(a_filters.processedFilters.MATCH)) { 79 | return false; 80 | } 81 | if (!a_filters.processedFilters.ANY.empty() && !ContainsStringFilter(a_filters.processedFilters.ANY)) { 82 | return false; 83 | } 84 | 85 | // TRAITS 86 | if (a_filters.traits && !a_filters.traits->PassFilter(item)) { 87 | return false; 88 | } 89 | 90 | keywords.emplace(a_keyword); 91 | 92 | return true; 93 | } 94 | 95 | bool Data::HasMutuallyExclusiveKeyword(RE::BGSKeyword* otherKeyword) const 96 | { 97 | auto excludedForms = ExclusiveGroups::Manager::GetSingleton()->MutuallyExclusiveKeywordsForKeyword(otherKeyword); 98 | if (excludedForms.empty()) { 99 | return false; 100 | } 101 | return std::ranges::any_of(excludedForms, [&](auto keyword) { 102 | return keywords.contains(keyword); 103 | }); 104 | } 105 | 106 | bool Data::HasFormOrStringFilter(const ProcessedVec& a_processed, bool a_all) const 107 | { 108 | const auto has_form_or_string_filter = [&](const FormOrString& a_formString) { 109 | bool result = false; 110 | std::visit(overload{ 111 | [&](RE::TESForm* a_form) { 112 | result = HasFormFilter(a_form); 113 | }, 114 | [&](const RE::TESFile* a_file) { 115 | result = a_file->IsFormInMod(item->GetFormID()); 116 | }, 117 | [&](const std::string& a_str) { 118 | result = HasStringFilter(a_str); 119 | } }, 120 | a_formString); 121 | return result; 122 | }; 123 | 124 | if (a_all) { 125 | return std::ranges::all_of(a_processed, has_form_or_string_filter); 126 | } else { 127 | return std::ranges::any_of(a_processed, has_form_or_string_filter); 128 | } 129 | } 130 | 131 | static RE::EffectSetting* GetCostliestMGEF(RE::TESForm* a_form) 132 | { 133 | if (a_form) { 134 | if (const auto magicItem = a_form->As()) { 135 | auto effect = magicItem->GetCostliestEffectItem(); 136 | if (const auto mgef = effect ? effect->baseEffect : nullptr) { 137 | return mgef; 138 | } 139 | } 140 | } 141 | return nullptr; 142 | } 143 | 144 | bool Data::HasFormFilter(RE::TESForm* a_formFilter) const 145 | { 146 | switch (a_formFilter->GetFormType()) { 147 | case RE::FormType::Weapon: 148 | case RE::FormType::Ammo: 149 | case RE::FormType::Scroll: 150 | case RE::FormType::Book: 151 | case RE::FormType::KeyMaster: 152 | case RE::FormType::SoulGem: 153 | case RE::FormType::Flora: 154 | case RE::FormType::Activator: 155 | case RE::FormType::Furniture: 156 | case RE::FormType::Race: 157 | case RE::FormType::TalkingActivator: 158 | return item == a_formFilter; 159 | case RE::FormType::Keyword: 160 | { 161 | auto keyword = a_formFilter->As(); 162 | if (keywords.contains(keyword)) { 163 | return true; 164 | } 165 | if (auto mgef = GetCostliestMGEF(item)) { 166 | return mgef->HasKeyword(keyword); 167 | } 168 | if (const auto book = item->As()) { 169 | if (auto mgef = GetCostliestMGEF(book->GetSpell())) { 170 | return mgef->HasKeyword(keyword); 171 | } 172 | } 173 | if (const auto armor = item->As()) { 174 | if (auto mgef = GetCostliestMGEF(armor->formEnchanting)) { 175 | return mgef->HasKeyword(keyword); 176 | } 177 | } 178 | if (const auto weapon = item->As()) { 179 | if (auto mgef = GetCostliestMGEF(weapon->formEnchanting)) { 180 | return mgef->HasKeyword(keyword); 181 | } 182 | } 183 | return false; 184 | } 185 | case RE::FormType::Armor: 186 | { 187 | if (const auto race = item->As()) { 188 | return race->skin == a_formFilter; 189 | } 190 | return item == a_formFilter; 191 | } 192 | case RE::FormType::Location: 193 | { 194 | const auto loc = item->As(); 195 | const auto filterLoc = a_formFilter->As(); 196 | 197 | return loc && filterLoc && (loc == filterLoc || loc->IsParent(filterLoc)); 198 | } 199 | case RE::FormType::Projectile: 200 | { 201 | if (const auto ammo = item->As()) { 202 | return ammo->data.projectile == a_formFilter; 203 | } else if (const auto mgef = item->As()) { 204 | return mgef->data.projectileBase == a_formFilter; 205 | } 206 | return false; 207 | } 208 | case RE::FormType::MagicEffect: 209 | { 210 | if (const auto spell = item->As()) { 211 | return std::ranges::any_of(spell->effects, [&](const auto& effect) { 212 | return effect && effect->baseEffect == a_formFilter; 213 | }); 214 | } 215 | return item == a_formFilter; 216 | } 217 | case RE::FormType::EffectShader: 218 | { 219 | const auto mgef = item->As(); 220 | return mgef && (mgef->data.effectShader == a_formFilter || mgef->data.enchantShader == a_formFilter); 221 | } 222 | case RE::FormType::ReferenceEffect: 223 | { 224 | const auto mgef = item->As(); 225 | return mgef && (mgef->data.hitVisuals == a_formFilter || mgef->data.enchantVisuals == a_formFilter); 226 | } 227 | case RE::FormType::ArtObject: 228 | { 229 | if (const auto mgef = item->As()) { 230 | return mgef->data.castingArt == a_formFilter || mgef->data.hitEffectArt == a_formFilter || mgef->data.enchantEffectArt == a_formFilter; 231 | } 232 | if (const auto race = item->As()) { 233 | return race->dismemberBlood == a_formFilter; 234 | } 235 | return false; 236 | } 237 | case RE::FormType::MusicType: 238 | { 239 | const auto loc = item->As(); 240 | return loc && loc->musicType == a_formFilter; 241 | } 242 | case RE::FormType::Faction: 243 | { 244 | const auto loc = item->As(); 245 | return loc && loc->unreportedCrimeFaction == a_formFilter; 246 | } 247 | case RE::FormType::AlchemyItem: 248 | case RE::FormType::Ingredient: 249 | case RE::FormType::Misc: 250 | { 251 | if (const auto flora = item->As()) { 252 | return flora->produceItem == a_formFilter; 253 | } 254 | return item == a_formFilter; 255 | } 256 | case RE::FormType::Spell: 257 | { 258 | const auto spell = a_formFilter->As(); 259 | if (const auto book = item->As()) { 260 | return book->GetSpell() == spell; 261 | } 262 | if (const auto race = item->As()) { 263 | return race->actorEffects && race->actorEffects->GetIndex(spell).has_value(); 264 | } 265 | if (const auto furniture = item->As()) { 266 | return furniture->associatedForm == a_formFilter; 267 | } 268 | return item == a_formFilter; 269 | } 270 | case RE::FormType::Enchantment: 271 | { 272 | if (const auto weapon = item->As()) { 273 | return weapon->formEnchanting == a_formFilter; 274 | } 275 | if (const auto armor = item->As()) { 276 | return armor->formEnchanting == a_formFilter; 277 | } 278 | if (const auto enchantment = item->As()) { 279 | return enchantment == a_formFilter || enchantment->data.baseEnchantment == a_formFilter; 280 | } 281 | return false; 282 | } 283 | case RE::FormType::EquipSlot: 284 | { 285 | if (const auto equipType = item->As()) { 286 | return equipType->GetEquipSlot() == a_formFilter; 287 | } 288 | return false; 289 | } 290 | case RE::FormType::VoiceType: 291 | { 292 | if (const auto talkingActivator = item->As()) { 293 | return talkingActivator->GetObjectVoiceType() == a_formFilter; 294 | } 295 | return false; 296 | } 297 | case RE::FormType::LeveledItem: 298 | { 299 | if (const auto flora = item->As()) { 300 | return flora->produceItem == a_formFilter; 301 | } 302 | return false; 303 | } 304 | case RE::FormType::Water: 305 | { 306 | if (const auto activator = item->As()) { 307 | return activator->GetWaterType() == a_formFilter; 308 | } 309 | return false; 310 | } 311 | case RE::FormType::Perk: 312 | { 313 | if (const auto spell = item->As()) { 314 | return spell->data.castingPerk == a_formFilter; 315 | } 316 | if (const auto mgef = item->As()) { 317 | return mgef->data.perk == a_formFilter; 318 | } 319 | return false; 320 | } 321 | case RE::FormType::FormList: 322 | { 323 | if (const auto enchantment = item->As()) { 324 | if (enchantment->data.wornRestrictions == a_formFilter) { 325 | return true; 326 | } 327 | } 328 | 329 | bool result = false; 330 | const auto list = a_formFilter->As(); 331 | list->ForEachForm([&](auto* a_form) { 332 | if (result = HasFormFilter(a_form); result) { 333 | return RE::BSContainer::ForEachResult::kStop; 334 | } 335 | return RE::BSContainer::ForEachResult::kContinue; 336 | }); 337 | return result; 338 | } 339 | default: 340 | return false; 341 | } 342 | } 343 | 344 | bool Data::HasStringFilter(const std::string& a_str) const 345 | { 346 | if (string::iequals(edid, a_str) || string::iequals(name, a_str)) { 347 | return true; 348 | } 349 | 350 | if (AV::map.contains(a_str)) { 351 | switch (item->GetFormType()) { 352 | case RE::FormType::Weapon: 353 | { 354 | const auto weapon = item->As(); 355 | return AV::GetActorValue(weapon->weaponData.skill.get()) == a_str; 356 | } 357 | case RE::FormType::MagicEffect: 358 | { 359 | const auto mgef = item->As(); 360 | return AV::GetActorValue(mgef->data.associatedSkill) == a_str || 361 | AV::GetActorValue(mgef->data.primaryAV) == a_str || 362 | AV::GetActorValue(mgef->data.secondaryAV) == a_str || 363 | AV::GetActorValue(mgef->data.resistVariable) == a_str; 364 | } 365 | case RE::FormType::Book: 366 | { 367 | const auto book = item->As(); 368 | auto skill = RE::ActorValue::kNone; 369 | if (book->TeachesSkill()) { 370 | skill = book->data.teaches.actorValueToAdvance; 371 | } else if (book->TeachesSpell() && book->data.teaches.spell) { 372 | skill = AV::GetAssociatedSkill(book->data.teaches.spell); 373 | } 374 | return AV::GetActorValue(skill) == a_str; 375 | } 376 | case RE::FormType::AlchemyItem: 377 | case RE::FormType::Ingredient: 378 | case RE::FormType::Scroll: 379 | case RE::FormType::Spell: 380 | case RE::FormType::Enchantment: 381 | { 382 | const auto magicItem = item->As(); 383 | if (AV::GetActorValue(AV::GetAssociatedSkill(magicItem)) == a_str) { 384 | return true; 385 | } 386 | if (const auto mgef = GetCostliestMGEF(magicItem)) { 387 | return AV::GetActorValue(mgef->data.associatedSkill) == a_str || 388 | AV::GetActorValue(mgef->data.primaryAV) == a_str || 389 | AV::GetActorValue(mgef->data.secondaryAV) == a_str || 390 | AV::GetActorValue(mgef->data.resistVariable) == a_str; 391 | } 392 | return false; 393 | } 394 | default: 395 | return false; 396 | } 397 | } 398 | 399 | if (ARCHETYPE::map.contains(a_str)) { 400 | if (auto mgef = item->As()) { 401 | return std::to_string(mgef->data.archetype) == a_str; 402 | } else if (mgef = GetCostliestMGEF(item); mgef) { 403 | return std::to_string(mgef->data.archetype) == a_str; 404 | } 405 | } 406 | 407 | if (a_str.contains(".nif")) { 408 | return model == a_str; 409 | } 410 | 411 | return false; 412 | } 413 | 414 | bool Data::ContainsStringFilter(const std::vector& a_strings) const 415 | { 416 | return std::ranges::any_of(a_strings, [&](const auto& str) { 417 | if (str.contains(".nif")) { 418 | return model.contains(str); 419 | } 420 | return string::icontains(edid, str) || 421 | string::icontains(name, str) || 422 | std::any_of(keywords.begin(), keywords.end(), [&](const auto& keyword) { 423 | return keyword->formEditorID.contains(str); 424 | }); 425 | }); 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /src/LookupForms.cpp: -------------------------------------------------------------------------------- 1 | #include "LookupForms.h" 2 | 3 | #include "KeywordData.h" 4 | #include "KeywordDependencies.h" 5 | #include "LookupConfigs.h" 6 | 7 | namespace Forms 8 | { 9 | using namespace Keyword; 10 | 11 | bool LookupForms() 12 | { 13 | logger::info("{:*^50}", "LOOKUP"); 14 | 15 | bool empty = true; 16 | ForEachDistributable([&](Distributable& a_distributable) { 17 | a_distributable.LookupForms(); 18 | Dependencies::ResolveKeywords(a_distributable); 19 | if (!a_distributable.empty()) { 20 | empty = false; 21 | } 22 | }); 23 | 24 | return !empty; 25 | } 26 | 27 | void LogFormLookup() 28 | { 29 | logger::info("{:*^50}", "PROCESSING"); 30 | 31 | ForEachDistributable([](const Distributable& a_distributable) { 32 | const auto type = a_distributable.GetType(); 33 | if (const auto& rawKeywords = INI::INIs[type]; !rawKeywords.empty()) { 34 | logger::info("Adding {}/{} keywords to {}", a_distributable.size(), rawKeywords.size(), ITEM::itemTypes[type].second); 35 | } 36 | }); 37 | 38 | // clear raw configs 39 | INI::INIs.clear(); 40 | // clear dependencies map 41 | allKeywords.clear(); 42 | 43 | // Clear logger's buffer to free some memory :) 44 | buffered_logger::clear(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/PCH.cpp: -------------------------------------------------------------------------------- 1 | #include "PCH.h" 2 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "Distribute.h" 2 | #include "Hooks.h" 3 | #include "LookupConfigs.h" 4 | #include "LookupForms.h" 5 | #include "ExclusiveGroups.h" 6 | 7 | namespace MessageHandler 8 | { 9 | bool shouldLookupForms{ false }; 10 | bool shouldLogErrors{ false }; 11 | 12 | void MessageHandler(SKSE::MessagingInterface::Message* a_message) 13 | { 14 | switch (a_message->type) { 15 | case SKSE::MessagingInterface::kPostLoad: 16 | { 17 | std::tie(shouldLookupForms, shouldLogErrors) = INI::GetConfigs(); 18 | Hooks::Install(); 19 | } 20 | break; 21 | case SKSE::MessagingInterface::kPostPostLoad: 22 | { 23 | logger::info("{:*^50}", "MERGES"); 24 | MergeMapperPluginAPI::GetMergeMapperInterface001(); // Request interface 25 | if (g_mergeMapperInterface) { // Use Interface 26 | const auto version = g_mergeMapperInterface->GetBuildNumber(); 27 | logger::info("\tGot MergeMapper interface buildnumber {}", version); 28 | } else { 29 | logger::info("INFO - MergeMapper not detected"); 30 | } 31 | } 32 | break; 33 | case SKSE::MessagingInterface::kDataLoaded: 34 | { 35 | if (shouldLookupForms) { 36 | Timer timer; 37 | timer.start(); 38 | if (Forms::LookupForms()) { 39 | Forms::LogFormLookup(); 40 | timer.end(); 41 | logger::info("Form lookup took {}μs / {}ms", timer.duration_μs(), timer.duration_ms()); 42 | 43 | ExclusiveGroups::Manager::GetSingleton()->LookupExclusiveGroups(); 44 | ExclusiveGroups::Manager::GetSingleton()->LogExclusiveGroupsLookup(); 45 | 46 | timer.start(); 47 | Distribute::AddKeywords(); 48 | timer.end(); 49 | 50 | logger::info("{:*^50}", "STATS"); 51 | logger::info("Distribution took {}μs / {}ms", timer.duration_μs(), timer.duration_ms()); 52 | } 53 | } 54 | 55 | // Clear logger's buffer to free some memory :) 56 | buffered_logger::clear(); 57 | 58 | const SKSE::ModCallbackEvent modEvent{ "KID_KeywordDistributionDone", {}, 0.0f, nullptr }; 59 | SKSE::GetModCallbackEventSource()->SendEvent(&modEvent); 60 | 61 | if (shouldLogErrors) { 62 | const auto error = std::format("[KID] Errors found when reading configs. Check {}.log in {} for more info\n", Version::PROJECT, SKSE::log::log_directory()->string()); 63 | RE::ConsoleLog::GetSingleton()->Print(error.c_str()); 64 | } 65 | } 66 | break; 67 | default: 68 | break; 69 | } 70 | } 71 | } 72 | 73 | #ifdef SKYRIM_AE 74 | extern "C" DLLEXPORT constinit auto SKSEPlugin_Version = []() { 75 | SKSE::PluginVersionData v; 76 | v.PluginVersion(Version::MAJOR); 77 | v.PluginName("Keyword Item Distributor"); 78 | v.AuthorName("powerofthree"); 79 | v.UsesAddressLibrary(); 80 | v.UsesNoStructs(); 81 | v.CompatibleVersions({ SKSE::RUNTIME_LATEST }); 82 | 83 | return v; 84 | }(); 85 | #else 86 | extern "C" DLLEXPORT bool SKSEAPI SKSEPlugin_Query(const SKSE::QueryInterface* a_skse, SKSE::PluginInfo* a_info) 87 | { 88 | a_info->infoVersion = SKSE::PluginInfo::kVersion; 89 | a_info->name = "Keyword Item Distributor"; 90 | a_info->version = Version::MAJOR; 91 | 92 | if (a_skse->IsEditor()) { 93 | logger::critical("Loaded in editor, marking as incompatible"sv); 94 | return false; 95 | } 96 | 97 | const auto ver = a_skse->RuntimeVersion(); 98 | if (ver < 99 | # ifdef SKYRIMVR 100 | SKSE::RUNTIME_VR_1_4_15 101 | # else 102 | SKSE::RUNTIME_1_5_39 103 | # endif 104 | ) { 105 | logger::critical(FMT_STRING("Unsupported runtime version {}"), ver.string()); 106 | return false; 107 | } 108 | 109 | return true; 110 | } 111 | #endif 112 | 113 | void InitializeLog() 114 | { 115 | auto path = logger::log_directory(); 116 | if (!path) { 117 | stl::report_and_fail("Failed to find standard logging directory"sv); 118 | } 119 | 120 | *path /= Version::PROJECT; 121 | *path += ".log"sv; 122 | auto sink = std::make_shared(path->string(), true); 123 | 124 | auto log = std::make_shared("global log"s, std::move(sink)); 125 | 126 | log->set_level(spdlog::level::info); 127 | log->flush_on(spdlog::level::info); 128 | 129 | spdlog::set_default_logger(std::move(log)); 130 | spdlog::set_pattern("[%H:%M:%S:%e] %v"s); 131 | 132 | logger::info(FMT_STRING("{} v{}"), Version::PROJECT, Version::NAME); 133 | } 134 | 135 | extern "C" DLLEXPORT bool SKSEAPI SKSEPlugin_Load(const SKSE::LoadInterface* a_skse) 136 | { 137 | InitializeLog(); 138 | 139 | logger::info("Game version : {}", a_skse->RuntimeVersion().string()); 140 | 141 | SKSE::Init(a_skse); 142 | 143 | SKSE::GetMessagingInterface()->RegisterListener(MessageHandler::MessageHandler); 144 | 145 | return true; 146 | } 147 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kid", 3 | "version-string": "3.5.0", 4 | "description": "Keyword Item Distributor", 5 | "homepage": "https://github.com/powerof3/Keyword-Item-Distributor", 6 | "license": "MIT", 7 | "dependencies": [ 8 | "clib-util", 9 | "mergemapper", 10 | "tsl-ordered-map", 11 | "rsm-binary-io", 12 | "spdlog", 13 | "srell", 14 | "unordered-dense", 15 | "xbyak" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------