├── .clang-format ├── .editorconfig ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── CMakePresets.json ├── LICENSE ├── README.md ├── cmake ├── Version.h.in ├── ports │ └── .keep └── version.rc.in ├── examples ├── HomieSpells_Avatar.json ├── HomieSpells_Emitter.json ├── HomieSpells_Emitter_Adv.json ├── HomieSpells_Events_Swing.json ├── HomieSpells_Firework.json ├── HomieSpells_Follower.json ├── HomieSpells_Follower_Adv.json ├── HomieSpells_Follower_Guards.json ├── HomieSpells_Follower_Guards_Attack.json ├── HomieSpells_Follower_Guards_Ice.json ├── HomieSpells_Follower_Guards_Lightning.json ├── HomieSpells_Homing.json ├── HomieSpells_Multicast.json ├── HomieSpells_Multicast_Adv.json ├── HomieSpells_Multicast_Armageddon.json ├── HomieSpells_Multicast_Homing.json ├── HomieSpells_Multicast_SkyLightning.json ├── HomieSpells_Multicast_ToTargetEvenly.json ├── HomieSpells_Shotgun.json └── test.json ├── gen.bat ├── gifs ├── Accomulate.gif ├── AnimEvent.gif ├── Armageddon.gif ├── Avatar.gif ├── Avatar1.gif ├── DelayedCast.gif ├── Explode.gif ├── FillCircle_FillHalfCircle.gif ├── FireballCannon.gif ├── FireballDown.gif ├── FireballFromSkyOnAttack.gif ├── FireballFromSkyOnShot.gif ├── FollowerFinal1.gif ├── FollowersCast.gif ├── GuardAttackerLightning.gif ├── GuardIce1.gif ├── HalfSphere.gif ├── HomingMulticast.gif ├── Impact.gif ├── Lightnings.gif ├── SwordLightning.gif └── ThroughWalls.gif ├── multiply.py ├── schema.json ├── src ├── Emitters.cpp ├── Emitters.h ├── Followers.cpp ├── Followers.h ├── Homing.cpp ├── Homing.h ├── Hooks.h ├── JsonUtils.cpp ├── JsonUtils.h ├── Multicast.cpp ├── Multicast.h ├── PCH.h ├── Positioning.cpp ├── Positioning.h ├── RuntimeData.cpp ├── RuntimeData.h ├── TriggerFunctions.cpp ├── TriggerFunctions.h ├── Triggers.cpp ├── Triggers.h └── main.cpp └── vcpkg.json /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AccessModifierOffset: -4 3 | AlignAfterOpenBracket: DontAlign 4 | AlignConsecutiveAssignments: 'false' 5 | AlignConsecutiveBitFields: 'false' 6 | AlignConsecutiveDeclarations: 'false' 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: 130 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | bin/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/.gitmodules -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | option(WITH_IMGUI "Add ImGui support" OFF) 2 | option(WITH_DRAWING "Add Debug render support" ON) 3 | 4 | cmake_minimum_required(VERSION 3.21) 5 | 6 | set(VALIDATE OFF) 7 | 8 | if (VALIDATE) 9 | add_compile_definitions(VALIDATE) 10 | endif () 11 | 12 | # ---- Cache build vars ---- 13 | 14 | macro(set_from_environment VARIABLE) 15 | if (NOT DEFINED ${VARIABLE} AND DEFINED ENV{${VARIABLE}}) 16 | set(${VARIABLE} $ENV{${VARIABLE}}) 17 | endif () 18 | endmacro() 19 | 20 | # ---- Vcpkg check ---- 21 | set_from_environment(VCPKG_ROOT) 22 | if (NOT DEFINED VCPKG_ROOT) 23 | message( 24 | FATAL_ERROR 25 | "Variable VCPKG_ROOT is not set." 26 | ) 27 | endif () 28 | 29 | # ---- Include guards ---- 30 | if(PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR) 31 | message( 32 | FATAL_ERROR 33 | "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there." 34 | ) 35 | endif() 36 | 37 | set(Boost_USE_STATIC_RUNTIME OFF CACHE BOOL "") 38 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" CACHE STRING "") 39 | 40 | # ---- Project ---- 41 | 42 | project( 43 | NewProjectilesTMP 44 | VERSION 1.0.0 45 | LANGUAGES CXX 46 | ) 47 | 48 | configure_file( 49 | ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Version.h.in 50 | ${CMAKE_CURRENT_BINARY_DIR}/include/Version.h 51 | @ONLY 52 | ) 53 | 54 | configure_file( 55 | ${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.rc.in 56 | ${CMAKE_CURRENT_BINARY_DIR}/version.rc 57 | @ONLY 58 | ) 59 | 60 | # ---- Globals ---- 61 | 62 | if (MSVC) 63 | add_compile_definitions( 64 | _UNICODE 65 | ) 66 | 67 | if (NOT ${CMAKE_GENERATOR} STREQUAL "Ninja") 68 | add_compile_options( 69 | /MP # Build with Multiple Processes 70 | ) 71 | endif () 72 | endif () 73 | 74 | set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) 75 | set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_DEBUG OFF) 76 | 77 | set(Boost_USE_STATIC_LIBS ON) 78 | 79 | if (DEFINED UseUselessUtils AND UseUselessUtils) 80 | add_compile_definitions(USELESS_UTILS) 81 | endif () 82 | 83 | if (WITH_IMGUI) 84 | add_compile_definitions(WITH_IMGUI) 85 | endif () 86 | if (WITH_DRAWING) 87 | add_compile_definitions(WITH_DRAWING) 88 | endif () 89 | 90 | 91 | # ---- Dependencies ---- 92 | 93 | if (DEFINED UseUselessUtils AND UseUselessUtils) 94 | add_subdirectory($ENV{UselessFenixUtils} ./UselessFenixUtils/build) 95 | else () 96 | add_subdirectory($ENV{CommonLibSSE} ./CommonLibSSE/build) 97 | endif () 98 | 99 | find_package(spdlog REQUIRED CONFIG) 100 | find_package(jsoncpp REQUIRED CONFIG) 101 | 102 | if (WITH_IMGUI) 103 | find_package(imgui REQUIRED CONFIG) 104 | endif () 105 | 106 | # ---- Add source files ---- 107 | 108 | set(SOURCES 109 | src/main.cpp 110 | src/Hooks.h 111 | src/RuntimeData.h 112 | src/RuntimeData.cpp 113 | src/JsonUtils.h 114 | src/JsonUtils.cpp 115 | src/Homing.h 116 | src/Homing.cpp 117 | src/Triggers.h 118 | src/Triggers.cpp 119 | src/Multicast.h 120 | src/Multicast.cpp 121 | src/TriggerFunctions.h 122 | src/TriggerFunctions.cpp 123 | src/Emitters.h 124 | src/Emitters.cpp 125 | src/Followers.h 126 | src/Followers.cpp 127 | src/Positioning.h 128 | src/Positioning.cpp 129 | src/PCH.h 130 | ) 131 | 132 | source_group( 133 | TREE 134 | ${CMAKE_CURRENT_SOURCE_DIR} 135 | FILES 136 | ${SOURCES} 137 | ) 138 | 139 | source_group( 140 | TREE 141 | ${CMAKE_CURRENT_BINARY_DIR} 142 | FILES 143 | ${CMAKE_CURRENT_BINARY_DIR}/include/Version.h 144 | ) 145 | 146 | # ---- Create DLL ---- 147 | 148 | if (VALIDATE) 149 | add_library(pythonjsonvalid STATIC IMPORTED) # or STATIC instead of SHARED 150 | set_target_properties(pythonjsonvalid PROPERTIES 151 | IMPORTED_LOCATION "c:/Python3/libs/python310.lib" 152 | INTERFACE_INCLUDE_DIRECTORIES "c:/Python3/include" 153 | ) 154 | endif() 155 | 156 | add_library( 157 | ${PROJECT_NAME} 158 | SHARED 159 | ${SOURCES} 160 | ${CMAKE_CURRENT_BINARY_DIR}/include/Version.h 161 | ${CMAKE_CURRENT_BINARY_DIR}/version.rc 162 | .clang-format 163 | .editorconfig 164 | ) 165 | 166 | target_compile_features( 167 | ${PROJECT_NAME} 168 | PRIVATE 169 | cxx_std_20 170 | ) 171 | 172 | target_include_directories( 173 | ${PROJECT_NAME} 174 | PRIVATE 175 | ${CMAKE_CURRENT_BINARY_DIR}/include 176 | ${CMAKE_CURRENT_SOURCE_DIR}/src 177 | ) 178 | 179 | target_link_libraries( 180 | ${PROJECT_NAME} 181 | PRIVATE 182 | CommonLibSSE::CommonLibSSE 183 | spdlog::spdlog 184 | JsonCpp::JsonCpp 185 | ) 186 | 187 | if (WITH_IMGUI) 188 | target_link_libraries( 189 | ${PROJECT_NAME} 190 | PRIVATE 191 | imgui::imgui 192 | ) 193 | endif () 194 | 195 | if (VALIDATE) 196 | target_link_libraries( 197 | ${PROJECT_NAME} 198 | PRIVATE 199 | pythonjsonvalid 200 | ) 201 | endif () 202 | 203 | if (DEFINED UseUselessUtils AND UseUselessUtils) 204 | target_link_libraries( 205 | ${PROJECT_NAME} 206 | PRIVATE 207 | UselessFenixUtils::UselessFenixUtils 208 | ) 209 | endif () 210 | 211 | if (MSVC) 212 | target_compile_options( 213 | ${PROJECT_NAME} 214 | PRIVATE 215 | /std:c17 216 | 217 | /sdl # Enable Additional Security Checks 218 | /utf-8 # Set Source and Executable character sets to UTF-8 219 | /Zi # Debug Information Format 220 | 221 | /permissive- # Standards conformance 222 | 223 | /Zc:alignedNew # C++17 over-aligned allocation 224 | /Zc:auto # Deduce Variable Type 225 | /Zc:char8_t 226 | /Zc:__cplusplus # Enable updated __cplusplus macro 227 | /Zc:externC 228 | /Zc:externConstexpr # Enable extern constexpr variables 229 | /Zc:forScope # Force Conformance in for Loop Scope 230 | /Zc:hiddenFriend 231 | /Zc:implicitNoexcept # Implicit Exception Specifiers 232 | /Zc:lambda 233 | /Zc:noexceptTypes # C++17 noexcept rules 234 | /Zc:preprocessor # Enable preprocessor conformance mode 235 | /Zc:referenceBinding # Enforce reference binding rules 236 | /Zc:rvalueCast # Enforce type conversion rules 237 | /Zc:sizedDealloc # Enable Global Sized Deallocation Functions 238 | /Zc:strictStrings # Disable string literal type conversion 239 | /Zc:ternary # Enforce conditional operator rules 240 | /Zc:threadSafeInit # Thread-safe Local Static Initialization 241 | /Zc:tlsGuards 242 | /Zc:trigraphs # Trigraphs Substitution 243 | /Zc:wchar_t # wchar_t Is Native Type 244 | 245 | /external:anglebrackets 246 | /external:W0 247 | 248 | /W4 # Warning level 249 | /WX # Warning level (warnings are errors) 250 | 251 | "$<$:>" 252 | "$<$:/Zc:inline;/JMC-;/Ob3>" 253 | ) 254 | 255 | target_link_options( 256 | ${PROJECT_NAME} 257 | PRIVATE 258 | /WX # Treat Linker Warnings as Errors 259 | 260 | "$<$:/INCREMENTAL;/OPT:NOREF;/OPT:NOICF>" 261 | "$<$:/INCREMENTAL:NO;/OPT:REF;/OPT:ICF;/DEBUG:FULL>" 262 | ) 263 | endif () 264 | 265 | target_precompile_headers( 266 | ${PROJECT_NAME} 267 | PRIVATE 268 | src/PCH.h 269 | ) 270 | 271 | add_custom_command( 272 | TARGET ${PROJECT_NAME} POST_BUILD 273 | COMMAND ${CMAKE_COMMAND} -E copy_if_different $ "e:/MO2_Data/mods/NewProjectilesTMP/skse/plugins/" 274 | COMMAND ${CMAKE_COMMAND} -E copy_if_different $ "e:/MO2_Data/mods/NewProjectilesTMP/skse/plugins/" 275 | ) 276 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurePresets": [ 3 | { 4 | "binaryDir": "${sourceDir}/build", 5 | "cacheVariables": { 6 | "CMAKE_BUILD_TYPE": { 7 | "type": "STRING", 8 | "value": "Release" 9 | } 10 | }, 11 | "errors": { 12 | "deprecated": true 13 | }, 14 | "hidden": true, 15 | "name": "cmake-dev", 16 | "warnings": { 17 | "deprecated": true, 18 | "dev": true 19 | } 20 | }, 21 | { 22 | "cacheVariables": { 23 | "CMAKE_TOOLCHAIN_FILE": { 24 | "type": "STRING", 25 | "value": "$env{VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake" 26 | }, 27 | "VCPKG_OVERLAY_PORTS": { 28 | "type": "STRING", 29 | "value": "${sourceDir}/cmake/ports/" 30 | } 31 | }, 32 | "hidden": true, 33 | "name": "vcpkg" 34 | }, 35 | { 36 | "cacheVariables": { 37 | "CMAKE_MSVC_RUNTIME_LIBRARY": { 38 | "type": "STRING", 39 | "value": "MultiThreaded$<$:Debug>" 40 | }, 41 | "VCPKG_TARGET_TRIPLET": { 42 | "type": "STRING", 43 | "value": "x64-windows-static" 44 | } 45 | }, 46 | "hidden": true, 47 | "name": "windows" 48 | }, 49 | { 50 | "cacheVariables": { 51 | "UseUselessUtils": { 52 | "type": "BOOL", 53 | "value": "TRUE" 54 | } 55 | }, 56 | "hidden": true, 57 | "name": "utils" 58 | }, 59 | { 60 | "binaryDir": "${sourceDir}/buildvr", 61 | "cacheVariables": { 62 | "BUILD_SKYRIMVR": true 63 | }, 64 | "hidden": true, 65 | "name": "vr" 66 | }, 67 | { 68 | "cacheVariables": { 69 | "CMAKE_CXX_FLAGS": "/EHsc /MP /W4 /external:anglebrackets /external:W0 $penv{CXXFLAGS}" 70 | }, 71 | "generator": "Visual Studio 16 2019", 72 | "inherits": [ 73 | "cmake-dev", 74 | "vcpkg", 75 | "windows" 76 | ], 77 | "name": "vs2019-windows-vcpkg" 78 | }, 79 | { 80 | "cacheVariables": { 81 | "CMAKE_CXX_FLAGS": "/EHsc /MP /W4 /WX $penv{CXXFLAGS}" 82 | }, 83 | "generator": "Visual Studio 17 2022", 84 | "inherits": [ 85 | "cmake-dev", 86 | "vcpkg", 87 | "windows" 88 | ], 89 | "name": "vs2022-windows-vcpkg", 90 | "toolset": "v143" 91 | }, 92 | { 93 | "cacheVariables": { 94 | "CMAKE_CXX_FLAGS": "/EHsc /MP /W4 /external:anglebrackets /external:W0 $penv{CXXFLAGS}" 95 | }, 96 | "generator": "Visual Studio 16 2019", 97 | "inherits": [ 98 | "vr", 99 | "cmake-dev", 100 | "vcpkg", 101 | "windows" 102 | ], 103 | "name": "vs2019-windows-vcpkg-vr" 104 | }, { 105 | "cacheVariables": { 106 | "CMAKE_CXX_FLAGS": "/EHsc /MP /W4 /external:anglebrackets /external:W0 $penv{CXXFLAGS}" 107 | }, 108 | "generator": "Visual Studio 17 2022", 109 | "inherits": [ 110 | "vr", 111 | "cmake-dev", 112 | "vcpkg", 113 | "windows" 114 | ], 115 | "name": "vs2022-windows-vcpkg-vr", 116 | "toolset": "v143" 117 | }, 118 | { 119 | "inherits": [ "vs2022-windows-vcpkg" ], 120 | "name": "default" 121 | }, 122 | { 123 | "inherits": [ "vs2022-windows-vcpkg", "utils" ], 124 | "name": "default-utils" 125 | } 126 | ], 127 | "version": 2 128 | } 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All rights reserved. Code is visible only for personal educational use. Porting to other game versions is prohibited. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NewProjectilesAPI 2 | 3 | This mod offers many new possibilities for projectiles, including homing, the ability to shoot and launch multiple projectiles simultaneously, ability to follow that caster, as well as the ability to perform an action during the flight of the projectile. 4 | 5 | This mod is a framework, by itself does not add anything to the game. The authors of magic/combat mods will be able to use it to improve their mods with new cool projectiles. 6 | 7 | In the json file you specify the properties of the new projectiles, see the [wiki](https://github.com/fenix31415/NewProjectilesTMP/wiki) for more information. There are also [examples](https://github.com/fenix31415/NewProjectilesTMP/tree/master/examples) of settings files. 8 | 9 | ## Installation 10 | 11 | * Go to [releases](https://github.com/fenix31415/NewProjectilesTMP/releases), pick the latest one and download `NewProjectilesTMP.zip`. 12 | * Install this archive as regular mod. 13 | * If you are mod author, read the [wiki](https://github.com/fenix31415/NewProjectilesTMP/wiki) and try [examples](https://github.com/fenix31415/NewProjectilesTMP/tree/master/examples). 14 | 15 | ## Features 16 | 17 | In a nutshell, the mod does the following. When some events are triggered, under certain conditions, you can launch the projectile(s), change the type of projectile. 18 | 19 | Here are some features of this mod: 20 | 21 | ### Homing projectiles 22 | 23 | Projectiles can now chase their target, making each shot more accurate and deadly. 24 | 25 | ![HomingMulticast](gifs/HomingMulticast.gif) 26 | ![FireballFromSkyOnAttack](gifs/FireballFromSkyOnAttack.gif) 27 | ![FireballFromSkyOnShot](gifs/FireballFromSkyOnShot.gif) 28 | 29 | ### Multiple shot 30 | 31 | You can fire multiple projectiles at the same time, creating a flurry of arrows or magic charges. 32 | 33 | ![Armageddon](gifs/Armageddon.gif) 34 | ![SwordLightning](gifs/SwordLightning.gif) 35 | ![Lightnings](gifs/Lightnings.gif) 36 | ![FillCircle_FillHalfCircle](gifs/FillCircle_FillHalfCircle.gif) 37 | 38 | ### Followers 39 | 40 | This new type of projectile will follow you, creating unique tactical opportunities. 41 | 42 | ![HalfSphere](gifs/HalfSphere.gif) 43 | ![FollowerFinal1](gifs/FollowerFinal1.gif) 44 | ![FollowersCast](gifs/FollowersCast.gif) 45 | 46 | ### Emitter 47 | 48 | You can configure the projectiles so that they perform certain actions during flight, for example, change their speed or direction, cast other projectile. 49 | 50 | ![FireballDown](gifs/FireballDown.gif) 51 | ![DelayedCast](gifs/DelayedCast.gif) 52 | 53 | The main advantage is it is possibly to mix all those features! 54 | 55 | ![Avatar](gifs/Avatar.gif) 56 | ![Avatar1](gifs/Avatar1.gif) 57 | ![Accomulate](gifs/Accomulate.gif) 58 | ![FireballCannon](gifs/FireballCannon.gif) 59 | ![GuardIce1](gifs/GuardIce1.gif) 60 | ![GuardAttackerLightning](gifs/GuardAttackerLightning.gif) 61 | 62 | Thanks to flexible settings, you can create new projectiles by combining these functions, which opens up endless possibilities for customizing your mod. 63 | 64 | ## Plans 65 | 66 | if you have an **idea** of some necessary for you function or event, or just an idea to improve the mod, feel free to **share** it! 67 | 68 | There is a list of my ideas, which are not implemented yet. And my thoughts about them. 69 | 70 | * Checking conditions **while flight**. E.g. make some feature active only when certain condition is pass. But I cannot imagine any cool use-case for it. 71 | * Allow follower projectiles float **around a cylinder** (currently implemented sphere and plane). Ahh, I cannot create math formula for smooth trajectory. 72 | * Make Homing target **selection priority**: create ways to increase/decrease it. But I cannot imagine any cool use-case for it. 73 | * Upgrade `AccelerateToMaxSpeed`: currently it have **poor acceleration formula**. I used it to catch enemies' projectiles and throw them back. How it can be used? 74 | * I made an attempt to create an **editor** for it. But unfortunately it took too many time, so I stopped. There is also json schema-based editor generators. Unfortunately all ones I found support draft-04. The schema is draft-12. Maybe I'll accumulate time and power to make another attempt.\ 75 | Anyway, I created a schema, in VS Code you just need to press Ctrl+Space to create your json. VS Code validates it for you, show errors & tooltips that I wrote. 76 | 77 | ## Bugs 78 | 79 | If you found a bug in docs/in schema/in the mod -- feel free to share. 80 | 81 | Here some known issues. 82 | 83 | * When Figure's normal looks straight up, the shape **do not rotates** with the caster/proj/whatever. For now, if you want rotation, use `"normal": [0,0.01,1]`. 84 | * If you create more followers than the number of points in the Figure, the remaining followers **have a zero index**. So, all of them will follow (or fly around) **single point** (which have 0 index in the Shape).\ 85 | I do not know how to fix it. At the same time it is possible to have multiple groups of followers, with different shaped. I can iterate every follower, but I cannot get the figure by the follower. So it is impossible for me to get the "smallest the most free index of the follower's shape" by a follower.\ 86 | A possible solution is to **iterate all** of them, determine a figure, get the smallest index. But I want to avoid iterating every creation. 87 | As an optimization, it could be done for multicast of followers. But I do not think it is worth it. 88 | 89 | ## Changelog 90 | 91 | ### New in 1.2.0 92 | 93 | ![Impact](gifs/Impact.gif) 94 | ![AnimEvent](gifs/AnimEvent.gif) 95 | ![Explode](gifs/Explode.gif) 96 | ![ThroughWalls](gifs/ThroughWalls.gif) 97 | 98 | #### General 99 | 100 | * Fixed CTD with loading **permanent runes**. 101 | * **Optimized** GetCollisionLayer hooks. 102 | * `conditions` field in `Triggers` is **optional**. 103 | 104 | #### Followers 105 | 106 | * Fixed a bug with **smooth** Follower rotating. ~~again~~. 107 | 108 | #### Multicast 109 | 110 | * Added **initial rotation** [To target](Multicast#spawn-group-rotation) (defined by the Homing feature). 111 | * Sound position **fix**. ~~again~~. 112 | 113 | #### Homing 114 | 115 | * Flame projectiles (e.g. flames, frost spells) **disable aim** if target is dead or too far away. 116 | 117 | #### Jsons 118 | 119 | * Added **[multiple](important-notes) json** support. You can use your own file. 120 | * Added **json validation** with schema on load. If there is any invalid json, load/reload cancelled. I still insist that you use schema validation in VS Code too. 121 | 122 | #### Settings 123 | 124 | * Added **settings**. 125 | * Added option for **reload json hotkey**. Supports combinations with shift, ctrl, alt. Default: `Ctrl+Shift+Q`. 126 | 127 | #### New Events 128 | 129 | * `ProjDestroyed`. After hit/end of lifetime/end of range. 130 | * `ProjImpact`. A projectile hits someone or something. 131 | * `EffectEnd`. Actor get dispelled an effect. 132 | 133 | #### New Conditions 134 | 135 | * `EffectHasKwd` now works with `ProjAppeared` event. 136 | 137 | #### New Trigger Functions 138 | 139 | * `Placeatme`: spawn something near to trigger. 140 | * `SendAnimEvent`: notify behavior graph of trigger. 141 | * `Explode` at trigger position. 142 | * `ChangeSpeed` is working in triggers, also simplified code regarding it. 143 | * `SetColLayer`: change collision type of a projectile. E.g. fly through walls. 144 | * `ChangeRange` works for flame and beam projectiles. 145 | -------------------------------------------------------------------------------- /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/ports/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/cmake/ports/.keep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/HomieSpells_Avatar.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_spellFireball": "Skyrim.esm|0x1C789", 4 | "key_spellIceSpike": "Skyrim.esm|0x2B96C", 5 | "key_spellIceSpear": "Skyrim.esm|0x10F7EC", 6 | "key_spellLightning": "Skyrim.esm|0x45F9D", 7 | "key_spellFlames": "Skyrim.esm|0x12FCD", 8 | "key_spellEmpty": "Skyrim.esm|0xE40CF", 9 | 10 | "key_projFireball": "Skyrim.esm|0x10FBED", 11 | "key_projIceSpike": "Skyrim.esm|0x2F774", 12 | "key_projLightning": "Skyrim.esm|0xA9D92", 13 | "key_projFlames": "Skyrim.esm|0x12FCF", 14 | "key_projFirebolt": "Skyrim.esm|0x12E84", 15 | "key_projLowlight": "Skyrim.esm|0x58E9C", 16 | "key_projIceSpear": "Skyrim.esm|0x10FBEE", 17 | "key_projEmpty": "Skyrim.esm|0x6F793", 18 | 19 | "key_projArrowIron": "Skyrim.esm|0x3BE11", 20 | "key_projArrowSteel": "Skyrim.esm|0x3BE12", 21 | "key_projArrowElven": "Skyrim.esm|0x3BE15", 22 | "key_projArrowGlass": "Skyrim.esm|0x3BE16", 23 | "key_projArrowEbony": "Skyrim.esm|0x3BE17", 24 | "key_projArrowDaedr": "Skyrim.esm|0x3BE18", 25 | 26 | "key_projArrowOrish": "Skyrim.esm|0x3BE13", 27 | "key_projArrowDwarv": "Skyrim.esm|0x3BE14", 28 | "key_projArrowFalmer": "Skyrim.esm|0x3BE19", 29 | "key_projArrowForsw": "Skyrim.esm|0xCEEA1", 30 | 31 | "key_arrowIron": "Skyrim.esm|0x1397D", 32 | "key_arrowSteel": "Skyrim.esm|0x1397F", 33 | 34 | "key_kywdMagicDamageFire": "Skyrim.esm|0x1CEAD", 35 | "key_kywdMagicDamageFrost": "Skyrim.esm|0x1CEAE", 36 | "key_kywdMagicRestoreHealth": "Skyrim.esm|0x1CEB0" 37 | }, 38 | 39 | "HomingData": { 40 | "key_home": { 41 | "type": "ConstSpeed", 42 | "rotationTime": 1.5, 43 | "aggressive": "Any" 44 | } 45 | }, 46 | 47 | "MulticastSpawnGroups": { 48 | "key_SP_fire": { 49 | "Pattern": { 50 | "Figure": { 51 | "shape": "Circle", 52 | "size": 100, 53 | "count": 10 54 | }, 55 | "normal": [0, 0, 1], 56 | "xDepends": false 57 | }, 58 | "sound": "Single" 59 | }, 60 | "key_SP_ice": { 61 | "Pattern": { 62 | "Figure": { 63 | "shape": "Circle", 64 | "size": 100, 65 | "count": 10 66 | }, 67 | "posOffset": [0, 0, -50], 68 | "normal": [1, 0, 1], 69 | "xDepends": false 70 | }, 71 | "sound": "Single" 72 | }, 73 | "key_SP_earth": { 74 | "Pattern": { 75 | "Figure": { 76 | "shape": "Circle", 77 | "size": 100, 78 | "count": 10 79 | }, 80 | "posOffset": [0, 0, -50], 81 | "normal": [-1, 0, 1], 82 | "xDepends": false 83 | }, 84 | "sound": "Single" 85 | }, 86 | "key_SP_scatter": { 87 | "Pattern": { 88 | "Figure": { 89 | "shape": "Single", 90 | "count": 10 91 | }, 92 | "posOffset": [0, 0, -50], 93 | "normal": [-1, 0, 1], 94 | "xDepends": false 95 | }, 96 | "sound": "Single", 97 | "rotRnd": [5, 5] 98 | } 99 | }, 100 | "MulticastData": { 101 | "key_multicast_fire": [ 102 | { 103 | "spellID": "Current", 104 | "spawn_group": "key_SP_fire", 105 | "TriggerFunctions": { 106 | "functions": [ 107 | { 108 | "type": "SetFollower", 109 | "id": "key_follower_fire" 110 | }, 111 | { 112 | "type": "ChangeRange", 113 | "data": { 114 | "type": "Mul", 115 | "value": 10 116 | } 117 | } 118 | ] 119 | } 120 | } 121 | ], 122 | "key_multicast_ice": [ 123 | { 124 | "spellID": "Current", 125 | "spawn_group": "key_SP_ice", 126 | "TriggerFunctions": { 127 | "functions": [ 128 | { 129 | "type": "SetFollower", 130 | "id": "key_follower_ice" 131 | }, 132 | { 133 | "type": "ChangeRange", 134 | "data": { 135 | "type": "Mul", 136 | "value": 10 137 | } 138 | } 139 | ] 140 | } 141 | } 142 | ], 143 | "key_multicast_earth": [ 144 | { 145 | "spellID": "Current", 146 | "spawn_group": "key_SP_earth", 147 | "TriggerFunctions": { 148 | "functions": [ 149 | { 150 | "type": "SetFollower", 151 | "id": "key_follower_earth" 152 | }, 153 | { 154 | "type": "ChangeRange", 155 | "data": { 156 | "type": "Mul", 157 | "value": 10 158 | } 159 | } 160 | ] 161 | } 162 | } 163 | ], 164 | "key_multicast_fireballs": [ 165 | { 166 | "spellID": "Current", 167 | "spawn_group": "key_SP_scatter", 168 | "TriggerFunctions": { 169 | "functions": [ 170 | { 171 | "type": "SetHoming", 172 | "id": "key_home" 173 | } 174 | ] 175 | } 176 | } 177 | ] 178 | }, 179 | 180 | "FollowersData": { 181 | "key_follower_fire": { 182 | "Pattern": { 183 | "Figure": { 184 | "shape": "Single" 185 | }, 186 | "normal": [0, 0, 1], 187 | "xDepends": false, 188 | "posOffset": [0, 0, 100] 189 | }, 190 | "rounding": "Plane", 191 | "roundingR": 120 192 | }, 193 | "key_follower_ice": { 194 | "Pattern": { 195 | "Figure": { 196 | "shape": "Single" 197 | }, 198 | "normal": [1, 0, 1], 199 | "xDepends": false, 200 | "posOffset": [0, 0, 100] 201 | }, 202 | "rounding": "Plane", 203 | "roundingR": 100 204 | }, 205 | "key_follower_earth": { 206 | "Pattern": { 207 | "Figure": { 208 | "shape": "Single" 209 | }, 210 | "normal": [-1, 0, 1], 211 | "xDepends": false, 212 | "posOffset": [0, 0, 100] 213 | }, 214 | "rounding": "Plane", 215 | "roundingR": 100 216 | } 217 | }, 218 | 219 | "Triggers": [ 220 | { 221 | "event": "ProjAppeared", 222 | "conditions": { 223 | "ProjBaseIsFormID": "key_projFirebolt" 224 | }, 225 | "TriggerFunctions": { 226 | "functions": [ 227 | { 228 | "type": "ApplyMultiCast", 229 | "id": "key_multicast_fire" 230 | } 231 | ], 232 | "disableOrigin": true 233 | } 234 | }, 235 | { 236 | "event": "ProjAppeared", 237 | "conditions": { 238 | "ProjBaseIsFormID": "key_projIceSpike" 239 | }, 240 | "TriggerFunctions": { 241 | "functions": [ 242 | { 243 | "type": "ApplyMultiCast", 244 | "id": "key_multicast_ice" 245 | } 246 | ], 247 | "disableOrigin": true 248 | } 249 | }, 250 | { 251 | "event": "ProjAppeared", 252 | "conditions": { 253 | "ProjBaseIsFormID": "Elemental Destruction Magic Redux.esp|0x9BF" 254 | }, 255 | "TriggerFunctions": { 256 | "functions": [ 257 | { 258 | "type": "ApplyMultiCast", 259 | "id": "key_multicast_earth" 260 | } 261 | ], 262 | "disableOrigin": true 263 | } 264 | }, 265 | { 266 | "event": "ProjAppeared", 267 | "conditions": { 268 | "ProjBaseIsFormID": "key_projFireball" 269 | }, 270 | "TriggerFunctions": { 271 | "functions": [ 272 | { 273 | "type": "ApplyMultiCast", 274 | "id": "key_multicast_fireballs" 275 | } 276 | ], 277 | "disableOrigin": true 278 | } 279 | } 280 | ] 281 | } 282 | -------------------------------------------------------------------------------- /examples/HomieSpells_Emitter.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_projFireball": "Skyrim.esm|0x10FBED" 4 | }, 5 | 6 | "MulticastSpawnGroups": { 7 | "key_SG_1": { 8 | "Pattern": { 9 | "Figure": { 10 | "shape": "Single", 11 | "count": 1 12 | }, 13 | "normal": [0,1,0], 14 | "xDepends": false 15 | }, 16 | "rotOffset": [90,0], 17 | "rotRnd": [10,10] 18 | } 19 | }, 20 | 21 | "MulticastData": { 22 | "key_MC_1": [ 23 | { 24 | "spellID": "Current", 25 | "spawn_group": "key_SG_1" 26 | } 27 | ] 28 | }, 29 | 30 | "EmittersData": { 31 | "key_EM_1": { 32 | "interval": 0.2, 33 | "functions": [ 34 | { 35 | "type": "TriggerFunctions", 36 | "TriggerFunctions": { 37 | "functions": [ 38 | { 39 | "type": "ApplyMultiCast", 40 | "id": "key_MC_1" 41 | } 42 | ] 43 | } 44 | } 45 | ] 46 | } 47 | }, 48 | 49 | "Triggers": [ 50 | { 51 | "event": "ProjAppeared", 52 | "conditions": { 53 | "ProjBaseIsFormID": "key_projFireball" 54 | }, 55 | "TriggerFunctions": { 56 | "functions": [ 57 | { 58 | "type": "SetEmitter", 59 | "id": "key_EM_1" 60 | } 61 | ] 62 | } 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /examples/HomieSpells_Emitter_Adv.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_projFireball": "Skyrim.esm|0x10FBED" 4 | }, 5 | 6 | "FollowersData": { 7 | "key_F_1": { 8 | "Pattern": { 9 | "Figure": { 10 | "shape": "Line", 11 | "count": 10, 12 | "size": 400 13 | }, 14 | "planeRotate": 0, 15 | "normal": [0, 1, 0], 16 | "xDepends": false, 17 | "posOffset": [0, 0, 300] 18 | } 19 | } 20 | }, 21 | 22 | "MulticastSpawnGroups": { 23 | "key_SG_1": { 24 | "Pattern": { 25 | "Figure": { 26 | "shape": "Single", 27 | "count": 1 28 | } 29 | } 30 | } 31 | }, 32 | 33 | "MulticastData": { 34 | "key_MC_1": [ 35 | { 36 | "spellID": "Current", 37 | "spawn_group": "key_SG_1" 38 | } 39 | ] 40 | }, 41 | 42 | "EmittersData": { 43 | "key_EM_1": { 44 | "interval": 1, 45 | "functions": [ 46 | { 47 | "type": "TriggerFunctions", 48 | "TriggerFunctions": { 49 | "functions": [ 50 | { 51 | "type": "ApplyMultiCast", 52 | "id": "key_MC_1" 53 | } 54 | ] 55 | } 56 | } 57 | ] 58 | } 59 | }, 60 | 61 | "Triggers": [ 62 | { 63 | "event": "ProjAppeared", 64 | "conditions": { 65 | "ProjBaseIsFormID": "key_projFireball" 66 | }, 67 | "TriggerFunctions": { 68 | "functions": [ 69 | { 70 | "type": "SetFollower", 71 | "id": "key_F_1" 72 | }, 73 | { 74 | "type": "SetEmitter", 75 | "id": "key_EM_1" 76 | } 77 | ] 78 | } 79 | } 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /examples/HomieSpells_Events_Swing.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_spellLightning": "Skyrim.esm|0x45F9D" 4 | }, 5 | 6 | "MulticastSpawnGroups": { 7 | "key_SG_1": { 8 | "Pattern": { 9 | "Figure": { 10 | "shape": "Single", 11 | "count": 1 12 | } 13 | } 14 | } 15 | }, 16 | 17 | "MulticastData": { 18 | "key_MC_1": [ 19 | { 20 | "spellID": "key_spellLightning", 21 | "spawn_group": "key_SG_1" 22 | } 23 | ] 24 | }, 25 | 26 | "Triggers": [ 27 | { 28 | "event": "Swing", 29 | "conditions": {}, 30 | "TriggerFunctions": { 31 | "functions": [ 32 | { 33 | "type": "ApplyMultiCast", 34 | "id": "key_MC_1" 35 | } 36 | ] 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /examples/HomieSpells_Firework.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_spellFireball": "Skyrim.esm|0x1C789", 4 | "key_spellLightning": "Skyrim.esm|0x45F9D", 5 | "key_spellIceSpear": "Skyrim.esm|0x10F7EC", 6 | 7 | "key_projIceSpike": "Skyrim.esm|0x2F774", 8 | "key_projIceSpear": "Skyrim.esm|0x10FBEE", 9 | "key_projFireball": "Skyrim.esm|0x10FBED" 10 | }, 11 | 12 | "HomingData": { 13 | "key_H_R": { 14 | "type": "ConstSpeed", 15 | "rotationTime": 1 16 | } 17 | }, 18 | 19 | "MulticastSpawnGroups": { 20 | "key_SP_sphere": { 21 | "rotation": "FromCenter", 22 | "Pattern": { 23 | "Figure": { 24 | "count": 70, 25 | "shape": "Sphere", 26 | "size": 10 27 | } 28 | } 29 | }, 30 | "key_SP_fire": { 31 | "rotOffset": [-90,0], 32 | "rotRnd": [20,20], 33 | "Pattern": { 34 | "Figure": { 35 | "shape": "Single" 36 | }, 37 | "normal": [0,0,1], 38 | "xDepends": true 39 | } 40 | } 41 | }, 42 | 43 | "MulticastData": { 44 | "key_MC_explosion1": [ 45 | { 46 | "spellID": "key_spellFireball", 47 | "spawn_group": "key_SP_sphere", 48 | "TriggerFunctions": { 49 | "functions": [ 50 | { 51 | "type": "ChangeRange", 52 | "data": { 53 | "type": "Mul", 54 | "value": 0.5 55 | } 56 | }, 57 | { 58 | "type": "ChangeSpeed", 59 | "data": { 60 | "type": "Mul", 61 | "value": 0.5 62 | } 63 | } 64 | ] 65 | } 66 | } 67 | ], 68 | "key_MC_explosion2": [ 69 | { 70 | "spellID": "key_spellLightning", 71 | "spawn_group": "key_SP_sphere", 72 | "TriggerFunctions": { 73 | "functions": [ 74 | { 75 | "type": "ChangeRange", 76 | "data": { 77 | "type": "Mul", 78 | "value": 0.5 79 | } 80 | }, 81 | { 82 | "type": "ChangeSpeed", 83 | "data": { 84 | "type": "Mul", 85 | "value": 0.5 86 | } 87 | } 88 | ] 89 | } 90 | } 91 | ], 92 | "key_MC_fire1": [ 93 | { 94 | "spellID": "key_spellIceSpear", 95 | "spawn_group": "key_SP_fire", 96 | "TriggerFunctions": { 97 | "functions": [ 98 | { 99 | "type": "SetEmitter", 100 | "id": "key_EM_rocket1" 101 | } 102 | ] 103 | } 104 | } 105 | ], 106 | "key_MC_fire2": [ 107 | { 108 | "spellID": "key_spellIceSpear", 109 | "spawn_group": "key_SP_fire", 110 | "TriggerFunctions": { 111 | "functions": [ 112 | { 113 | "type": "SetEmitter", 114 | "id": "key_EM_rocket2" 115 | } 116 | ] 117 | } 118 | } 119 | ] 120 | }, 121 | 122 | "EmittersData": { 123 | "key_EM_rocket1": { 124 | "destroyAfter": true, 125 | "interval": 0.5, 126 | "limited": true, 127 | "functions": [ 128 | { 129 | "type": "TriggerFunctions", 130 | "TriggerFunctions": { 131 | "functions": [ 132 | { 133 | "type": "ApplyMultiCast", 134 | "id": "key_MC_explosion1" 135 | } 136 | ] 137 | } 138 | } 139 | ] 140 | }, 141 | "key_EM_rocket2": { 142 | "destroyAfter": true, 143 | "interval": 0.5, 144 | "limited": true, 145 | "functions": [ 146 | { 147 | "type": "TriggerFunctions", 148 | "TriggerFunctions": { 149 | "functions": [ 150 | { 151 | "type": "ApplyMultiCast", 152 | "id": "key_MC_explosion2" 153 | } 154 | ] 155 | } 156 | } 157 | ] 158 | }, 159 | "key_EM_firework1": { 160 | "count": 5, 161 | "destroyAfter": true, 162 | "interval": 1, 163 | "limited": true, 164 | "functions": [ 165 | { 166 | "type": "TriggerFunctions", 167 | "TriggerFunctions": { 168 | "functions": [ 169 | { 170 | "type": "ApplyMultiCast", 171 | "id": "key_MC_fire1" 172 | } 173 | ] 174 | } 175 | } 176 | ] 177 | }, 178 | "key_EM_firework2": { 179 | "count": 5, 180 | "destroyAfter": true, 181 | "interval": 1, 182 | "limited": true, 183 | "functions": [ 184 | { 185 | "type": "TriggerFunctions", 186 | "TriggerFunctions": { 187 | "functions": [ 188 | { 189 | "type": "ApplyMultiCast", 190 | "id": "key_MC_fire2" 191 | } 192 | ] 193 | } 194 | } 195 | ] 196 | } 197 | }, 198 | 199 | "FollowersData": { 200 | "key_F_firework": { 201 | "collision": "None", 202 | "Pattern": { 203 | "Figure": { 204 | "count": 5, 205 | "shape": "Line", 206 | "size": 1000 207 | }, 208 | "normal": [0,0,1], 209 | "planeRotate": 90, 210 | "xDepends": false, 211 | "posOffset": [0,4000,100] 212 | }, 213 | "speed": 0 214 | } 215 | }, 216 | 217 | "Triggers": [ 218 | { 219 | "event": "ProjAppeared", 220 | "conditions": { "ProjBaseIsFormID": "key_projIceSpear" }, 221 | "TriggerFunctions": { 222 | "functions": [ 223 | { 224 | "type": "SetEmitter", 225 | "id": "key_EM_firework1" 226 | }, 227 | { 228 | "type": "SetFollower", 229 | "id": "key_F_firework" 230 | }, 231 | { 232 | "type": "ChangeRange", 233 | "data": { 234 | "type": "Mul", 235 | "value": 10 236 | } 237 | } 238 | ] 239 | } 240 | }, 241 | { 242 | "event": "ProjAppeared", 243 | "conditions": { "ProjBaseIsFormID": "key_projIceSpike" }, 244 | "TriggerFunctions": { 245 | "functions": [ 246 | { 247 | "type": "SetEmitter", 248 | "id": "key_EM_firework2" 249 | }, 250 | { 251 | "type": "SetFollower", 252 | "id": "key_F_firework" 253 | }, 254 | { 255 | "type": "ChangeRange", 256 | "data": { 257 | "type": "Mul", 258 | "value": 10 259 | } 260 | } 261 | ] 262 | } 263 | } 264 | ] 265 | } 266 | -------------------------------------------------------------------------------- /examples/HomieSpells_Follower.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_projFireball": "Skyrim.esm|0x10FBED" 4 | }, 5 | 6 | "FollowersData": { 7 | "key_F_1": { 8 | "Pattern": { 9 | "Figure": { 10 | "shape": "Circle", 11 | "count": 10, 12 | "size": 100 13 | }, 14 | "normal": [0, 0, 1], 15 | "xDepends": false, 16 | "posOffset": [0, 0, 100] 17 | }, 18 | "speed": 0 19 | } 20 | }, 21 | 22 | "Triggers": [ 23 | { 24 | "event": "ProjAppeared", 25 | "conditions": { 26 | "ProjBaseIsFormID": "key_projFireball" 27 | }, 28 | "TriggerFunctions": { 29 | "functions": [ 30 | { 31 | "type": "SetFollower", 32 | "id": "key_F_1" 33 | } 34 | ] 35 | } 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /examples/HomieSpells_Follower_Adv.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_projFireball": "Skyrim.esm|0x10FBED" 4 | }, 5 | 6 | "FollowersData": { 7 | "key_F_1": { 8 | "Pattern": { 9 | "Figure": { 10 | "shape": "Single" 11 | }, 12 | "normal": [0, 1, 0], 13 | "xDepends": false, 14 | "posOffset": [0, 0, 200], 15 | "origin": "NPC Spine [Spn0]" 16 | }, 17 | "rounding": "Sphere", 18 | "roundingR": 300 19 | } 20 | }, 21 | 22 | "MulticastSpawnGroups": { 23 | "key_SG_1": { 24 | "Pattern": { 25 | "Figure": { 26 | "shape": "Sphere", 27 | "count": 30, 28 | "size": 300 29 | }, 30 | "normal": [0, 1, 0], 31 | "xDepends": false, 32 | "posOffset": [0, 0, 200], 33 | "origin": "NPC Spine [Spn0]" 34 | }, 35 | "rotRnd": [50,50] 36 | } 37 | }, 38 | 39 | "MulticastData": { 40 | "key_MC_1": [ 41 | { 42 | "spawn_group": "key_SG_1", 43 | "spellID": "Current", 44 | "TriggerFunctions": { 45 | "functions": [ 46 | { 47 | "type": "SetFollower", 48 | "id": "key_F_1" 49 | } 50 | ] 51 | } 52 | } 53 | ] 54 | }, 55 | 56 | "Triggers": [ 57 | { 58 | "event": "ProjAppeared", 59 | "conditions": { 60 | "ProjBaseIsFormID": "key_projFireball" 61 | }, 62 | "TriggerFunctions": { 63 | "disableOrigin": true, 64 | "functions": [ 65 | { 66 | "type": "ApplyMultiCast", 67 | "id": "key_MC_1" 68 | } 69 | ] 70 | } 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /examples/HomieSpells_Follower_Guards.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_projFirebolt": "Skyrim.esm|0x12E84" 4 | }, 5 | 6 | "HomingData": { 7 | "key_H_1": { 8 | "type": "ConstSpeed", 9 | "rotationTime": 1, 10 | "aggressive": "Any" 11 | } 12 | }, 13 | 14 | "FollowersData": { 15 | "key_F_1": { 16 | "Pattern": { 17 | "Figure": { 18 | "shape": "HalfCircle", 19 | "size": 100, 20 | "count": 10 21 | }, 22 | "normal": [0, 1, 0], 23 | "xDepends": false, 24 | "posOffset": [0, -50, 0], 25 | "origin": "NPC Spine [Spn0]" 26 | }, 27 | "collision": "None" 28 | } 29 | }, 30 | 31 | "Triggers": [ 32 | { 33 | "event": "ProjAppeared", 34 | "conditions": { 35 | "ProjBaseIsFormID": "key_projFirebolt" 36 | }, 37 | "TriggerFunctions": { 38 | "functions": [ 39 | { 40 | "type": "SetFollower", 41 | "id": "key_F_1" 42 | }, 43 | { 44 | "type": "ChangeSpeed", 45 | "data": { 46 | "type": "Mul", 47 | "value": 0.1 48 | } 49 | } 50 | ] 51 | } 52 | }, 53 | { 54 | "event": "HitByProjectile", 55 | "conditions": { 56 | "CasterIsFormID": "0x14" 57 | }, 58 | "TriggerFunctions": { 59 | "functions": [ 60 | { 61 | "type": "SetRotationHoming", 62 | "id": "key_H_1", 63 | "on_followers": true 64 | }, 65 | { 66 | "type": "DisableFollower", 67 | "on_followers": true 68 | } 69 | ] 70 | } 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /examples/HomieSpells_Follower_Guards_Attack.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_projFireball": "Skyrim.esm|0x10FBED", 4 | "key_projFirebolt": "Skyrim.esm|0x12E84" 5 | }, 6 | 7 | "FollowersData": { 8 | "key_F_1": { 9 | "Pattern": { 10 | "Figure": { 11 | "shape": "FillCircle", 12 | "size": 200, 13 | "count": 20 14 | }, 15 | "normal": [0, 1, 0], 16 | "xDepends": false, 17 | "posOffset": [0, -100, 100], 18 | "origin": "NPC Spine [Spn0]" 19 | }, 20 | "collision": "None" 21 | } 22 | }, 23 | 24 | "Triggers": [ 25 | { 26 | "event": "ProjAppeared", 27 | "conditions": { 28 | "ProjBaseIsFormID": "key_projFirebolt" 29 | }, 30 | "TriggerFunctions": { 31 | "functions": [ 32 | { 33 | "type": "SetFollower", 34 | "id": "key_F_1" 35 | } 36 | ] 37 | } 38 | }, 39 | { 40 | "event": "ProjAppeared", 41 | "conditions": { 42 | "CasterIsFormID": "0x14", 43 | "ProjBaseIsFormID": "key_projFireball" 44 | }, 45 | "TriggerFunctions": { 46 | "functions": [ 47 | { 48 | "type": "SetRotationToSight", 49 | "on_followers": true 50 | }, 51 | { 52 | "type": "DisableFollower", 53 | "restore_speed": false, 54 | "on_followers": true 55 | } 56 | ] 57 | } 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /examples/HomieSpells_Follower_Guards_Ice.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_spellIceSpike": "Skyrim.esm|0x2B96C", 4 | "key_projIceSpear": "Skyrim.esm|0x10FBEE" 5 | }, 6 | 7 | "HomingData": { 8 | "key_H_1": { 9 | "type": "ConstAccel", 10 | "acceleration": 300, 11 | "aggressive": "Any" 12 | } 13 | }, 14 | 15 | "FollowersData": { 16 | "key_F_1": { 17 | "Pattern": { 18 | "Figure": { 19 | "shape": "Single" 20 | }, 21 | "normal": [0, 1, 0], 22 | "xDepends": false, 23 | "posOffset": [0, 0, 100], 24 | "origin": "NPC Spine [Spn0]" 25 | }, 26 | "collision": "None" 27 | } 28 | }, 29 | 30 | "MulticastSpawnGroups": { 31 | "key_SP_1": { 32 | "Pattern": { 33 | "Figure": { 34 | "shape": "Single", 35 | "count": 10 36 | } 37 | }, 38 | "rotOffset": [-90, 0] 39 | } 40 | }, 41 | "MulticastData": { 42 | "key_MC_1": [ 43 | { 44 | "spellID": "key_spellIceSpike", 45 | "spawn_group": "key_SP_1", 46 | "TriggerFunctions": { 47 | "functions": [ 48 | { 49 | "type": "SetHoming", 50 | "id": "key_H_1" 51 | } 52 | ] 53 | }, 54 | "HomingDetection": "Evenly" 55 | } 56 | ] 57 | }, 58 | 59 | "Triggers": [ 60 | { 61 | "event": "ProjAppeared", 62 | "conditions": { 63 | "ProjBaseIsFormID": "key_projIceSpear" 64 | }, 65 | "TriggerFunctions": { 66 | "functions": [ 67 | { 68 | "type": "SetFollower", 69 | "id": "key_F_1" 70 | } 71 | ] 72 | } 73 | }, 74 | { 75 | "event": "HitByMelee", 76 | "conditions": { 77 | "CasterIsFormID": "0x14" 78 | }, 79 | "TriggerFunctions": { 80 | "functions": [ 81 | { 82 | "type": "ApplyMultiCast", 83 | "id": "key_MC_1", 84 | "on_followers": true 85 | } 86 | ] 87 | } 88 | } 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /examples/HomieSpells_Follower_Guards_Lightning.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_spellLightning": "Skyrim.esm|0x45F9D", 4 | "key_projFireball": "Skyrim.esm|0x10FBED" 5 | }, 6 | 7 | "HomingData": { 8 | "key_H_1": { 9 | "type": "ConstSpeed", 10 | "rotationTime": 1, 11 | "aggressive": "Any" 12 | } 13 | }, 14 | 15 | "FollowersData": { 16 | "key_F_1": { 17 | "Pattern": { 18 | "Figure": { 19 | "shape": "Single" 20 | }, 21 | "normal": [0, 1, 0], 22 | "xDepends": false, 23 | "posOffset": [0, 0, 100], 24 | "origin": "NPC Spine [Spn0]" 25 | }, 26 | "collision": "None", 27 | "speed": 0 28 | } 29 | }, 30 | 31 | "MulticastSpawnGroups": { 32 | "key_SP_1": { 33 | "Pattern": { 34 | "Figure": { 35 | "shape": "Single" 36 | } 37 | } 38 | } 39 | }, 40 | "MulticastData": { 41 | "key_MC_1": [ 42 | { 43 | "spellID": "key_spellLightning", 44 | "spawn_group": "key_SP_1", 45 | "TriggerFunctions": { 46 | "functions": [ 47 | { 48 | "type": "SetHoming", 49 | "id": "key_H_1" 50 | } 51 | ] 52 | } 53 | } 54 | ] 55 | }, 56 | 57 | "EmittersData": { 58 | "key_E_1": { 59 | "interval": 1, 60 | "functions": [ 61 | { 62 | "type": "TriggerFunctions", 63 | "TriggerFunctions": { 64 | "functions": [ 65 | { 66 | "type": "ApplyMultiCast", 67 | "id": "key_MC_1" 68 | } 69 | ] 70 | } 71 | } 72 | ] 73 | } 74 | }, 75 | 76 | "Triggers": [ 77 | { 78 | "event": "ProjAppeared", 79 | "conditions": { 80 | "ProjBaseIsFormID": "key_projFireball" 81 | }, 82 | "TriggerFunctions": { 83 | "functions": [ 84 | { 85 | "type": "SetFollower", 86 | "id": "key_F_1" 87 | }, 88 | { 89 | "type": "SetEmitter", 90 | "id": "key_E_1" 91 | } 92 | ] 93 | } 94 | } 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /examples/HomieSpells_Homing.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_projFireball": "Skyrim.esm|0x10FBED" 4 | }, 5 | 6 | "HomingData": { 7 | "key_H_R": { 8 | "type": "ConstSpeed", 9 | "rotationTime": 1 10 | }, 11 | "key_H_L": { 12 | "type": "ConstAccel", 13 | "acceleration": 100, 14 | "target": "Cursor", 15 | "cursorAngle": 10 16 | } 17 | }, 18 | 19 | "Triggers": [ 20 | { 21 | "event": "ProjAppeared", 22 | "conditions": { 23 | "Hand": "Right", 24 | "CasterIsFormID": "0x14", 25 | "ProjBaseIsFormID": "key_projFireball" 26 | }, 27 | "TriggerFunctions": { 28 | "functions": [ 29 | { 30 | "type": "SetHoming", 31 | "id": "key_H_R" 32 | } 33 | ] 34 | } 35 | }, 36 | { 37 | "event": "ProjAppeared", 38 | "conditions": { 39 | "Hand": "Left", 40 | "CasterIsFormID": "0x14", 41 | "ProjBaseIsFormID": "key_projFireball" 42 | }, 43 | "TriggerFunctions": { 44 | "functions": [ 45 | { 46 | "type": "SetHoming", 47 | "id": "key_H_L" 48 | } 49 | ] 50 | } 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /examples/HomieSpells_Multicast.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_spellFireball": "Skyrim.esm|0x1C789" 4 | }, 5 | 6 | "MulticastSpawnGroups": { 7 | "key_SG_1": { 8 | "Pattern": { 9 | "Figure": { 10 | "shape": "Line", 11 | "count": 10, 12 | "size": 500 13 | } 14 | } 15 | } 16 | }, 17 | 18 | "MulticastData": { 19 | "key_MC_1": [ 20 | { 21 | "spawn_group": "key_SG_1", 22 | "spellID": "key_spellFireball" 23 | } 24 | ] 25 | }, 26 | 27 | "Triggers": [ 28 | { 29 | "event": "ProjAppeared", 30 | "conditions": {}, 31 | "TriggerFunctions": { 32 | "functions": [ 33 | { 34 | "type": "ApplyMultiCast", 35 | "id": "key_MC_1" 36 | } 37 | ], 38 | "disableOrigin": true 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /examples/HomieSpells_Multicast_Adv.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_spellFireball": "Skyrim.esm|0x1C789", 4 | "key_spellIceSpike": "Skyrim.esm|0x2B96C", 5 | "key_spellLightning": "Skyrim.esm|0x45F9D" 6 | }, 7 | 8 | "MulticastSpawnGroups": { 9 | "key_SG_1": { 10 | "Pattern": { 11 | "Figure": { 12 | "shape": "Circle", 13 | "count": 10, 14 | "size": 50 15 | }, 16 | "normal": [0, 0, 1], 17 | "xDepends": false, 18 | "origin": "NPC Spine [Spn0]" 19 | }, 20 | "rotation": "FromCenter" 21 | }, 22 | "key_SG_2": { 23 | "Pattern": { 24 | "Figure": { 25 | "shape": "Line", 26 | "count": 10, 27 | "size": 500 28 | }, 29 | "posOffset": [0, -100, 400], 30 | "origin": "NPC Spine [Spn0]", 31 | "planeRotate": 30 32 | }, 33 | "rotOffset": [10, 0] 34 | }, 35 | "key_SG_3": { 36 | "Pattern": { 37 | "Figure": { 38 | "shape": "Sphere", 39 | "count": 50, 40 | "size": 400 41 | }, 42 | "posOffset": [0, 0, 400], 43 | "origin": "NPC Spine [Spn0]", 44 | "normal": [0,1,0], 45 | "xDepends": false, 46 | "planeRotate": 30 47 | }, 48 | "rotOffset": [60, 0] 49 | } 50 | }, 51 | 52 | "MulticastData": { 53 | "key_MC_1": [ 54 | { 55 | "spawn_group": "key_SG_1", 56 | "spellID": "key_spellLightning" 57 | }, 58 | { 59 | "spawn_group": "key_SG_2", 60 | "spellID": "key_spellFireball" 61 | }, 62 | { 63 | "spawn_group": "key_SG_3", 64 | "spellID": "key_spellIceSpike" 65 | } 66 | ] 67 | }, 68 | 69 | "Triggers": [ 70 | { 71 | "event": "ProjAppeared", 72 | "conditions": {}, 73 | "TriggerFunctions": { 74 | "functions": [ 75 | { 76 | "type": "ApplyMultiCast", 77 | "id": "key_MC_1" 78 | } 79 | ], 80 | "disableOrigin": true 81 | } 82 | } 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /examples/HomieSpells_Multicast_Armageddon.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_spellFireball": "Skyrim.esm|0x1C789" 4 | }, 5 | 6 | "MulticastSpawnGroups": { 7 | "key_SG_1": { 8 | "Pattern": { 9 | "Figure": { 10 | "shape": "Line", 11 | "count": 50, 12 | "size": 5000 13 | }, 14 | "planeRotate": 90, 15 | "normal": [0, 1, 0], 16 | "xDepends": false, 17 | "origin": "NPC Spine [Spn0]", 18 | "posOffset": [0, 500, 3000] 19 | }, 20 | "rotation": "ToSight", 21 | "posRnd": [200, 200, 200], 22 | "rotRnd": [0, 0] 23 | } 24 | }, 25 | 26 | "MulticastData": { 27 | "key_MC_1": [ 28 | { 29 | "spawn_group": "key_SG_1", 30 | "spellID": "key_spellFireball" 31 | } 32 | ] 33 | }, 34 | 35 | "Triggers": [ 36 | { 37 | "event": "ProjAppeared", 38 | "conditions": {}, 39 | "TriggerFunctions": { 40 | "functions": [ 41 | { 42 | "type": "ApplyMultiCast", 43 | "id": "key_MC_1" 44 | } 45 | ], 46 | "disableOrigin": true 47 | } 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /examples/HomieSpells_Multicast_Homing.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_spellIceSpike": "Skyrim.esm|0x2B96C", 4 | "key_projFireball": "Skyrim.esm|0x10FBED" 5 | }, 6 | 7 | "HomingData": { 8 | "key_H_1": { 9 | "type": "ConstSpeed", 10 | "rotationTime": 1, 11 | "aggressive": "Any" 12 | } 13 | }, 14 | 15 | "MulticastSpawnGroups": { 16 | "key_SG_1": { 17 | "Pattern": { 18 | "Figure": { 19 | "shape": "Circle", 20 | "count": 20, 21 | "size": 300 22 | }, 23 | "normal": [0,0,1], 24 | "xDepends": false, 25 | "posOffset": [0,0,300] 26 | }, 27 | "rotation": "FromCenter", 28 | "rotOffset": [-60,0] 29 | } 30 | }, 31 | 32 | "MulticastData": { 33 | "key_MC_1": [ 34 | { 35 | "spawn_group": "key_SG_1", 36 | "spellID": "key_spellIceSpike", 37 | "HomingDetection": "Evenly", 38 | "TriggerFunctions": { 39 | "functions": [ 40 | { 41 | "type": "SetHoming", 42 | "id": "key_H_1" 43 | } 44 | ] 45 | } 46 | } 47 | ] 48 | }, 49 | 50 | "Triggers": [ 51 | { 52 | "event": "ProjAppeared", 53 | "conditions": { 54 | "ProjBaseIsFormID": "key_projFireball" 55 | }, 56 | "TriggerFunctions": { 57 | "functions": [ 58 | { 59 | "type": "ApplyMultiCast", 60 | "id": "key_MC_1" 61 | } 62 | ], 63 | "disableOrigin": true 64 | } 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /examples/HomieSpells_Multicast_SkyLightning.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_spellLightning": "Skyrim.esm|0x45F9D" 4 | }, 5 | 6 | "MulticastSpawnGroups": { 7 | "key_SG_1": { 8 | "Pattern": { 9 | "Figure": { 10 | "shape": "Single", 11 | "count": 10 12 | }, 13 | "normal": [0, 0, 1], 14 | "xDepends": false, 15 | "origin": "NPC Spine [Spn0]", 16 | "posOffset": [0, 0, 1000] 17 | }, 18 | "rotation": "ToSight", 19 | "posRnd": [100, 100, 0] 20 | } 21 | }, 22 | 23 | "MulticastData": { 24 | "key_MC_1": [ 25 | { 26 | "spawn_group": "key_SG_1", 27 | "spellID": "key_spellLightning" 28 | } 29 | ] 30 | }, 31 | 32 | "Triggers": [ 33 | { 34 | "event": "ProjAppeared", 35 | "conditions": {}, 36 | "TriggerFunctions": { 37 | "functions": [ 38 | { 39 | "type": "ApplyMultiCast", 40 | "id": "key_MC_1" 41 | } 42 | ], 43 | "disableOrigin": true 44 | } 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /examples/HomieSpells_Multicast_ToTargetEvenly.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_spellIceSpike": "Skyrim.esm|0x2B96C", 4 | "key_projFireball": "Skyrim.esm|0x10FBED" 5 | }, 6 | 7 | "HomingData": { 8 | "key_H_1": { 9 | "type": "ConstSpeed", 10 | "rotationTime": 1, 11 | "aggressive": "Any" 12 | } 13 | }, 14 | 15 | "MulticastSpawnGroups": { 16 | "key_SG_1": { 17 | "Pattern": { 18 | "Figure": { 19 | "shape": "Sphere", 20 | "count": 50, 21 | "size": 300 22 | }, 23 | "normal": [0,0,1], 24 | "xDepends": false, 25 | "posOffset": [0,0,500] 26 | }, 27 | "rotation": "ToTarget", 28 | "rotationTarget": "key_H_1", 29 | "rotRnd": [3,3] 30 | } 31 | }, 32 | 33 | "MulticastData": { 34 | "key_MC_1": [ 35 | { 36 | "spawn_group": "key_SG_1", 37 | "spellID": "key_spellIceSpike", 38 | "HomingDetection": "Evenly" 39 | } 40 | ] 41 | }, 42 | 43 | "Triggers": [ 44 | { 45 | "event": "ProjAppeared", 46 | "conditions": { 47 | "ProjBaseIsFormID": "key_projFireball" 48 | }, 49 | "TriggerFunctions": { 50 | "functions": [ 51 | { 52 | "type": "ApplyMultiCast", 53 | "id": "key_MC_1" 54 | } 55 | ], 56 | "disableOrigin": true 57 | } 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /examples/HomieSpells_Shotgun.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_spellFireball": "Skyrim.esm|0x1C789", 4 | "key_spellEmpty": "Skyrim.esm|0xE40CF", 5 | "key_projFireball": "Skyrim.esm|0x10FBED" 6 | }, 7 | 8 | "FollowersData": { 9 | "key_F_1": { 10 | "Pattern": { 11 | "Figure": { 12 | "count": 1, 13 | "shape": "Single" 14 | }, 15 | "origin": "NPC R MagicNode [RMag]" 16 | }, 17 | "speed": 0 18 | } 19 | }, 20 | 21 | "EmittersData": { 22 | "key_EM_1": { 23 | "count": 4, 24 | "limited": true, 25 | "destroyAfter": true, 26 | "interval": 0.1, 27 | "functions": [ 28 | { 29 | "type": "TriggerFunctions", 30 | "TriggerFunctions": { 31 | "functions": [ 32 | { 33 | "type": "ApplyMultiCast", 34 | "id": "key_MC_2" 35 | } 36 | ] 37 | } 38 | } 39 | ] 40 | } 41 | }, 42 | 43 | "MulticastSpawnGroups": { 44 | "key_SG_1": { 45 | "Pattern": { 46 | "Figure": { 47 | "shape": "Single", 48 | "count": 1 49 | } 50 | } 51 | } 52 | }, 53 | 54 | "MulticastData": { 55 | "key_MC_1": [ 56 | { 57 | "spellID": "key_spellEmpty", 58 | "spawn_group": "key_SG_1", 59 | "TriggerFunctions": { 60 | "functions": [ 61 | { 62 | "type": "SetFollower", 63 | "id": "key_F_1" 64 | }, 65 | { 66 | "type": "SetEmitter", 67 | "id": "key_EM_1" 68 | } 69 | ] 70 | } 71 | } 72 | ], 73 | "key_MC_2": [ 74 | { 75 | "spellID": "key_spellFireball", 76 | "spawn_group": "key_SG_1" 77 | } 78 | ] 79 | }, 80 | 81 | "Triggers": [ 82 | { 83 | "event": "ProjAppeared", 84 | "conditions": { 85 | "ProjBaseIsFormID": "key_projFireball" 86 | }, 87 | "TriggerFunctions": { 88 | "functions": [ 89 | { 90 | "type": "ApplyMultiCast", 91 | "id": "key_MC_1" 92 | } 93 | ] 94 | } 95 | } 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /examples/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormIDs": { 3 | "key_spellIceSpike": "Skeletons.esp|0x23950", 4 | "key_spellPowerofIce": "Skeletons.esp|0x238FB", 5 | "key_BossJurik": "Skyrim.esm|0x1BB28" 6 | }, 7 | "HomingData": { 8 | "key_H_1": { 9 | "aggressive": "Any", 10 | "type": "ConstSpeed", 11 | "rotationTime": 1 12 | } 13 | }, 14 | 15 | "FollowersData": { 16 | "key_F_1": { 17 | "Pattern": { 18 | "Figure": { 19 | "shape": "Circle", 20 | "count": 1, 21 | "size": 50 22 | }, 23 | "normal": [0, 0, 1], 24 | "xDepends": false, 25 | "posOffset": [0, 0, 150] 26 | }, 27 | "collision": "None" 28 | } 29 | }, 30 | 31 | "MulticastSpawnGroups": { 32 | "key_SP_1": { 33 | "Pattern": { 34 | "Figure": { 35 | "count": 1, 36 | "shape": "Single" 37 | } 38 | } 39 | } 40 | }, 41 | 42 | "MulticastData": { 43 | "key_MC_1": [ 44 | { 45 | "spellID": "key_spellIceSpike", 46 | "spawn_group": "key_SP_1", 47 | "TriggerFunctions": { 48 | "functions": [ 49 | { 50 | "type": "SetFollower", 51 | "id": "key_F_1" 52 | } 53 | ] 54 | } 55 | } 56 | ] 57 | }, 58 | 59 | "Triggers": [ 60 | { 61 | "event": "EffectStart", 62 | "conditions": { 63 | "SpellIsFormID": "key_spellPowerofIce" 64 | }, 65 | "TriggerFunctions": { 66 | "functions": [ 67 | { 68 | "type": "ApplyMultiCast", 69 | "id": "key_MC_1" 70 | } 71 | ] 72 | } 73 | }, 74 | { 75 | "event": "HitByProjectile", 76 | "conditions": { 77 | "CasterBaseIsFormID": "key_BossJurik" 78 | }, 79 | "TriggerFunctions": { 80 | "functions": [ 81 | { 82 | "type": "SetRotationHoming", 83 | "id": "key_H_1", 84 | "on_followers": true 85 | }, 86 | { 87 | "type": "SetHoming", 88 | "id": "key_H_1", 89 | "on_followers": true 90 | }, 91 | { 92 | "type": "ChangeSpeed", 93 | "data": { 94 | "type": "Mul", 95 | "value": 0.9 96 | }, 97 | "on_followers": true 98 | }, 99 | { 100 | "type": "DisableFollower", 101 | "on_followers": true, 102 | "restore_speed": true 103 | } 104 | ] 105 | } 106 | } 107 | ] 108 | } 109 | -------------------------------------------------------------------------------- /gen.bat: -------------------------------------------------------------------------------- 1 | cmake --preset=default-utils -B build -S . 2 | pause -------------------------------------------------------------------------------- /gifs/Accomulate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/Accomulate.gif -------------------------------------------------------------------------------- /gifs/AnimEvent.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/AnimEvent.gif -------------------------------------------------------------------------------- /gifs/Armageddon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/Armageddon.gif -------------------------------------------------------------------------------- /gifs/Avatar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/Avatar.gif -------------------------------------------------------------------------------- /gifs/Avatar1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/Avatar1.gif -------------------------------------------------------------------------------- /gifs/DelayedCast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/DelayedCast.gif -------------------------------------------------------------------------------- /gifs/Explode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/Explode.gif -------------------------------------------------------------------------------- /gifs/FillCircle_FillHalfCircle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/FillCircle_FillHalfCircle.gif -------------------------------------------------------------------------------- /gifs/FireballCannon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/FireballCannon.gif -------------------------------------------------------------------------------- /gifs/FireballDown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/FireballDown.gif -------------------------------------------------------------------------------- /gifs/FireballFromSkyOnAttack.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/FireballFromSkyOnAttack.gif -------------------------------------------------------------------------------- /gifs/FireballFromSkyOnShot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/FireballFromSkyOnShot.gif -------------------------------------------------------------------------------- /gifs/FollowerFinal1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/FollowerFinal1.gif -------------------------------------------------------------------------------- /gifs/FollowersCast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/FollowersCast.gif -------------------------------------------------------------------------------- /gifs/GuardAttackerLightning.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/GuardAttackerLightning.gif -------------------------------------------------------------------------------- /gifs/GuardIce1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/GuardIce1.gif -------------------------------------------------------------------------------- /gifs/HalfSphere.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/HalfSphere.gif -------------------------------------------------------------------------------- /gifs/HomingMulticast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/HomingMulticast.gif -------------------------------------------------------------------------------- /gifs/Impact.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/Impact.gif -------------------------------------------------------------------------------- /gifs/Lightnings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/Lightnings.gif -------------------------------------------------------------------------------- /gifs/SwordLightning.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/SwordLightning.gif -------------------------------------------------------------------------------- /gifs/ThroughWalls.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenix31415/NewProjectilesTMP/71fdf57fc850421e20912cb2835b98cdd887bf72/gifs/ThroughWalls.gif -------------------------------------------------------------------------------- /multiply.py: -------------------------------------------------------------------------------- 1 | from jsonschema import validate 2 | from jsonschema import exceptions 3 | import json 4 | import os 5 | 6 | def load_schema(sch_path): 7 | with open(sch_path, 'r') as schemaString: 8 | return json.load(schemaString) 9 | 10 | def validate_one_(path, schema): 11 | with open(path, 'r') as jsonString: 12 | try: 13 | validate(instance=json.load(jsonString), schema=schema) 14 | return '' 15 | except exceptions.ValidationError as e: 16 | return f'{e.message} at {e.absolute_schema_path}' 17 | #print(e.schema) 18 | #print(e.instance) 19 | 20 | def validate_one(path, sch_path): 21 | return validate_one_(path, load_schema(sch_path)) 22 | 23 | def validate_all(path, sch_path): 24 | schema = load_schema(sch_path) 25 | 26 | ans = '' 27 | for file in os.listdir(path): 28 | filename = os.fsdecode(file) 29 | if filename.endswith('.json') and not filename.endswith('schema.json'): 30 | ans_ = validate_one_(os.path.join(path, filename), schema) 31 | if len(ans_) > 0: 32 | ans += f'{filename}: {ans_}\n' 33 | continue 34 | else: 35 | continue 36 | return ans 37 | 38 | #print(validate_one('json/HomieSpells_Homing.json', 'json/schema.json')) 39 | #print(validate_all('json', 'json/schema.json')) 40 | -------------------------------------------------------------------------------- /src/Emitters.cpp: -------------------------------------------------------------------------------- 1 | #include "TriggerFunctions.h" 2 | #include "JsonUtils.h" 3 | #include "RuntimeData.h" 4 | 5 | namespace Emitters 6 | { 7 | struct SpeedData 8 | { 9 | enum class SpeedChangeTypes : uint32_t 10 | { 11 | Linear, 12 | Quadratic, 13 | Exponential 14 | } type; 15 | 16 | float time; 17 | }; 18 | 19 | struct FunctionData 20 | { 21 | enum class Type : uint32_t 22 | { 23 | AccelerateToMaxSpeed, // accelerate until max speed, during given time 24 | TriggerFunctions // call triggers in NewProjsType 25 | }; 26 | 27 | std::variant data; 28 | 29 | FunctionData(const std::string& filename, const Json::Value& function) 30 | { 31 | Type type = JsonUtils::read_enum(function, "type"); 32 | switch (type) { 33 | case Type::AccelerateToMaxSpeed: 34 | data = SpeedData{ JsonUtils::read_enum(function, "speedType"), 35 | JsonUtils::getFloat(function, "time") }; 36 | break; 37 | case Type::TriggerFunctions: 38 | data = TriggerFunctions::Functions(filename, function["TriggerFunctions"]); 39 | break; 40 | default: 41 | assert(false); 42 | } 43 | } 44 | 45 | Type get_type() const { return static_cast(data.index()); } 46 | }; 47 | 48 | struct Data 49 | { 50 | std::vector functions; 51 | float interval; 52 | uint32_t limited: 1; 53 | uint32_t count: 30; 54 | uint32_t destroy_after: 1; 55 | }; 56 | 57 | struct Storage 58 | { 59 | static void clear_keys() 60 | { 61 | keys.clear(); 62 | } 63 | static void clear() 64 | { 65 | clear_keys(); 66 | data_static.clear(); 67 | } 68 | 69 | static void init(const std::string& filename, const Json::Value& HomingData) 70 | { 71 | for (auto& key : HomingData.getMemberNames()) { 72 | read_json_entry(filename, key, HomingData[key]); 73 | } 74 | } 75 | 76 | static void init_keys(const std::string& filename, const Json::Value& HomingData) 77 | { 78 | for (auto& key : HomingData.getMemberNames()) { 79 | read_json_entry_keys(filename, key, HomingData[key]); 80 | } 81 | } 82 | 83 | static const auto& get_data(uint32_t ind) { return data_static[ind - 1]; } 84 | 85 | static uint32_t get_key_ind(const std::string& filename, const std::string& key) { return keys.get(filename, key); } 86 | 87 | private: 88 | static void read_json_entry(const std::string& filename, const std::string& key, const Json::Value& item) 89 | { 90 | [[maybe_unused]] uint32_t ind = keys.get(filename, key); 91 | assert(ind == data_static.size() + 1); 92 | 93 | const auto& functions = item["functions"]; 94 | 95 | data_static.emplace_back(std::vector(), JsonUtils::getFloat(item, "interval"), 96 | JsonUtils::mb_read_field(item, "limited"), JsonUtils::mb_read_field<1u>(item, "count"), 97 | JsonUtils::mb_read_field(item, "destroyAfter")); 98 | 99 | auto& new_functions = data_static.back().functions; 100 | for (size_t i = 0; i < functions.size(); i++) { 101 | const auto& function = functions[(int)i]; 102 | 103 | new_functions.emplace_back(filename, function); 104 | } 105 | } 106 | 107 | static void read_json_entry_keys(const std::string& filename, const std::string& key, const Json::Value&) 108 | { 109 | keys.add(filename, key); 110 | } 111 | 112 | static inline JsonUtils::KeysMap keys; 113 | static inline std::vector data_static; 114 | }; 115 | 116 | uint32_t get_key_ind(const std::string& filename, const std::string& key) { return Storage::get_key_ind(filename, key); } 117 | 118 | void clear() { Storage::clear(); } 119 | void clear_keys() { Storage::clear_keys(); } 120 | 121 | void init(const std::string& filename, const Json::Value& json_root) 122 | { 123 | if (json_root.isMember("EmittersData")) { 124 | Storage::init(filename, json_root["EmittersData"]); 125 | } 126 | } 127 | 128 | void init_keys(const std::string& filename, const Json::Value& json_root) 129 | { 130 | if (json_root.isMember("EmittersData")) { 131 | Storage::init_keys(filename, json_root["EmittersData"]); 132 | } 133 | } 134 | 135 | void set_emitter_ind(RE::Projectile* proj, uint32_t ind) { ::set_emitter_ind(proj, ind); } 136 | uint32_t get_emitter_ind(RE::Projectile* proj) { return ::get_emitter_ind(proj); } 137 | bool is_emitter(RE::Projectile* proj) { return get_emitter_ind(proj) != 0; } 138 | void disable_emitter(RE::Projectile* proj) { set_emitter_ind(proj, 0); } 139 | 140 | void disable(RE::Projectile* proj) 141 | { 142 | if (proj && proj->IsMissileProjectile()) { 143 | disable_emitter(proj); 144 | } 145 | } 146 | 147 | void apply(RE::Projectile* proj, uint32_t ind) 148 | { 149 | if (proj->IsMissileProjectile()) { 150 | assert(ind > 0); 151 | set_emitter_ind(proj, ind); 152 | auto emitter_ind = get_emitter_ind(proj); 153 | auto& data = Storage::get_data(emitter_ind); 154 | if (data.limited) { 155 | set_emitter_rest(proj, data.count); 156 | } 157 | } 158 | } 159 | 160 | void onUpdate(RE::Projectile* proj, float dtime) 161 | { 162 | auto emitter_ind = get_emitter_ind(proj); 163 | 164 | auto& data = Storage::get_data(emitter_ind); 165 | 166 | if (proj->livingTime < data.interval) 167 | return; 168 | 169 | if (data.limited && get_emitter_rest(proj) == 0) 170 | return; 171 | 172 | proj->livingTime = 0.000001f; 173 | 174 | for (const auto& function : data.functions) { 175 | switch (function.get_type()) { 176 | case FunctionData::Type::TriggerFunctions: 177 | std::get(function.data).call(proj); 178 | break; 179 | case FunctionData::Type::AccelerateToMaxSpeed: 180 | { 181 | // TODO: fix 182 | // y = a*e^bx, a = M/X, b = ln X / N 183 | //constexpr float X = 70.0f; 184 | //constexpr float LN_X = 4.248495242049359f; // ln 70 185 | //float b = LN_X / function.args.time; 186 | //float cur_speed = proj->linearVelocity.Length(); 187 | //float add_to_speed = exp(b * dtime); 188 | //float new_speed = cur_speed * add_to_speed; 189 | //proj->linearVelocity *= (new_speed / cur_speed); 190 | 191 | float max_speed = FenixUtils::Projectile__GetSpeed(proj); 192 | float cur_speed = proj->linearVelocity.Length(); 193 | if (cur_speed < max_speed) { 194 | auto& function_speed = std::get(function.data); 195 | float TOTAL_TIME = function_speed.time; 196 | float dspeed = 0; 197 | switch (function_speed.type) { 198 | case SpeedData::SpeedChangeTypes::Linear: 199 | dspeed = max_speed / TOTAL_TIME * dtime; 200 | break; 201 | case SpeedData::SpeedChangeTypes::Quadratic: 202 | dspeed = 2 * std::sqrt(cur_speed * max_speed) * TOTAL_TIME * dtime; 203 | break; 204 | case SpeedData::SpeedChangeTypes::Exponential: 205 | dspeed = cur_speed * std::log(max_speed) / TOTAL_TIME * dtime; 206 | break; 207 | } 208 | 209 | float new_speed = cur_speed + dspeed; 210 | proj->linearVelocity *= (new_speed / cur_speed); 211 | } 212 | } 213 | break; 214 | default: 215 | break; 216 | } 217 | } 218 | 219 | if (data.limited) { 220 | auto rest = get_emitter_rest(proj); 221 | set_emitter_rest(proj, rest - 1); 222 | if (rest == 1) { 223 | if (data.destroy_after) { 224 | proj->Kill(); 225 | } else { 226 | disable_emitter(proj); 227 | } 228 | } 229 | } 230 | } 231 | 232 | namespace Hooks 233 | { 234 | class EmitterHook 235 | { 236 | public: 237 | static void Hook() 238 | { 239 | _CheckExplosion = SKSE::GetTrampoline().write_call<5>(REL::ID(42852).address() + 0x680, 240 | CheckExplosion); // SkyrimSE.exe+745450 241 | _AddImpact = SKSE::GetTrampoline().write_call<5>(REL::ID(42547).address() + 0x56, 242 | AddImpact); // SkyrimSE.exe+732456 -- disable on hit 243 | _BSSoundHandle__ClearFollowedObject = SKSE::GetTrampoline().write_call<5>(REL::ID(42930).address() + 0x21, 244 | BSSoundHandle__ClearFollowedObject); // SkyrimSE.exe+74BC21 -- disable on kill 245 | } 246 | 247 | private: 248 | static RE::Explosion* CheckExplosion(RE::MissileProjectile* proj, float dtime) 249 | { 250 | if (is_emitter(proj)) { 251 | onUpdate(proj, dtime); 252 | } 253 | 254 | return _CheckExplosion(proj, dtime); 255 | } 256 | 257 | static void* AddImpact(RE::Projectile* proj, RE::TESObjectREFR* a2, RE::NiPoint3* a3, RE::NiPoint3* a_velocity, 258 | RE::hkpCollidable* a_collidable, uint32_t a6, char a7) 259 | { 260 | auto ans = _AddImpact(proj, a2, a3, a_velocity, a_collidable, a6, a7); 261 | if (is_emitter(proj)) { 262 | disable_emitter(proj); 263 | } 264 | return ans; 265 | } 266 | static void BSSoundHandle__ClearFollowedObject(char* sound) 267 | { 268 | _BSSoundHandle__ClearFollowedObject(sound); 269 | auto proj = reinterpret_cast(sound - 0x128); 270 | if (is_emitter(proj)) { 271 | disable_emitter(proj); 272 | } 273 | } 274 | 275 | static inline REL::Relocation _CheckExplosion; 276 | static inline REL::Relocation _AddImpact; 277 | static inline REL::Relocation _BSSoundHandle__ClearFollowedObject; 278 | }; 279 | } 280 | 281 | void install() { Hooks::EmitterHook::Hook(); } 282 | } 283 | -------------------------------------------------------------------------------- /src/Emitters.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "json/json.h" 4 | 5 | namespace Emitters 6 | { 7 | uint32_t get_key_ind(const std::string& filename, const std::string& key); 8 | void install(); 9 | void init(const std::string& filename, const Json::Value& json_root); 10 | void init_keys(const std::string& filename, const Json::Value& json_root); 11 | void clear(); 12 | void clear_keys(); 13 | void apply(RE::Projectile* proj, uint32_t ind); 14 | void disable(RE::Projectile* proj); 15 | } 16 | -------------------------------------------------------------------------------- /src/Followers.cpp: -------------------------------------------------------------------------------- 1 | #include "Followers.h" 2 | #include "JsonUtils.h" 3 | #include "RuntimeData.h" 4 | #include 5 | #include "Positioning.h" 6 | 7 | namespace Followers 8 | { 9 | // TODO: cylinder 10 | enum class Rounding : uint32_t 11 | { 12 | None, 13 | Plane, 14 | Sphere 15 | }; 16 | 17 | struct Data 18 | { 19 | Positioning::Pattern pattern; // 00 20 | 21 | Rounding rounding: 2; 22 | Collision collision: 2; 23 | float rounding_radius; // if rounding 24 | float speed_mult; // 0 for instant, default: 1 25 | 26 | explicit Data(const Json::Value& item) : 27 | pattern(item["Pattern"]), rounding(JsonUtils::mb_read_field(item, "rounding")), 28 | rounding_radius(rounding != Rounding::None ? JsonUtils::getFloat(item, "roundingR") : 0), 29 | collision(JsonUtils::mb_read_field(item, "collision")), 30 | speed_mult(JsonUtils::mb_getFloat<1.0f>(item, "speed")) 31 | {} 32 | }; 33 | static_assert(sizeof(Data) == 0x40); 34 | 35 | struct Storage 36 | { 37 | static void clear_keys() 38 | { 39 | keys.clear(); 40 | } 41 | static void clear() 42 | { 43 | clear_keys(); 44 | data_static.clear(); 45 | } 46 | 47 | static void init(const std::string& filename, const Json::Value& HomingData) 48 | { 49 | for (auto& key : HomingData.getMemberNames()) { 50 | read_json_entry(filename, key, HomingData[key]); 51 | } 52 | } 53 | 54 | static void init_keys(const std::string& filename, const Json::Value& HomingData) 55 | { 56 | for (auto& key : HomingData.getMemberNames()) { 57 | read_json_entry_keys(filename, key, HomingData[key]); 58 | } 59 | } 60 | 61 | static const auto& get_data(uint32_t ind) { return data_static[ind - 1]; } 62 | 63 | static uint32_t get_key_ind(const std::string& filename, const std::string& key) { return keys.get(filename, key); } 64 | 65 | private: 66 | static void read_json_entry(const std::string& filename, const std::string& key, const Json::Value& item) 67 | { 68 | [[maybe_unused]] uint32_t ind = keys.get(filename, key); 69 | assert(ind == data_static.size() + 1); 70 | 71 | data_static.emplace_back(item); 72 | } 73 | 74 | static void read_json_entry_keys(const std::string& filename, const std::string& key, const Json::Value&) 75 | { 76 | keys.add(filename, key); 77 | } 78 | 79 | static inline JsonUtils::KeysMap keys; 80 | static inline std::vector data_static; 81 | }; 82 | 83 | uint32_t get_key_ind(const std::string& filename, const std::string& key) { return Storage::get_key_ind(filename, key); } 84 | 85 | void set_follower_ind(RE::Projectile* proj, uint32_t ind) { ::set_follower_ind(proj, ind); } 86 | uint32_t get_follower_ind(RE::Projectile* proj) { return ::get_follower_ind(proj); } 87 | void set_follower_shape_ind(RE::Projectile* proj, uint32_t ind) { ::set_follower_shape_ind(proj, ind); } 88 | uint32_t get_follower_shape_ind(RE::Projectile* proj) { return ::get_follower_shape_ind(proj); } 89 | bool is_follower(RE::Projectile* proj) { return get_follower_ind(proj) != 0; } 90 | void disable_follower(RE::Projectile* proj) { set_follower_ind(proj, 0); } 91 | 92 | namespace Moving 93 | { 94 | auto get_target_point(RE::Projectile* proj) 95 | { 96 | auto& data = Storage::get_data(get_follower_ind(proj)); 97 | 98 | RE::NiPoint3 final_vel; 99 | auto caster = proj->shooter.get().get()->As(); 100 | RE::Projectile::ProjectileRot dir{ caster->GetAngleX(), caster->GetAngleZ() }; 101 | 102 | RE::NiPoint3 spawn_center = caster->GetPosition(); 103 | data.pattern.initCenter(spawn_center, dir, caster); 104 | RE::NiPoint3 cast_dir = data.pattern.getCastDir(dir); 105 | cast_dir.Unitize(); 106 | 107 | return data.pattern.GetPosition(spawn_center, cast_dir, get_follower_shape_ind(proj)); 108 | } 109 | 110 | RE::NiPoint2 rotate(RE::NiPoint2 P, float alpha) 111 | { 112 | float _cos = cosf(alpha); 113 | float _sin = sin(alpha); 114 | return { P.x * _cos - P.y * _sin, P.y * _cos + P.x * _sin }; 115 | } 116 | 117 | const float CIRCLE_K = 0.01f; 118 | const float CIRCLE_K_BIG = 1.0f + CIRCLE_K; 119 | const float CIRCLE_K_SML = 1.0f - CIRCLE_K; 120 | 121 | RE::NiPoint3 get_target_point_rounding_sphere(RE::Projectile* proj, RE::NiPoint3* dV) 122 | { 123 | auto& data = Storage::get_data(get_follower_ind(proj)); 124 | 125 | auto target_pos = get_target_point(proj); 126 | float D2 = proj->GetPosition().GetSquaredDistance(target_pos); 127 | 128 | float dtime = sqrtf(dV->SqrLength() / proj->linearVelocity.SqrLength()); 129 | float speed_origin = proj->linearVelocity.Length(); 130 | float R = data.rounding_radius; 131 | float R2 = R * R; 132 | 133 | auto cast_dir = proj->linearVelocity.UnitCross(target_pos - proj->GetPosition()); 134 | Positioning::Plane plane(target_pos, cast_dir); 135 | 136 | //draw_circle0(target_pos, R, cast_dir); 137 | 138 | if (D2 > R2 * CIRCLE_K_BIG * CIRCLE_K_BIG) { 139 | // Out of circle 140 | // Move to tangent 141 | RE::NiPoint2 P = plane.project(proj->GetPosition() - target_pos); 142 | RE::NiPoint2 vel = plane.project(proj->linearVelocity); 143 | 144 | RE::NiPoint2 Q = { -P.y, P.x }; 145 | auto d2 = P.SqrLength(); 146 | float a = R2 / d2; 147 | float b = R / d2 * sqrtf(d2 - R2); 148 | if (P.Cross(vel) < 0) 149 | b *= -1; 150 | auto T3 = plane.unproject(P * a + Q * b); 151 | auto V = T3 - proj->GetPosition(); 152 | float len = V.Unitize(); 153 | auto my_len = speed_origin * dtime; 154 | float circle_len = my_len - len; 155 | if (circle_len > 0) { 156 | return FenixUtils::Geom::rotate(T3, circle_len / R, target_pos, cast_dir); 157 | } else { 158 | return V * my_len + proj->GetPosition(); 159 | } 160 | } else if (D2 < R2 * CIRCLE_K_SML * CIRCLE_K_SML) { 161 | // Inside of circle 162 | // Slightly rotate velocity to tangent 163 | 164 | RE::NiPoint3 proj_dir_final = (proj->GetPosition() - target_pos).UnitCross(cast_dir); 165 | if (proj_dir_final.Dot(proj->linearVelocity) < 0) { 166 | proj_dir_final *= -1; 167 | } else { 168 | } 169 | 170 | proj->linearVelocity = 171 | FenixUtils::Geom::rotateVel(proj->linearVelocity, dtime * speed_origin / R, proj_dir_final); 172 | 173 | return proj->GetPosition() + proj->linearVelocity * dtime; 174 | } else { 175 | // On circle 176 | // Move around the circle 177 | 178 | float phi = dtime * speed_origin / R; 179 | RE::NiPoint3 proj_dir_final = (proj->GetPosition() - target_pos).UnitCross(cast_dir); 180 | if (proj_dir_final.Dot(proj->linearVelocity) < 0) { 181 | proj_dir_final *= -1; 182 | } else { 183 | phi *= -1; 184 | } 185 | 186 | proj->linearVelocity = proj_dir_final * speed_origin; 187 | return FenixUtils::Geom::rotate(proj->GetPosition(), phi, target_pos, cast_dir); 188 | } 189 | } 190 | 191 | RE::NiPoint3 get_target_point_rounding_plane(RE::Projectile* proj, RE::NiPoint3* dV) 192 | { 193 | auto& data = Storage::get_data(get_follower_ind(proj)); 194 | 195 | auto target_pos = get_target_point(proj); 196 | 197 | float dtime = sqrtf(dV->SqrLength() / proj->linearVelocity.SqrLength()); 198 | float speed_origin = proj->linearVelocity.Length(); 199 | float R = data.rounding_radius; 200 | float R2 = R * R; 201 | 202 | auto caster = proj->shooter.get().get()->As(); 203 | RE::NiPoint3 cast_dir = data.pattern.getCastDir({ caster->GetAngleX(), caster->GetAngleZ() }); 204 | cast_dir.Unitize(); 205 | Positioning::Plane plane(target_pos, cast_dir); 206 | 207 | float dir_z = -cast_dir.Dot(proj->GetPosition() - target_pos); 208 | RE::NiPoint2 P = plane.project(proj->GetPosition() - target_pos); 209 | RE::NiPoint2 vel = plane.project(proj->linearVelocity); 210 | 211 | float D2 = P.SqrLength(); 212 | 213 | if (D2 > R2 * CIRCLE_K_BIG * CIRCLE_K_BIG) { 214 | // Out of cylinder 215 | // Move to tangent 216 | 217 | RE::NiPoint2 Q = { -P.y, P.x }; 218 | float a = R2 / D2; 219 | float b = R / D2 * sqrtf(D2 - R2); 220 | if (P.Cross(vel) < 0) 221 | b *= -1; 222 | auto T3 = plane.unproject(P * a + Q * b); 223 | auto V = T3 - proj->GetPosition(); 224 | float len = V.Unitize(); 225 | auto my_len = speed_origin * dtime; 226 | float circle_len = my_len - len; 227 | if (circle_len > 0) { 228 | return FenixUtils::Geom::rotate(T3, circle_len / R, target_pos, cast_dir); 229 | } else { 230 | return V * my_len + proj->GetPosition(); 231 | } 232 | } else if (D2 < R2 * CIRCLE_K_SML * CIRCLE_K_SML) { 233 | // Inside of cylinder 234 | // Slightly rotate velocity to tangent 235 | 236 | RE::NiPoint3 proj_dir_final = (proj->GetPosition() - target_pos).UnitCross(cast_dir); 237 | if (proj_dir_final.Dot(proj->linearVelocity) < 0) { 238 | proj_dir_final *= -1; 239 | } 240 | 241 | proj->linearVelocity = 242 | FenixUtils::Geom::rotateVel(proj->linearVelocity, dtime * speed_origin / R, proj_dir_final); 243 | 244 | return proj->GetPosition() + proj->linearVelocity * dtime; 245 | } else { 246 | // On cylinder 247 | // Move around the cylinder 248 | 249 | float L = dtime * speed_origin; 250 | float H = abs(dir_z); 251 | float t; 252 | 253 | if (L > H + 0.0001f) { 254 | t = std::min(1.0f, H / sqrtf(L * L - H * H)); 255 | } else { 256 | t = 1; 257 | } 258 | 259 | float dl = L / sqrtf(1 + t * t); 260 | float dh = dl * t; 261 | float df = dl / R; 262 | if (P.Cross(vel) >= 0) 263 | df *= -1; 264 | auto ans = FenixUtils::Geom::rotate(proj->GetPosition(), df, target_pos, cast_dir); 265 | if (dir_z < 0) 266 | dh *= -1; 267 | ans += cast_dir * dh; 268 | return ans; 269 | } 270 | } 271 | 272 | void change_direction_linVel(RE::Projectile* proj, const RE::NiPoint3& target_pos, float speed_mult) 273 | { 274 | auto dir = target_pos - proj->GetPosition(); 275 | auto dist = dir.Unitize(); 276 | auto speed_origin = FenixUtils::Projectile__GetSpeed(proj); 277 | auto speed_needed = dist * speed_mult; 278 | float speed = std::min(speed_origin, speed_needed); 279 | proj->linearVelocity = dir * speed; 280 | } 281 | 282 | void change_direction(RE::Projectile* proj, RE::NiPoint3*, float dtime) 283 | { 284 | auto target_pos = get_target_point(proj); 285 | 286 | auto& data = Storage::get_data(get_follower_ind(proj)); 287 | switch (data.rounding) { 288 | case Rounding::Sphere: 289 | //change_direction_rounding_sphere(proj, target_pos, dtime); 290 | break; 291 | case Rounding::Plane: 292 | //change_direction_rounding_plane(proj, target_pos, dtime); 293 | break; 294 | case Rounding::None: 295 | default: 296 | change_direction_linVel(proj, target_pos, data.speed_mult); 297 | break; 298 | } 299 | 300 | // Smooth rotating 301 | RE::NiPoint3 proj_dir; 302 | if (proj->linearVelocity.SqrLength() > 30.0f) { 303 | proj_dir = proj->linearVelocity; 304 | } else { 305 | auto proj_dir_final = FenixUtils::Geom::angles2dir(proj->shooter.get().get()->data.angle); 306 | auto proj_dir_cur = FenixUtils::Geom::angles2dir(proj->data.angle); 307 | proj_dir = FenixUtils::Geom::rotateVel(proj_dir_cur, data.speed_mult * dtime, proj_dir_final); 308 | } 309 | FenixUtils::Geom::Projectile::update_node_rotation(proj, proj_dir); 310 | 311 | } 312 | 313 | void change_direction_instant(RE::Projectile* proj, RE::NiPoint3* dV) 314 | { 315 | auto& data = Storage::get_data(get_follower_ind(proj)); 316 | 317 | RE::NiPoint3 P; 318 | RE::NiPoint3 proj_dir; 319 | if (data.speed_mult == 0) { 320 | P = get_target_point(proj); 321 | proj_dir = FenixUtils::Geom::angles2dir(proj->shooter.get().get()->data.angle); 322 | proj_dir.Unitize(); 323 | } else if (data.rounding == Rounding::Sphere) { 324 | P = get_target_point_rounding_sphere(proj, dV); 325 | } else if (data.rounding == Rounding::Plane) { 326 | P = get_target_point_rounding_plane(proj, dV); 327 | } else { 328 | return; 329 | } 330 | 331 | if (data.rounding == Rounding::Sphere || data.rounding == Rounding::Plane) { 332 | proj_dir = P - proj->GetPosition(); 333 | proj_dir.Unitize(); 334 | proj->linearVelocity = proj_dir * proj->linearVelocity.Length(); 335 | } 336 | 337 | FenixUtils::Geom::Projectile::update_node_rotation(proj, proj_dir); 338 | *dV = P - proj->GetPosition(); 339 | } 340 | } 341 | 342 | namespace Hooks 343 | { 344 | // Make projectile follow the caster 345 | class FollowingHook 346 | { 347 | public: 348 | static void Hook() 349 | { 350 | _Projectile__apply_gravity = SKSE::GetTrampoline().write_call<5>(REL::ID(43006).address() + 0x69, 351 | change_direction); // SkyrimSE.exe+751309 352 | _Projectile__MovePoint = SKSE::GetTrampoline().write_call<5>(REL::ID(43006).address() + 0x85, 353 | change_direction_instant); // 140751325 354 | } 355 | 356 | private: 357 | static bool change_direction(RE::Projectile* proj, RE::NiPoint3* dV, float dtime) 358 | { 359 | bool ans = _Projectile__apply_gravity(proj, dV, dtime); 360 | if (is_follower(proj)) { 361 | Moving::change_direction(proj, dV, dtime); 362 | } 363 | return ans; 364 | } 365 | 366 | static void change_direction_instant(RE::Projectile* proj, RE::NiPoint3* dV) 367 | { 368 | if (is_follower(proj)) { 369 | Moving::change_direction_instant(proj, dV); 370 | } 371 | 372 | _Projectile__MovePoint(proj, dV); 373 | } 374 | 375 | static inline REL::Relocation _Projectile__apply_gravity; 376 | static inline REL::Relocation _Projectile__MovePoint; 377 | }; 378 | 379 | class NoCollisionHook 380 | { 381 | public: 382 | static void Hook() 383 | { 384 | { 385 | // InitHavok 386 | struct Code : Xbyak::CodeGenerator 387 | { 388 | Code(uintptr_t func_addr) 389 | { 390 | // rdi == proj 391 | mov(rdx, rdi); 392 | mov(rax, func_addr); 393 | jmp(rax); 394 | } 395 | } xbyakCode{ uintptr_t(InitHavok__GetCollisionLayer) }; 396 | 397 | _InitHavok__GetCollisionLayer = add_trampoline<5, 42934, 0x9d, true>(&xbyakCode); // SkyrimSE.exe+74beed 398 | } 399 | } 400 | 401 | private: 402 | static RE::COL_LAYER GetCollisionLayer(RE::Projectile* proj, RE::COL_LAYER origin) 403 | { 404 | if (is_follower(proj)) { 405 | return layer2layer(Storage::get_data(get_follower_ind(proj)).collision); 406 | } 407 | 408 | return origin; 409 | } 410 | 411 | static RE::COL_LAYER InitHavok__GetCollisionLayer(RE::BGSProjectile* bproj, RE::Projectile* proj) 412 | { 413 | return GetCollisionLayer(proj, _InitHavok__GetCollisionLayer(bproj)); 414 | } 415 | 416 | using func_t = RE::COL_LAYER(RE::BGSProjectile*); 417 | static inline REL::Relocation _InitHavok__GetCollisionLayer; 418 | }; 419 | } 420 | 421 | void get_unused_shape_ind(RE::Projectile* proj, std::set& ans, const RE::BSTArray& a) 422 | { 423 | auto baseType = get_follower_ind(proj); 424 | for (auto& i : a) { 425 | if (auto _proj = i.get().get(); _proj && get_follower_ind(_proj) == baseType && _proj->formID != proj->formID) { 426 | uint32_t ind = get_follower_shape_ind(_proj); 427 | ans.erase(ind); 428 | } 429 | } 430 | } 431 | 432 | uint32_t get_unused_shape_ind(RE::Projectile* proj) 433 | { 434 | std::set ans; 435 | { 436 | auto& data = Storage::get_data(get_follower_ind(proj)); 437 | int tmp = 0; 438 | std::generate_n(std::inserter(ans, ans.begin()), data.pattern.getSize(), [&tmp]() { return tmp++; }); 439 | } 440 | 441 | auto manager = RE::Projectile::Manager::GetSingleton(); 442 | get_unused_shape_ind(proj, ans, manager->limited); 443 | get_unused_shape_ind(proj, ans, manager->pending); 444 | get_unused_shape_ind(proj, ans, manager->unlimited); 445 | return ans.begin() == ans.end() ? -1 : *ans.begin(); 446 | } 447 | 448 | forEachRes forEachFollower(RE::TESObjectREFR* a, const RE::BSTArray& arr, 449 | const forEachF& func) 450 | { 451 | for (auto& i : arr) { 452 | if (auto proj = i.get().get(); proj && proj->shooter.get().get()) { 453 | if (proj->shooter.get().get()->formID == a->formID && is_follower(proj)) { 454 | if (func(proj) == forEachRes::kStop) 455 | return forEachRes::kStop; 456 | } 457 | } 458 | } 459 | return forEachRes::kContinue; 460 | } 461 | 462 | void forEachFollower(RE::TESObjectREFR* a, const forEachF& func) 463 | { 464 | auto manager = RE::Projectile::Manager::GetSingleton(); 465 | if (forEachFollower(a, manager->limited, func) == forEachRes::kStop) 466 | return; 467 | if (forEachFollower(a, manager->pending, func) == forEachRes::kStop) 468 | return; 469 | if (forEachFollower(a, manager->unlimited, func) == forEachRes::kStop) 470 | return; 471 | } 472 | 473 | void disable(RE::Projectile* proj, bool restore_speed) 474 | { 475 | if (is_follower(proj)) { 476 | if (restore_speed) { 477 | auto& data = Storage::get_data(get_follower_ind(proj)); 478 | if (data.rounding == Followers::Rounding::None) { 479 | float speed = FenixUtils::Projectile__GetSpeed(proj); 480 | proj->linearVelocity *= speed / proj->linearVelocity.Length(); 481 | } 482 | } 483 | FenixUtils::Projectile__set_collision_layer(proj, RE::COL_LAYER::kSpell); 484 | disable_follower(proj); 485 | } 486 | } 487 | 488 | RE::COL_LAYER layer2layer(Collision l) 489 | { 490 | switch (l) { 491 | case Collision::Actor: 492 | return RE::COL_LAYER(54); 493 | case Collision::None: 494 | return RE::COL_LAYER::kNonCollidable; 495 | case Collision::Spell: 496 | default: 497 | return RE::COL_LAYER::kSpell; 498 | } 499 | } 500 | 501 | void apply(RE::Projectile* proj, uint32_t ind) 502 | { 503 | if (proj->IsMissileProjectile() && proj->shooter.get().get() && proj->shooter.get().get()->As()) { 504 | assert(ind > 0); 505 | 506 | set_follower_ind(proj, ind); 507 | 508 | auto& data = Storage::get_data(ind); 509 | 510 | if (!data.pattern.isShapeless()) { 511 | // TODO: optimize for MC 512 | auto new_ind = get_unused_shape_ind(proj); 513 | if (new_ind == -1 || new_ind >= Storage::get_data(ind).pattern.getSize()) 514 | new_ind = 0; 515 | 516 | set_follower_shape_ind(proj, new_ind); 517 | } 518 | } 519 | } 520 | 521 | void install() 522 | { 523 | using namespace Hooks; 524 | FollowingHook::Hook(); 525 | NoCollisionHook::Hook(); 526 | } 527 | 528 | void clear_keys() { Storage::clear_keys(); } 529 | void clear() { Storage::clear(); } 530 | 531 | void init(const std::string& filename, const Json::Value& json_root) 532 | { 533 | if (json_root.isMember("FollowersData")) { 534 | Storage::init(filename, json_root["FollowersData"]); 535 | } 536 | } 537 | 538 | void init_keys(const std::string& filename, const Json::Value& json_root) 539 | { 540 | if (json_root.isMember("FollowersData")) { 541 | Storage::init_keys(filename, json_root["FollowersData"]); 542 | } 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /src/Followers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "json/json.h" 3 | 4 | namespace Followers 5 | { 6 | enum class Collision : uint32_t 7 | { 8 | Actor, 9 | Spell, 10 | None 11 | }; 12 | 13 | void install(); 14 | void clear(); 15 | void clear_keys(); 16 | void init(const std::string& filename, const Json::Value& json_root); 17 | void init_keys(const std::string& filename, const Json::Value& json_root); 18 | uint32_t get_key_ind(const std::string& filename, const std::string& key); 19 | void apply(RE::Projectile* proj, uint32_t ind); 20 | void disable(RE::Projectile* proj, bool restore_speed = true); 21 | RE::COL_LAYER layer2layer(Collision l); 22 | 23 | using forEachRes = RE::BSContainer::ForEachResult; 24 | using forEachF = std::function; 25 | void forEachFollower(RE::TESObjectREFR* a, const forEachF& func); 26 | } 27 | -------------------------------------------------------------------------------- /src/Homing.cpp: -------------------------------------------------------------------------------- 1 | #include "Homing.h" 2 | #include "JsonUtils.h" 3 | #include "RuntimeData.h" 4 | 5 | namespace Homing 6 | { 7 | enum class HomingTypes : uint32_t 8 | { 9 | ConstSpeed, // Projectile has constant speed 10 | ConstAccel // Projectile has constant rotation time 11 | }; 12 | 13 | enum class TargetTypes : uint32_t 14 | { 15 | Nearest, // Find nearest target 16 | Cursor // Find closest to cursor (within radius) target 17 | }; 18 | static constexpr TargetTypes TargetTypes__DEFAULT = TargetTypes::Nearest; 19 | 20 | enum class AggressiveTypes : uint32_t 21 | { 22 | Aggressive, // Accept only aggressive to caster at the moment targets 23 | Hostile, // Accept only hostile targets 24 | Any, // Accept any target 25 | }; 26 | static constexpr AggressiveTypes AggressiveTypes__DEFAULT = AggressiveTypes::Hostile; 27 | 28 | struct Data 29 | { 30 | HomingTypes type: 1; 31 | TargetTypes target: 2; 32 | uint32_t check_LOS: 1; 33 | AggressiveTypes hostile_filter: 2; 34 | float detection_angle; // valid for target == cursor 35 | float val1; // rotation time (ConstSpeed) or acceleration (ConstAccel) 36 | }; 37 | static_assert(sizeof(Data) == 12); 38 | 39 | struct Storage 40 | { 41 | static void clear_keys() { keys.clear(); } 42 | static void clear() 43 | { 44 | clear_keys(); 45 | data_static.clear(); 46 | } 47 | 48 | static void init(const std::string& filename, const Json::Value& HomingData) 49 | { 50 | for (auto& key : HomingData.getMemberNames()) { 51 | read_json_entry(filename, key, HomingData[key]); 52 | } 53 | } 54 | 55 | static void init_keys(const std::string& filename, const Json::Value& HomingData) 56 | { 57 | for (auto& key : HomingData.getMemberNames()) { 58 | read_json_entry_keys(filename, key, HomingData[key]); 59 | } 60 | } 61 | 62 | static const auto& get_data(uint32_t ind) { return data_static[ind - 1]; } 63 | 64 | static uint32_t get_key_ind(const std::string& filename, const std::string& key) { return keys.get(filename, key); } 65 | 66 | private: 67 | static void read_json_entry(const std::string& filename, const std::string& key, const Json::Value& item) 68 | { 69 | [[maybe_unused]] uint32_t ind = keys.get(filename, key); 70 | assert(ind == data_static.size() + 1); 71 | 72 | auto type = JsonUtils::read_enum(item, "type"); 73 | auto target = JsonUtils::mb_read_field(item, "target"); 74 | bool check_los = JsonUtils::mb_read_field(item, "checkLOS"); 75 | auto aggressive = JsonUtils::mb_read_field(item, "aggressive"); 76 | 77 | float detection_angle = 0.0f; 78 | if (target == TargetTypes::Cursor) { 79 | detection_angle = JsonUtils::getFloat(item, "cursorAngle"); 80 | } 81 | 82 | float val1 = 0.0f; 83 | switch (type) { 84 | case HomingTypes::ConstAccel: 85 | val1 = JsonUtils::getFloat(item, "acceleration"); 86 | break; 87 | case HomingTypes::ConstSpeed: 88 | val1 = JsonUtils::getFloat(item, "rotationTime"); 89 | break; 90 | default: 91 | assert(false); 92 | break; 93 | } 94 | 95 | data_static.emplace_back(type, target, check_los, aggressive, detection_angle, val1); 96 | } 97 | 98 | static void read_json_entry_keys(const std::string& filename, const std::string& key, const Json::Value&) 99 | { 100 | keys.add(filename, key); 101 | } 102 | 103 | static inline JsonUtils::KeysMap keys; 104 | static inline std::vector data_static; 105 | }; 106 | 107 | // used in multicast evenly 108 | const Data& get_data(uint32_t ind) { return Storage::get_data(ind); } 109 | 110 | uint32_t get_key_ind(const std::string& filename, const std::string& key) { return Storage::get_key_ind(filename, key); } 111 | 112 | void set_homing_ind(RE::Projectile* proj, uint32_t ind) { ::set_homing_ind(proj, ind); } 113 | uint32_t get_homing_ind(RE::Projectile* proj) { return ::get_homing_ind(proj); } 114 | bool is_homing(RE::Projectile* proj) { return get_homing_ind(proj) != 0; } 115 | void disable_homing(RE::Projectile* proj) { set_homing_ind(proj, 0); } 116 | 117 | namespace Targeting 118 | { 119 | constexpr float WITHIN_DIST2 = 4.0E7f; 120 | 121 | using FenixUtils::Geom::Actor::AnticipatePos; 122 | 123 | bool is_hostile(RE::TESObjectREFR* refr, RE::TESObjectREFR* _caster) 124 | { 125 | auto target = refr->As(); 126 | auto caster = _caster->As(); 127 | if (!target || !caster) 128 | return false; 129 | return target->currentCombatTarget.get().get() == caster; 130 | } 131 | 132 | bool filter_target_base(RE::TESObjectREFR& _refr, RE::TESObjectREFR* caster) 133 | { 134 | return !_refr.IsDisabled() && !_refr.IsDead() && _refr.GetFormType() == RE::FormType::ActorCharacter && 135 | _refr.formID != caster->formID; 136 | } 137 | 138 | bool filter_target_los(RE::TESObjectREFR& _refr, RE::TESObjectREFR* caster, bool check_los) 139 | { 140 | return !check_los || !caster->As() || !_refr.As() || 141 | FenixUtils::Geom::Actor::ActorInLOS(caster->As(), _refr.As(), 100); 142 | } 143 | 144 | bool filter_target_aggressive(RE::TESObjectREFR& _refr, RE::TESObjectREFR* caster, AggressiveTypes type) 145 | { 146 | return type == AggressiveTypes::Any || (type == AggressiveTypes::Aggressive && is_hostile(&_refr, caster)) || 147 | (type == AggressiveTypes::Hostile && _refr.As() && caster->As() && 148 | _refr.As()->IsHostileToActor(caster->As())); 149 | } 150 | 151 | bool filter_target_dist(RE::TESObjectREFR& _refr, const RE::NiPoint3& origin_pos, float within_dist2) 152 | { 153 | return origin_pos.GetSquaredDistance(_refr.GetPosition()) < within_dist2; 154 | } 155 | 156 | bool filter_target(RE::TESObjectREFR& _refr, RE::TESObjectREFR* caster, const RE::NiPoint3& origin_pos, 157 | AggressiveTypes type, bool check_los, float within_dist2) 158 | { 159 | return filter_target_base(_refr, caster) && filter_target_dist(_refr, origin_pos, within_dist2) && 160 | filter_target_los(_refr, caster, check_los) && filter_target_aggressive(_refr, caster, type); 161 | } 162 | 163 | RE::Actor* find_nearest_target(RE::TESObjectREFR* caster, const RE::NiPoint3& origin_pos, const Data& data, 164 | float within_dist2 = WITHIN_DIST2) 165 | { 166 | bool check_los = data.check_LOS; 167 | auto hostile_filter = data.hostile_filter; 168 | 169 | float mindist2 = 1.0E15f; 170 | RE::TESObjectREFR* refr = nullptr; 171 | RE::TES::GetSingleton()->ForEachReference([=, &mindist2, &refr](RE::TESObjectREFR& _refr) { 172 | if (filter_target(_refr, caster, origin_pos, hostile_filter, check_los, within_dist2)) { 173 | float curdist2 = origin_pos.GetSquaredDistance(_refr.GetPosition()); 174 | if (curdist2 < mindist2) { 175 | mindist2 = curdist2; 176 | refr = &_refr; 177 | } 178 | } 179 | return RE::BSContainer::ForEachResult::kContinue; 180 | }); 181 | 182 | if (!refr) 183 | return nullptr; 184 | 185 | return refr->As(); 186 | } 187 | 188 | std::vector get_nearest_targets(RE::TESObjectREFR* caster, const RE::NiPoint3& origin_pos, const Data& data, 189 | float within_dist2 = WITHIN_DIST2) 190 | { 191 | bool check_los = data.check_LOS; 192 | auto hostile_filter = data.hostile_filter; 193 | 194 | std::vector ans; 195 | 196 | RE::TES::GetSingleton()->ForEachReference([=, &ans](RE::TESObjectREFR& _refr) { 197 | if (filter_target(_refr, caster, origin_pos, hostile_filter, check_los, within_dist2)) { 198 | ans.push_back(_refr.As()); 199 | } 200 | return RE::BSContainer::ForEachResult::kContinue; 201 | }); 202 | 203 | return ans; 204 | } 205 | 206 | namespace Cursor 207 | { 208 | static bool is_anglebetween_less(const RE::NiPoint3& A, const RE::NiPoint3& B1, const RE::NiPoint3& B2, 209 | float angle_deg) 210 | { 211 | auto AB1 = B1 - A; 212 | auto AB2 = B2 - A; 213 | AB1.Unitize(); 214 | AB2.Unitize(); 215 | return acos(AB1.Dot(AB2)) < angle_deg / 180.0f * 3.1415926f; 216 | } 217 | 218 | static bool is_near_to_cursor(RE::Actor* caster, RE::Actor* target, float angle) 219 | { 220 | RE::NiPoint3 caster_pos, caster_sight, target_pos; 221 | 222 | caster_pos = FenixUtils::Geom::Actor::CalculateLOSLocation(caster, FenixUtils::LineOfSightLocation::kHead); 223 | target_pos = FenixUtils::Geom::Actor::CalculateLOSLocation(target, FenixUtils::LineOfSightLocation::kTorso); 224 | 225 | caster_sight = caster_pos; 226 | caster_sight += FenixUtils::Geom::angles2dir(caster->data.angle); 227 | 228 | return is_anglebetween_less(caster_pos, caster_sight, target_pos, angle); 229 | } 230 | 231 | bool filter_target_cursor(RE::TESObjectREFR& _refr, RE::Actor* caster, AggressiveTypes type, bool check_los, 232 | float angle, float within_dist2) 233 | { 234 | auto refr = _refr.As(); 235 | return filter_target(_refr, caster, caster->GetPosition(), type, check_los, within_dist2) && refr && 236 | is_near_to_cursor(caster, refr, angle); 237 | } 238 | 239 | RE::Actor* find_cursor_target(RE::TESObjectREFR* _caster, const Data& data, float within_dist2 = WITHIN_DIST2) 240 | { 241 | if (!_caster->IsPlayerRef()) 242 | return nullptr; 243 | 244 | auto caster = _caster->As(); 245 | std::vector> targets; 246 | 247 | auto angle = data.detection_angle; 248 | bool check_los = data.check_LOS; 249 | auto hostile_filter = data.hostile_filter; 250 | 251 | RE::TES::GetSingleton()->ForEachReference([=, &targets](RE::TESObjectREFR& _refr) { 252 | if (filter_target_cursor(_refr, caster, hostile_filter, check_los, angle, within_dist2)) { 253 | auto caster_pos = 254 | FenixUtils::Geom::Actor::CalculateLOSLocation(caster, FenixUtils::LineOfSightLocation::kHead); 255 | auto target_pos = 256 | FenixUtils::Geom::Actor::CalculateLOSLocation(&_refr, FenixUtils::LineOfSightLocation::kTorso); 257 | 258 | auto caster_sight = caster_pos; 259 | caster_sight += FenixUtils::Geom::angles2dir(caster->data.angle); 260 | 261 | auto AB1 = caster_sight - caster_pos; 262 | auto AB2 = target_pos - caster_pos; 263 | AB1.Unitize(); 264 | AB2.Unitize(); 265 | 266 | targets.push_back( 267 | //{ _refr.As(), caster->GetPosition().GetSquaredDistance(_refr.GetPosition()) }); 268 | { _refr.As(), abs(acos(AB1.Dot(AB2))) }); 269 | } 270 | return RE::BSContainer::ForEachResult::kContinue; 271 | }); 272 | 273 | if (!targets.size()) 274 | return nullptr; 275 | 276 | return (*std::min_element(targets.begin(), targets.end(), 277 | [](const std::pair& a, const std::pair& b) { 278 | return a.second < b.second; 279 | })) 280 | .first; 281 | } 282 | 283 | std::vector get_cursor_targets(RE::TESObjectREFR* _caster, const Data& data, 284 | float within_dist2 = WITHIN_DIST2) 285 | { 286 | std::vector ans; 287 | 288 | if (!_caster->IsPlayerRef()) 289 | return ans; 290 | 291 | auto caster = _caster->As(); 292 | 293 | auto angle = data.detection_angle; 294 | bool check_los = data.check_LOS; 295 | auto hostile_filter = data.hostile_filter; 296 | 297 | RE::TES::GetSingleton()->ForEachReference([=, &ans](RE::TESObjectREFR& _refr) { 298 | if (filter_target_cursor(_refr, caster, hostile_filter, check_los, angle, within_dist2)) { 299 | auto refr = _refr.As(); 300 | if (caster->GetPosition().GetSquaredDistance(refr->GetPosition()) < within_dist2) { 301 | ans.push_back(refr); 302 | } 303 | } 304 | return RE::BSContainer::ForEachResult::kContinue; 305 | }); 306 | 307 | return ans; 308 | } 309 | } 310 | 311 | RE::Actor* findTarget(RE::TESObjectREFR* origin, const Data& data) 312 | { 313 | auto proj = origin->As(); 314 | 315 | if (proj) { 316 | auto target = proj->desiredTarget.get().get(); 317 | if (target) 318 | return target->As(); 319 | } 320 | 321 | auto caster = proj ? proj->shooter.get().get() : origin; 322 | if (!caster) 323 | return nullptr; 324 | 325 | if (auto caster_npc = caster->As(); caster_npc && !caster_npc->IsPlayerRef()) { 326 | return caster_npc->currentCombatTarget.get().get(); 327 | } 328 | 329 | auto target_type = data.target; 330 | float within_dist = 331 | proj && (proj->IsFlameProjectile() || proj->IsBeamProjectile()) ? proj->range * proj->range : WITHIN_DIST2; 332 | 333 | RE::TESObjectREFR* refr; 334 | switch (target_type) { 335 | case TargetTypes::Nearest: 336 | { 337 | refr = find_nearest_target(caster, (proj ? proj : caster)->GetPosition(), data, within_dist); 338 | break; 339 | } 340 | case TargetTypes::Cursor: 341 | refr = Cursor::find_cursor_target(caster, data, within_dist); 342 | break; 343 | default: 344 | refr = nullptr; 345 | break; 346 | } 347 | 348 | if (!refr) 349 | return nullptr; 350 | 351 | #ifdef DEBUG 352 | FenixUtils::notification("Target found: %s", refr->GetName()); 353 | #endif // DEBUG 354 | 355 | if (proj) 356 | proj->desiredTarget = refr->GetHandle(); 357 | return refr->As(); 358 | } 359 | } 360 | 361 | namespace Moving 362 | { 363 | bool get_shoot_dir(RE::Projectile* proj, RE::Actor* target, float dtime, RE::NiPoint3& ans) 364 | { 365 | RE::NiPoint3 target_dir; 366 | target->GetLinearVelocity(target_dir); 367 | double target_speed = target_dir.Length(); 368 | 369 | double proj_speed = FenixUtils::Projectile__GetSpeed(proj); 370 | 371 | auto target_pos = Targeting::AnticipatePos(target, dtime); 372 | auto strait_dir = target_pos - proj->GetPosition(); 373 | 374 | double a = proj_speed * proj_speed - target_speed * target_speed; 375 | 376 | double strait_len = strait_dir.Unitize(); 377 | double c = -(strait_len * strait_len); 378 | double b; 379 | 380 | if (target_speed > 0.0001) { 381 | target_dir.Unitize(); 382 | double cos_phi = -target_dir.Dot(strait_dir); 383 | b = 2 * strait_len * target_speed * cos_phi; 384 | } else { 385 | b = 0.0; 386 | } 387 | 388 | double D = b * b - 4 * a * c; 389 | if (D < 0) 390 | return false; 391 | 392 | D = sqrt(D); 393 | double t1 = (-b + D) / a * 0.5; 394 | double t2 = (-b - D) / a * 0.5; 395 | 396 | if (t1 <= 0 && t2 <= 0) 397 | return false; 398 | 399 | double t = t1; 400 | if (t2 > 0 && t2 < t1) 401 | t = t2; 402 | 403 | ans = target_dir * (float)target_speed + strait_dir * (float)(strait_len / t); 404 | return true; 405 | } 406 | 407 | // constant speed, limited rotation angle 408 | void change_direction_1(RE::Projectile* proj, float dtime, const RE::NiPoint3& final_vel, float param) 409 | { 410 | auto get_rotation_speed = []([[maybe_unused]] RE::Projectile* proj, float param) { 411 | // param1 / 100 = time to rotate at 180 412 | // 250 350 500 norm 413 | return 3.1415926f / param; 414 | }; 415 | 416 | auto final_dir = final_vel; 417 | final_dir.Unitize(); 418 | 419 | proj->linearVelocity = 420 | FenixUtils::Geom::rotateVel(proj->linearVelocity, get_rotation_speed(proj, param) * dtime, final_dir); 421 | } 422 | 423 | // constant acceleration length 424 | void change_direction_2(RE::Projectile* proj, [[maybe_unused]] float dtime, const RE::NiPoint3& final_vel, float param) 425 | { 426 | auto get_acceleration = []([[maybe_unused]] RE::Projectile* proj, float param) { 427 | // param1 / 10 = acceleration vector length 428 | // 50 100 500 429 | return param; 430 | }; 431 | 432 | auto V = final_vel; 433 | auto speed = proj->linearVelocity.Length(); 434 | V.Unitize(); 435 | V *= speed; 436 | V -= proj->linearVelocity; 437 | V.Unitize(); 438 | V *= get_acceleration(proj, param); 439 | speed = FenixUtils::Projectile__GetSpeed(proj); 440 | proj->linearVelocity += V; 441 | float newspeed = proj->linearVelocity.Length(); 442 | proj->linearVelocity *= speed / newspeed; 443 | } 444 | 445 | void change_direction_linVel(RE::Projectile* proj, float dtime) 446 | { 447 | RE::NiPoint3 final_vel; 448 | auto& data = Storage::get_data(get_homing_ind(proj)); 449 | if (auto target = Targeting::findTarget(proj, data); target && get_shoot_dir(proj, target, dtime, final_vel)) { 450 | auto val1 = data.val1; 451 | auto type = data.type; 452 | switch (type) { 453 | case HomingTypes::ConstSpeed: 454 | change_direction_1(proj, dtime, final_vel, val1); 455 | break; 456 | case HomingTypes::ConstAccel: 457 | change_direction_2(proj, dtime, final_vel, val1); 458 | break; 459 | default: 460 | break; 461 | } 462 | } else { 463 | disable_homing(proj); 464 | } 465 | } 466 | 467 | void change_direction(RE::Projectile* proj, RE::NiPoint3*, float dtime) 468 | { 469 | change_direction_linVel(proj, dtime); 470 | 471 | FenixUtils::Geom::Projectile::update_node_rotation(proj); 472 | 473 | #ifdef DEBUG 474 | { 475 | auto proj_dir = proj->linearVelocity; 476 | proj_dir.Unitize(); 477 | draw_line(proj->GetPosition(), proj->GetPosition() + proj_dir, Colors::RED); 478 | } 479 | #endif // DEBUG 480 | } 481 | } 482 | 483 | namespace Hooks 484 | { 485 | // Change ShouldUseDesiredTarget vfunc 486 | class HomingFlamesHook 487 | { 488 | public: 489 | static void Hook() 490 | { 491 | _ShouldUseDesiredTarget = 492 | REL::Relocation(REL::ID(RE::VTABLE_FlameProjectile[0])).write_vfunc(0xC1, ShouldUseDesiredTarget); 493 | } 494 | 495 | private: 496 | // TODO: update if dist too far away 497 | static bool ShouldUseDesiredTarget(RE::Projectile* proj) 498 | { 499 | bool ans = _ShouldUseDesiredTarget(proj); 500 | if (auto target = proj->desiredTarget.get().get()) { 501 | draw_point(target->GetPosition(), Colors::RED, 0); 502 | draw_point(proj->GetPosition(), Colors::BLU, 0); 503 | 504 | auto range = proj->GetProjectileBase()->data.range; 505 | 506 | if (target->IsDead() || target->GetPosition().GetSquaredDistance(proj->GetPosition()) > range * range) { 507 | proj->desiredTarget = {}; 508 | return ans; 509 | } 510 | 511 | return true; 512 | } 513 | return ans; 514 | } 515 | 516 | static inline REL::Relocation _ShouldUseDesiredTarget; 517 | }; 518 | 519 | // Make projectile move to the target 520 | class HomingMissilesHook 521 | { 522 | public: 523 | static void Hook() 524 | { 525 | _Projectile__ApplyGravity = SKSE::GetTrampoline().write_call<5>(REL::ID(43006).address() + 0x69, 526 | change_direction); // SkyrimSE.exe+751309 527 | } 528 | 529 | private: 530 | static bool change_direction(RE::Projectile* proj, RE::NiPoint3* dV, float dtime) 531 | { 532 | bool ans = _Projectile__ApplyGravity(proj, dV, dtime); 533 | if (is_homing(proj)) { 534 | Moving::change_direction(proj, dV, dtime); 535 | } 536 | return ans; 537 | } 538 | 539 | static inline REL::Relocation _Projectile__ApplyGravity; 540 | }; 541 | 542 | #ifdef DEBUG 543 | namespace Debug 544 | { 545 | // TODO 546 | uint32_t get_cursor_ind(RE::PlayerCharacter*) 547 | { 548 | /*if (auto obj = a->GetEquippedObject(false)) 549 | if (auto spel = obj->As()) 550 | if (auto mgef = FenixUtils::getAVEffectSetting(spel)) 551 | if (auto ind = Triggers::get_homing_ind(mgef->data.projectileBase)) { 552 | const auto& data = Storage::get_data(ind); 553 | if (data.target == TargetTypes::Cursor) 554 | return ind; 555 | } 556 | */ 557 | return 0; 558 | } 559 | 560 | // Draw debug lines to captured targets 561 | class CursorDetectedHook 562 | { 563 | public: 564 | static void Hook() 565 | { 566 | _Update = REL::Relocation(REL::ID(RE::VTABLE_PlayerCharacter[0])).write_vfunc(0xad, Update); 567 | } 568 | 569 | private: 570 | static void Update(RE::PlayerCharacter* a, float delta) 571 | { 572 | _Update(a, delta); 573 | 574 | if (auto ind = get_cursor_ind(a)) { 575 | const auto& data = Storage::get_data(ind); 576 | if (auto target = Targeting::Cursor::find_cursor_target(a, data, Targeting::WITHIN_DIST2)) 577 | draw_line(a->GetPosition(), target->GetPosition(), Colors::RED, 0); 578 | } 579 | } 580 | 581 | static inline REL::Relocation _Update; 582 | }; 583 | 584 | // Draw debug circle of target capturing 585 | class CursorCircleHook 586 | { 587 | public: 588 | static void Hook() 589 | { 590 | _Update = REL::Relocation(REL::ID(RE::VTABLE_PlayerCharacter[0])).write_vfunc(0xad, Update); 591 | } 592 | 593 | private: 594 | static void Update(RE::PlayerCharacter* a, float delta) 595 | { 596 | _Update(a, delta); 597 | 598 | if (auto ind = get_cursor_ind(a)) { 599 | const auto& data = Storage::get_data(ind); 600 | float alpha_max = data.detection_angle; 601 | alpha_max = alpha_max / 180.0f * 3.1415926f; 602 | 603 | RE::NiPoint3 origin, caster_dir; 604 | origin = FenixUtils::Geom::Actor::CalculateLOSLocation(a, FenixUtils::LineOfSightLocation::kHead); 605 | 606 | const float circle_dist = 2000; 607 | caster_dir = FenixUtils::Geom::angles2dir(a->data.angle); 608 | 609 | float circle_r = circle_dist * tan(alpha_max); 610 | RE::NiPoint3 right_dir = RE::NiPoint3(0, 0, -1).UnitCross(caster_dir); 611 | if (right_dir.SqrLength() == 0) 612 | right_dir = { 1, 0, 0 }; 613 | right_dir *= circle_r; 614 | RE::NiPoint3 up_dir = right_dir.Cross(caster_dir); 615 | 616 | origin += caster_dir * circle_dist; 617 | 618 | RE::NiPoint3 old = origin + right_dir; 619 | const int N = 31; 620 | for (int i = 1; i <= N; i++) { 621 | float alpha = 2 * 3.1415926f / N * i; 622 | 623 | auto cur_p = origin + right_dir * cos(alpha) + up_dir * sin(alpha); 624 | 625 | draw_line(old, cur_p, Colors::RED, 0); 626 | old = cur_p; 627 | } 628 | } 629 | } 630 | 631 | static inline REL::Relocation _Update; 632 | }; 633 | } 634 | #endif // DEBUG 635 | } 636 | 637 | std::vector get_targets(uint32_t homingInd, RE::TESObjectREFR* caster, const RE::NiPoint3& origin_pos) 638 | { 639 | auto& homing_data = get_data(homingInd); 640 | return homing_data.target == TargetTypes::Cursor ? Targeting::Cursor::get_cursor_targets(caster, homing_data) : 641 | Targeting::get_nearest_targets(caster, origin_pos, homing_data); 642 | } 643 | 644 | void applyRotate(RE::Projectile* proj, uint32_t ind, RE::Actor* targetOverride) 645 | { 646 | auto caster = proj->shooter.get().get(); 647 | if (!caster) 648 | return; 649 | 650 | if (proj->IsMissileProjectile() || proj->IsBeamProjectile()) { 651 | auto& data = Storage::get_data(ind); 652 | 653 | if (!targetOverride) 654 | targetOverride = Targeting::findTarget(proj, data); 655 | 656 | if (targetOverride) { 657 | FenixUtils::Geom::Projectile::aimToPoint(proj, Targeting::AnticipatePos(targetOverride)); 658 | } 659 | } 660 | } 661 | 662 | void disable(RE::Projectile* proj) 663 | { 664 | if (proj->IsMissileProjectile()) { 665 | disable_homing(proj); 666 | } 667 | } 668 | 669 | void apply(RE::Projectile* proj, uint32_t ind, RE::Actor* targetOverride) 670 | { 671 | auto caster = proj->shooter.get().get(); 672 | if (!caster) 673 | return; 674 | 675 | assert(ind > 0); 676 | 677 | if (proj->IsMissileProjectile()) { 678 | set_homing_ind(proj, ind); 679 | } 680 | 681 | auto& data = Storage::get_data(ind); 682 | 683 | if (!targetOverride) 684 | targetOverride = Targeting::findTarget(proj, data); 685 | else 686 | proj->desiredTarget = targetOverride->GetHandle(); 687 | 688 | if (!targetOverride) 689 | return; 690 | 691 | if (proj->IsBeamProjectile()) { 692 | auto dir = FenixUtils::Geom::rot_at(proj->GetPosition(), Targeting::AnticipatePos(targetOverride)); 693 | 694 | FenixUtils::TESObjectREFR__SetAngleOnReferenceZ(proj, dir.z); 695 | FenixUtils::TESObjectREFR__SetAngleOnReferenceX(proj, dir.x); 696 | } 697 | } 698 | 699 | void install() 700 | { 701 | using namespace Hooks; 702 | 703 | HomingFlamesHook::Hook(); 704 | HomingMissilesHook::Hook(); 705 | 706 | #ifdef DEBUG 707 | Debug::CursorDetectedHook::Hook(); 708 | Debug::CursorCircleHook::Hook(); 709 | #endif 710 | } 711 | 712 | void clear() { Storage::clear(); } 713 | void clear_keys() { Storage::clear_keys(); } 714 | 715 | void init(const std::string& filename, const Json::Value& json_root) 716 | { 717 | if (json_root.isMember("HomingData")) { 718 | Storage::init(filename, json_root["HomingData"]); 719 | } 720 | } 721 | 722 | void init_keys(const std::string& filename, const Json::Value& json_root) 723 | { 724 | if (json_root.isMember("HomingData")) { 725 | Storage::init_keys(filename, json_root["HomingData"]); 726 | } 727 | } 728 | } 729 | -------------------------------------------------------------------------------- /src/Homing.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "json/json.h" 4 | 5 | namespace Homing 6 | { 7 | void applyRotate(RE::Projectile* proj, uint32_t ind, RE::Actor* targetOverride); 8 | void apply(RE::Projectile* proj, uint32_t ind, RE::Actor* targetOverride); 9 | void disable(RE::Projectile* proj); 10 | 11 | void install(); 12 | void init(const std::string& filename, const Json::Value& json_root); 13 | void init_keys(const std::string& filename, const Json::Value& json_root); 14 | void clear(); 15 | void clear_keys(); 16 | uint32_t get_key_ind(const std::string& filename, const std::string& key); 17 | 18 | // For MC 19 | std::vector get_targets(uint32_t homingInd, RE::TESObjectREFR* caster, const RE::NiPoint3& origin_pos); 20 | } 21 | -------------------------------------------------------------------------------- /src/Hooks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RuntimeData.h" 4 | 5 | namespace Hooks 6 | { 7 | // Create proj & load from save with zero padding value 8 | // TODO: replace load part to serialization 9 | class PaddingsProjectileHook 10 | { 11 | public: 12 | static void Hook() 13 | { 14 | auto& trmpl = SKSE::GetTrampoline(); 15 | 16 | // SkyrimSE.exe+74a978 17 | _ctor1 = trmpl.write_call<5>(REL::ID(42920).address() + 0x28, ctor1); 18 | // SkyrimSE.exe+74a766 19 | _ctor2 = trmpl.write_call<5>(REL::ID(42919).address() + 0x16, ctor2); 20 | _TESObjectREFR__ReadFromSaveGame_140286FD0 = 21 | trmpl.write_call<5>(REL::ID(42953).address() + 0x4b, LoadGame); // SkyrimSE.exe+74D28B 22 | } 23 | 24 | private: 25 | static void ctor1(RE::Projectile* proj) 26 | { 27 | _ctor1(proj); 28 | init_NormalType(proj); 29 | } 30 | static void ctor2(RE::Projectile* proj) 31 | { 32 | _ctor2(proj); 33 | init_NormalType(proj); 34 | } 35 | 36 | static void __fastcall LoadGame(RE::Projectile* proj, RE::BGSLoadGameBuffer* buf) 37 | { 38 | _TESObjectREFR__ReadFromSaveGame_140286FD0(proj, buf); 39 | init_NormalType(proj); 40 | } 41 | 42 | static inline REL::Relocation _ctor1; 43 | static inline REL::Relocation _ctor2; 44 | static inline REL::Relocation _TESObjectREFR__ReadFromSaveGame_140286FD0; 45 | }; 46 | 47 | // Allows to create many beams. 48 | // NewBeam returns `found`, if false, old proj not removes 49 | class MultipleBeamsHook 50 | { 51 | public: 52 | static void Hook() 53 | { 54 | auto& trmpl = SKSE::GetTrampoline(); 55 | 56 | _RefHandle__get = trmpl.write_call<5>(REL::ID(42928).address() + 0x117, NewBeam); // SkyrimSE.exe+74B287 57 | 58 | { 59 | // SkyrimSE.exe+733F93 60 | uintptr_t ret_addr = REL::ID(42586).address() + 0x2d3; 61 | 62 | struct Code : Xbyak::CodeGenerator 63 | { 64 | Code(uintptr_t func_addr, uintptr_t ret_addr) 65 | { 66 | Xbyak::Label nocancel; 67 | 68 | // rsi = proj 69 | // xmm0 -- xmm2 = node pos 70 | mov(r9, rsi); 71 | mov(rax, func_addr); 72 | call(rax); 73 | mov(rax, ret_addr); 74 | jmp(rax); 75 | } 76 | } xbyakCode{ uintptr_t(update_node_pos), ret_addr }; 77 | 78 | FenixUtils::add_trampoline<5, 42586, 0x2c1>(&xbyakCode); // SkyrimSE.exe+733F81 79 | } 80 | 81 | _TESObjectREFR__SetPosition_140296910 = 82 | trmpl.write_call<5>(REL::ID(42586).address() + 0x2db, UpdatePos); // SkyrimSE.exe+733F9B 83 | _Projectile__SetRotation = trmpl.write_call<5>(REL::ID(42586).address() + 0x249, UpdateRot); // SkyrimSE.exe+733F09 84 | _matrix_mul = trmpl.write_call<5>(REL::ID(42586).address() + 0x212, matrix_mul); 85 | } 86 | 87 | private: 88 | static RE::NiMatrix3* matrix_mul(RE::NiMatrix3* A, RE::NiMatrix3* ans, RE::NiMatrix3* B) 89 | { 90 | auto proj = (RE::Projectile*)((char*)A - 0xA8); 91 | if (allows_multiple_beams(proj)) { 92 | auto node = proj->Get3D2(); 93 | return &node->local.rotate; 94 | } else { 95 | return _matrix_mul(A, ans, B); 96 | } 97 | } 98 | 99 | static bool NewBeam(uint32_t* handle, RE::Projectile** proj) 100 | { 101 | auto found = _RefHandle__get(handle, proj); 102 | if (!found || !*proj) 103 | return found; 104 | 105 | return !allows_multiple_beams(*proj); 106 | } 107 | 108 | static void update_node_pos(float x, float y, float z, RE::Projectile* proj) 109 | { 110 | if (auto node = proj->Get3D()) { 111 | if (!allows_multiple_beams(proj)) { 112 | node->local.translate.x = x; 113 | node->local.translate.y = y; 114 | node->local.translate.z = z; 115 | } 116 | } 117 | } 118 | 119 | static void UpdatePos(RE::Projectile* proj, RE::NiPoint3* pos) 120 | { 121 | if (!allows_multiple_beams(proj)) { 122 | _TESObjectREFR__SetPosition_140296910(proj, pos); 123 | } 124 | } 125 | 126 | static void UpdateRot(RE::Projectile* proj, float rot_X) 127 | { 128 | if (!allows_multiple_beams(proj)) { 129 | _Projectile__SetRotation(proj, rot_X); 130 | } 131 | } 132 | 133 | static inline REL::Relocation _RefHandle__get; 134 | static inline REL::Relocation _TESObjectREFR__SetPosition_140296910; 135 | static inline REL::Relocation _Projectile__SetRotation; 136 | static inline REL::Relocation _matrix_mul; 137 | }; 138 | 139 | // Detach instant beams from magic node 140 | class NormLightingsHook 141 | { 142 | public: 143 | static void Hook() 144 | { 145 | _BeamProjectile__ctor = 146 | SKSE::GetTrampoline().write_call<5>(REL::ID(42928).address() + 0x185, Ctor); // SkyrimSE.exe+74B2F5 147 | } 148 | 149 | private: 150 | static RE::BeamProjectile* Ctor(RE::BeamProjectile* proj, RE::Projectile::LaunchData* ldata) 151 | { 152 | if (allows_detach_beam(ldata->spell)) { 153 | ldata->useOrigin = true; 154 | ldata->autoAim = false; 155 | } 156 | 157 | return _BeamProjectile__ctor(proj, ldata); 158 | } 159 | 160 | static inline REL::Relocation _BeamProjectile__ctor; 161 | }; 162 | } 163 | -------------------------------------------------------------------------------- /src/JsonUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "JsonUtils.h" 2 | 3 | namespace JsonUtils 4 | { 5 | uint32_t get_formid(const std::string& filename, const std::string& name) 6 | { 7 | if (name.starts_with("key_")) { 8 | return FormIDsMap::get(filename, name); 9 | } else { 10 | return FenixUtils::Json::get_formid(name); 11 | } 12 | } 13 | 14 | void FormIDsMap::init(const std::string& filename, const Json::Value& json_root) 15 | { 16 | if (!json_root.isMember("FormIDs")) 17 | return; 18 | 19 | const auto& formids = json_root["FormIDs"]; 20 | for (auto& key : formids.getMemberNames()) { 21 | formIDs.insert({ filename + key, FenixUtils::Json::get_formid(formids[key].asString()) }); 22 | } 23 | } 24 | 25 | void FormIDsMap::clear() { formIDs.clear(); } 26 | } 27 | -------------------------------------------------------------------------------- /src/JsonUtils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "json/json.h" 6 | #include "magic_enum.hpp" 7 | #include "RE/P/Projectile.h" 8 | 9 | namespace JsonUtils 10 | { 11 | using namespace FenixUtils::Json; 12 | 13 | uint32_t get_formid(const std::string& filename, const std::string& name); 14 | 15 | class FormIDsMap 16 | { 17 | static inline std::unordered_map formIDs; 18 | 19 | public: 20 | static void init(const std::string& filename, const Json::Value& json_root); 21 | static void clear(); 22 | 23 | // `key` must present and starts with "key_" 24 | static auto get(const std::string& filename, const std::string& key) 25 | { 26 | auto found = formIDs.find(filename + key); 27 | assert(found != formIDs.end()); 28 | return found->second; 29 | } 30 | }; 31 | 32 | // Stores map key_... -> 1 ... 33 | class KeysMap 34 | { 35 | std::unordered_map keys; 36 | 37 | public: 38 | void clear() { keys.clear(); } 39 | 40 | // `key` must present and starts with "key_" 41 | auto get(const std::string& filename, const std::string& key) 42 | { 43 | auto found = keys.find(filename + key); 44 | assert(found != keys.end()); 45 | return (*found).second; 46 | } 47 | 48 | uint32_t add(const std::string& filename, const std::string& key) 49 | { 50 | auto finalkey = filename + key; 51 | auto found = keys.find(finalkey); 52 | assert(found == keys.end()); 53 | uint32_t new_key = static_cast(keys.size()) + 1; 54 | keys.insert({ finalkey, new_key }); 55 | return new_key; 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/Multicast.cpp: -------------------------------------------------------------------------------- 1 | #include "JsonUtils.h" 2 | #include "TriggerFunctions.h" 3 | #include "Triggers.h" 4 | #include "Homing.h" 5 | #include "Positioning.h" 6 | #include 7 | 8 | namespace Multicast 9 | { 10 | using ProjectileRot = RE::Projectile::ProjectileRot; 11 | 12 | using Positioning::Shape; 13 | 14 | // Projectiles already placed to cast. Determine SP initial direction. 15 | enum class LaunchDir : uint32_t 16 | { 17 | Parallel, 18 | ToSight, 19 | ToCenter, 20 | FromCenter, 21 | 22 | // TODO: implement 23 | ToTarget 24 | }; 25 | 26 | enum class SoundType : uint32_t 27 | { 28 | Every, 29 | Single, 30 | None 31 | }; 32 | 33 | // A blueprint for setting projectiles 34 | struct SpawnGroupData 35 | { 36 | Positioning::Pattern pattern; // 00 37 | 38 | // "NPC R UpperArm [RUar]" and "NPC L UpperArm [LUar]" is cool yeah. 39 | RE::NiPoint3 pos_rnd; // 30 rnd offset for every individual proj 40 | ProjectileRot rot_offset; // 3C offset of SP rotation from actual cast rotation 41 | ProjectileRot rot_rnd; // 44 rnd rotation offset for every individual proj 42 | 43 | LaunchDir rot: 3; // 48:00 44 | SoundType sound: 2; // 48:03 45 | uint32_t rotation_target: 27; // 48:05 used if rotation == ToTarget 46 | 47 | SpawnGroupData(const std::string& filename, const Json::Value& item) : 48 | pattern(item["Pattern"]), rot(JsonUtils::mb_read_field(item, "rotation")), 49 | sound(JsonUtils::mb_read_field(item, "sound")), pos_rnd(JsonUtils::mb_getPoint3(item, "posRnd")), 50 | rot_offset(JsonUtils::mb_getPoint2(item, "rotOffset")), rot_rnd(JsonUtils::mb_getPoint2(item, "rotRnd")), 51 | rotation_target( 52 | rot == LaunchDir::ToTarget ? Homing::get_key_ind(filename, JsonUtils::getString(item, "rotationTarget")) : 0) 53 | {} 54 | }; 55 | static_assert(sizeof(SpawnGroupData) == 0x50); 56 | 57 | struct SpawnGroupStorage 58 | { 59 | static void clear_keys() 60 | { 61 | keys.clear(); 62 | } 63 | static void clear() 64 | { 65 | clear_keys(); 66 | data.clear(); 67 | } 68 | 69 | static void init(const std::string& filename, const Json::Value& SpawnGroups) 70 | { 71 | for (auto& key : SpawnGroups.getMemberNames()) { 72 | read_json_entry(filename, key, SpawnGroups[key]); 73 | } 74 | } 75 | 76 | static void init_keys(const std::string& filename, const Json::Value& SpawnGroups) 77 | { 78 | for (auto& key : SpawnGroups.getMemberNames()) { 79 | read_json_entry_keys(filename, key, SpawnGroups[key]); 80 | } 81 | } 82 | 83 | static const auto& get_data(uint32_t ind) { return data[ind - 1]; } 84 | 85 | static uint32_t get_key_ind(const std::string& filename, const std::string& key) { return keys.get(filename, key); } 86 | 87 | private: 88 | static void read_json_entry(const std::string& filename, const std::string& key, const Json::Value& item) 89 | { 90 | [[maybe_unused]] uint32_t ind = get_key_ind(filename, key); 91 | assert(ind == data.size() + 1); 92 | 93 | data.emplace_back(filename, item); 94 | } 95 | 96 | static void read_json_entry_keys(const std::string& filename, const std::string& key, const Json::Value&) 97 | { 98 | keys.add(filename, key); 99 | } 100 | 101 | static inline JsonUtils::KeysMap keys; 102 | static inline std::vector data; 103 | }; 104 | 105 | struct SpellData 106 | { 107 | uint32_t spellID; // -1 = Current 108 | }; 109 | 110 | struct ArrowData 111 | { 112 | uint32_t weapID; // -1 = Current 113 | uint32_t arrowID; // -1 = Current 114 | }; 115 | 116 | struct SpellArrowData 117 | { 118 | std::variant data; 119 | 120 | static uint32_t currentOrID(const std::string& filename, const Json::Value& item, const std::string& field) 121 | { 122 | auto spellID = JsonUtils::getString(item, field); 123 | if (spellID == "Current") 124 | return CURRENT; 125 | else 126 | return JsonUtils::get_formid(filename, spellID); 127 | } 128 | 129 | static constexpr uint32_t CURRENT = static_cast(-1); 130 | }; 131 | 132 | enum class HomingDetectionType : uint32_t 133 | { 134 | Individual, 135 | Evenly 136 | }; 137 | 138 | struct Data 139 | { 140 | Data(SpellArrowData origin_formIDs, TriggerFunctions::Functions functions, uint32_t pattern_ind, 141 | HomingDetectionType homing_setting, bool call_triggers) : 142 | origin_formIDs(std::move(origin_formIDs)), 143 | functions(std::move(functions)), pattern_ind(pattern_ind), homing_setting(homing_setting), 144 | call_triggers(call_triggers) 145 | {} 146 | 147 | SpellArrowData origin_formIDs; 148 | TriggerFunctions::Functions functions; 149 | uint32_t pattern_ind; // for SpawnGroupData 150 | HomingDetectionType homing_setting: 3; // if new type is homing, how to chose targets 151 | uint32_t call_triggers: 1; 152 | }; 153 | 154 | struct Storage 155 | { 156 | static void clear_keys() { keys.clear(); } 157 | static void clear() 158 | { 159 | clear_keys(); 160 | data.clear(); 161 | } 162 | 163 | static void init(const std::string& filename, const Json::Value& MulticastData) 164 | { 165 | for (auto& key : MulticastData.getMemberNames()) { 166 | read_json_entry(filename, key, MulticastData[key]); 167 | } 168 | } 169 | 170 | static void init_keys(const std::string& filename, const Json::Value& MulticastData) 171 | { 172 | for (auto& key : MulticastData.getMemberNames()) { 173 | read_json_entry_keys(filename, key, MulticastData[key]); 174 | } 175 | } 176 | 177 | static const auto& get_data(uint32_t ind) { return data[ind - 1]; } 178 | 179 | static uint32_t get_key_ind(const std::string& filename, const std::string& key) { return keys.get(filename, key); } 180 | 181 | private: 182 | static void read_json_entry(const std::string& filename, const std::string& key, const Json::Value& item) 183 | { 184 | [[maybe_unused]] uint32_t ind = get_key_ind(filename, key); 185 | assert(ind == data.size() + 1); 186 | 187 | data.push_back(std::vector()); 188 | auto& new_data = data.back(); 189 | 190 | for (int i = 0; i < (int)item.size(); i++) { 191 | read_json_entry_item(filename, new_data, item[i]); 192 | } 193 | } 194 | 195 | static void read_json_entry_keys(const std::string& filename, const std::string& key, const Json::Value&) 196 | { 197 | keys.add(filename, key); 198 | } 199 | 200 | static void read_json_entry_item(const std::string& filename, std::vector& new_data, const Json::Value& item) 201 | { 202 | SpellArrowData origin_formIDs; 203 | if (item.isMember("spellID")) { 204 | origin_formIDs.data = SpellData{}; 205 | auto& spelldata = std::get(origin_formIDs.data); 206 | 207 | spelldata.spellID = SpellArrowData::currentOrID(filename, item, "spellID"); 208 | } else if (item.isMember("weapID")) { 209 | origin_formIDs.data = ArrowData{}; 210 | auto& arrowdata = std::get(origin_formIDs.data); 211 | 212 | arrowdata.weapID = SpellArrowData::currentOrID(filename, item, "weapID"); 213 | 214 | if (item.isMember("arrowID")) { 215 | arrowdata.arrowID = SpellArrowData::currentOrID(filename, item, "arrowID"); 216 | } else { 217 | arrowdata.arrowID = SpellArrowData::CURRENT; 218 | } 219 | } else { 220 | assert(false); 221 | } 222 | 223 | TriggerFunctions::Functions functions; 224 | HomingDetectionType homing_detection = 225 | JsonUtils::mb_read_field(item, "HomingDetection"); 226 | 227 | if (item.isMember("TriggerFunctions")) { 228 | functions = TriggerFunctions::Functions(filename, item["TriggerFunctions"]); 229 | } 230 | 231 | auto pattern_ind = SpawnGroupStorage::get_key_ind(filename, item["spawn_group"].asString()); 232 | auto call_triggers = JsonUtils::mb_read_field(item, "callTriggers"); 233 | 234 | new_data.emplace_back(origin_formIDs, functions, pattern_ind, homing_detection, call_triggers); 235 | } 236 | 237 | static inline JsonUtils::KeysMap keys; 238 | static inline std::vector> data; 239 | }; 240 | 241 | uint32_t get_key_ind(const std::string& filename, const std::string& key) { return Storage::get_key_ind(filename, key); } 242 | 243 | namespace Sounds 244 | { 245 | RE::BGSSoundDescriptorForm* EffectSetting__get_sndr(RE::EffectSetting* a1, RE::MagicSystem::SoundID sid) 246 | { 247 | return _generic_foo_<11001, decltype(EffectSetting__get_sndr)>::eval(a1, sid); 248 | } 249 | 250 | void PlaySound_func3_140BEDB10(RE::BSSoundHandle* a1, RE::NiAVObject* source_node) 251 | { 252 | return _generic_foo_<66375, decltype(PlaySound_func3_140BEDB10)>::eval(a1, source_node); 253 | } 254 | 255 | char set_sound_position(RE::BSSoundHandle* shandle, float x, float y, float z) 256 | { 257 | return _generic_foo_<66370, decltype(set_sound_position)>::eval(shandle, x, y, z); 258 | } 259 | 260 | RE::BSSoundHandle tmpsound; 261 | 262 | void prepare(RE::BSSoundHandle& shandle, RE::MagicItem* spel, RE::TESObjectREFR* caster) 263 | { 264 | auto sid = RE::MagicSystem::SoundID::kRelease; 265 | auto eff = FenixUtils::getAVEffectSetting(spel); 266 | auto sndr = EffectSetting__get_sndr(eff, sid); 267 | // Release 268 | _generic_foo_<66382, bool(RE::BSSoundHandle&)>::eval(shandle); 269 | RE::BSAudioManager::GetSingleton()->BuildSoundDataFromDescriptor(shandle, sndr, 0); 270 | shandle.SetObjectToFollow(caster->Get3D()); 271 | } 272 | 273 | void play_cast_sound(RE::TESObjectREFR* caster, RE::MagicItem* spel, const RE::NiPoint3& start_pos) 274 | { 275 | RE::BSSoundHandle shandle; 276 | 277 | auto sid = RE::MagicSystem::SoundID::kRelease; 278 | if (auto eff = FenixUtils::getAVEffectSetting(spel)) { 279 | if (auto sndr = EffectSetting__get_sndr(eff, sid)) { 280 | RE::BSAudioManager::GetSingleton()->BuildSoundDataFromDescriptor(shandle, sndr, 16); 281 | 282 | //shandle.SetPosition(); 283 | //const auto& start_pos = caster->GetPosition(); 284 | if (_generic_foo_<66370, bool(RE::BSSoundHandle&, float x, float y, float z)>::eval(shandle, start_pos.x, 285 | start_pos.y, start_pos.z)) { 286 | shandle.SetObjectToFollow(caster->Get3D()); 287 | if (shandle.Play()) 288 | logger::info("Q"); 289 | } 290 | } 291 | } 292 | } 293 | 294 | void play_cast_sound__(RE::TESObjectREFR* caster, RE::MagicItem* spel, const RE::NiPoint3& start_pos) 295 | { 296 | //RE::BSSoundHandle shandle; 297 | RE::BSSoundHandle& shandle = tmpsound; 298 | if (!shandle.IsValid()) { 299 | prepare(shandle, spel, caster); 300 | } 301 | 302 | if (shandle.IsValid()) { 303 | if (shandle.IsPlaying()) 304 | shandle.Stop(); 305 | //shandle.SetPosition(); 306 | const auto& start_pos_ = RE::PlayerCharacter::GetSingleton()->GetPosition(); 307 | _generic_foo_<66382, bool(RE::BSSoundHandle&, float x, float y, float z)>::eval(shandle, start_pos_.x, 308 | start_pos_.y, start_pos_.z); 309 | start_pos; 310 | shandle.Play(); 311 | } 312 | } 313 | 314 | void play_cast_sound_(RE::TESObjectREFR* caster, RE::MagicItem* spel, const RE::NiPoint3& start_pos) 315 | { 316 | if (auto root = caster->Get3D2()) { 317 | RE::BSSoundHandle shandle; 318 | auto sid = RE::MagicSystem::SoundID::kRelease; 319 | if (auto eff = FenixUtils::getAVEffectSetting(spel)) { 320 | if (auto sndr = EffectSetting__get_sndr(eff, sid)) { 321 | RE::BSAudioManager::GetSingleton()->BuildSoundDataFromDescriptor(shandle, sndr, 0); 322 | if (shandle.IsValid()) { 323 | PlaySound_func3_140BEDB10(&shandle, root); 324 | 325 | set_sound_position(&shandle, start_pos.x, start_pos.y, start_pos.z); 326 | shandle.Play(); 327 | } 328 | } 329 | } 330 | } 331 | } 332 | } 333 | 334 | namespace Casting 335 | { 336 | struct CastData 337 | { 338 | ProjectileRot parallel_rot; 339 | RE::NiPoint3 start_pos; 340 | 341 | struct SpellData 342 | { 343 | RE::MagicItem* spel; 344 | }; 345 | 346 | struct ArrowData 347 | { 348 | RE::TESObjectWEAP* weap; 349 | RE::TESAmmo* ammo; 350 | }; 351 | 352 | std::variant spellarrow_data; 353 | }; 354 | 355 | namespace Rotation 356 | { 357 | float add_rot_x(float val, float d) 358 | { 359 | const float PI = 3.1415926f; 360 | // -pi/2..pi/2 361 | d = d * PI / 180.0f; 362 | val += d; 363 | val = std::max(val, -PI / 2); 364 | val = std::min(val, PI / 2); 365 | return val; 366 | } 367 | 368 | float add_rot_z(float val, float d) 369 | { 370 | const float PI = 3.1415926f; 371 | // -pi/2..pi/2 372 | d = d * PI / 180.0f; 373 | val += d; 374 | while (val < 0) val += 2 * PI; 375 | while (val > 2 * PI) val -= 2 * PI; 376 | return val; 377 | } 378 | 379 | auto add_rot(ProjectileRot rot, ProjectileRot delta) 380 | { 381 | rot.x = add_rot_x(rot.x, delta.x); 382 | rot.z = add_rot_z(rot.z, delta.z); 383 | return rot; 384 | } 385 | 386 | auto add_rot_rnd(ProjectileRot rot, ProjectileRot rnd) 387 | { 388 | using FenixUtils::Random::FloatNeg1To1; 389 | 390 | if (rnd.x == 0 && rnd.z == 0) 391 | return rot; 392 | 393 | rot.x = add_rot_x(rot.x, rnd.x * FloatNeg1To1()); 394 | rot.z = add_rot_z(rot.z, rnd.z * FloatNeg1To1()); 395 | return rot; 396 | } 397 | 398 | auto add_point_rnd(const RE::NiPoint3& rnd) 399 | { 400 | using FenixUtils::Random::FloatNeg1To1; 401 | 402 | if (rnd.x == 0 && rnd.y == 0 && rnd.z == 0) 403 | return RE::NiPoint3{ 0, 0, 0 }; 404 | 405 | return RE::NiPoint3{ rnd.x * FloatNeg1To1(), rnd.y * FloatNeg1To1(), rnd.z * FloatNeg1To1() }; 406 | } 407 | } 408 | 409 | auto get_SPItem_rot(LaunchDir rot, const RE::NiPoint3& item_pos, const RE::NiPoint3& SP_center, 410 | const RE::NiPoint3& cast_dir, RE::TESObjectREFR* caster, RE::TESObjectREFR* target) 411 | { 412 | using FenixUtils::Geom::rot_at; 413 | 414 | switch (rot) { 415 | case LaunchDir::ToTarget: 416 | { 417 | if (target) 418 | return rot_at(item_pos, target->As() ? 419 | FenixUtils::Geom::Actor::AnticipatePos(target->As()) : 420 | target->GetPosition()); 421 | else 422 | break; 423 | } 424 | case LaunchDir::FromCenter: 425 | return rot_at(SP_center, item_pos); 426 | case LaunchDir::ToCenter: 427 | return rot_at(item_pos, SP_center); 428 | case LaunchDir::ToSight: 429 | if (caster->As()) 430 | return rot_at(item_pos, FenixUtils::Geom::Actor::raycast(caster->As())); 431 | else 432 | break; 433 | case LaunchDir::Parallel: 434 | default: 435 | break; 436 | } 437 | 438 | return rot_at(cast_dir); 439 | } 440 | 441 | // Given cur_proj pos, SP's CD 442 | // 1. Determine rotation 443 | // 1. Initial rotation determined right after blueprint constructed 444 | // 2. Added rot_offset 445 | // 3. Added rot_rnd 446 | // 2. Add rnd_offset to pos 447 | // 3. Launch the proj either as spell or as arrow 448 | auto multiCastGroupItem(RE::NiPoint3 pos, const Data& data, const CastData& SP_CD, bool withSound, 449 | const RE::NiPoint3& cast_dir, RE::TESObjectREFR* caster, RE::TESObjectREFR* target) 450 | { 451 | auto& pattern_data = SpawnGroupStorage::get_data(data.pattern_ind); 452 | 453 | ProjectileRot item_rot = get_SPItem_rot(pattern_data.rot, pos, SP_CD.start_pos, cast_dir, caster, target); 454 | 455 | item_rot = Rotation::add_rot(item_rot, pattern_data.rot_offset); 456 | item_rot = Rotation::add_rot_rnd(item_rot, pattern_data.rot_rnd); 457 | 458 | RE::NiPoint3 rnd_offset = Rotation::add_point_rnd(pattern_data.pos_rnd); 459 | rnd_offset = pattern_data.pattern.rotateDependsX(rnd_offset, SP_CD.parallel_rot); 460 | pos += rnd_offset; 461 | 462 | auto type = SP_CD.spellarrow_data.index(); 463 | RE::ProjectileHandle handle; 464 | // SpellData 465 | if (type == 0) { 466 | auto spel = std::get(SP_CD.spellarrow_data).spel->As(); 467 | assert(spel); 468 | 469 | RE::Projectile::LaunchSpell(&handle, caster, spel, pos, item_rot); 470 | 471 | if (withSound) { 472 | if (auto proj = handle.get().get()) 473 | Sounds::play_cast_sound(proj, spel, pos); 474 | } 475 | } 476 | // ArrowData 477 | if (type == 1) { 478 | auto& arrow_data = std::get(SP_CD.spellarrow_data); 479 | RE::Projectile::LaunchArrow(&handle, caster, arrow_data.ammo, arrow_data.weap, pos, item_rot); 480 | 481 | if (auto proj = handle.get().get()) { 482 | if (proj->power > 0) { 483 | proj->weaponDamage /= proj->power; 484 | proj->power = 1.0f; 485 | proj->weaponDamage *= proj->power; 486 | } 487 | 488 | // TODO: sound 489 | } 490 | } 491 | 492 | return handle; 493 | } 494 | 495 | // SP_CD has info about cast. Copied, because every SP has info itself. 496 | void multiCastGroup(CastData SP_CD, const Data& data, RE::TESObjectREFR* origin, RE::TESObjectREFR* caster) 497 | { 498 | const auto& spellarrow_data = data.origin_formIDs; 499 | 500 | auto type = spellarrow_data.data.index(); 501 | // SpellData 502 | if (type == 0) { 503 | auto& spell_data = std::get(spellarrow_data.data); 504 | auto spell_id = spell_data.spellID; 505 | 506 | assert(SP_CD.spellarrow_data.index() == 0 || spell_id != SpellArrowData::CURRENT); 507 | 508 | if (SP_CD.spellarrow_data.index() != 0) 509 | SP_CD.spellarrow_data = CastData::SpellData{}; 510 | 511 | if (spell_id != SpellArrowData::CURRENT) 512 | std::get(SP_CD.spellarrow_data).spel = RE::TESForm::LookupByID(spell_id); 513 | } 514 | // ArrowData 515 | if (type == 1) { 516 | auto& arrow_data = std::get(spellarrow_data.data); 517 | 518 | auto arrow_id = arrow_data.arrowID; 519 | auto weap_id = arrow_data.weapID; 520 | 521 | assert(SP_CD.spellarrow_data.index() == 1 || 522 | (weap_id != SpellArrowData::CURRENT && arrow_id != SpellArrowData::CURRENT)); 523 | 524 | if (SP_CD.spellarrow_data.index() != 1) 525 | SP_CD.spellarrow_data = CastData::ArrowData{}; 526 | 527 | auto& arrowdata = std::get(SP_CD.spellarrow_data); 528 | if (weap_id != SpellArrowData::CURRENT) { 529 | arrowdata.weap = RE::TESForm::LookupByID(weap_id); 530 | } 531 | if (arrow_id != SpellArrowData::CURRENT) { 532 | arrowdata.ammo = RE::TESForm::LookupByID(arrow_id); 533 | } 534 | } 535 | 536 | auto& pattern_data = SpawnGroupStorage::get_data(data.pattern_ind); 537 | 538 | pattern_data.pattern.initCenter(SP_CD.start_pos, SP_CD.parallel_rot, origin); 539 | RE::NiPoint3 cast_dir = pattern_data.pattern.getCastDir(SP_CD.parallel_rot); 540 | cast_dir.Unitize(); 541 | 542 | // Homing::Evenly and ToTarget support 543 | std::vector targets; 544 | uint32_t homingInd = pattern_data.rotation_target; 545 | if (!homingInd) 546 | homingInd = data.functions.get_homing_ind(false); 547 | 548 | if (!homingInd) 549 | homingInd = data.functions.get_homing_ind(true); 550 | 551 | // homingInd may be 0 for ToTarget 552 | if (homingInd) { 553 | targets = Homing::get_targets(homingInd, caster, SP_CD.start_pos); 554 | 555 | std::random_device rd; 556 | std::mt19937 g(rd()); 557 | std::shuffle(targets.begin(), targets.end(), g); 558 | } 559 | 560 | bool needsound_every = pattern_data.sound == SoundType::Every; 561 | bool needsound_single = type == 0 && pattern_data.sound == SoundType::Single; 562 | size_t target_ind = 0; 563 | 564 | Positioning::Plane plane(SP_CD.start_pos, cast_dir); 565 | for (size_t i = 0; i < pattern_data.pattern.getSize(); i++) { 566 | auto point = pattern_data.pattern.GetPosition(plane, cast_dir, i); 567 | 568 | RE::Actor* target = nullptr; 569 | 570 | if (targets.size()) { 571 | target = targets[target_ind++]; 572 | if (target_ind >= targets.size()) 573 | target_ind = 0; 574 | } 575 | 576 | auto handle = multiCastGroupItem(point, data, SP_CD, needsound_every || needsound_single && i == 0, cast_dir, 577 | caster, target); 578 | 579 | if (auto proj = handle.get().get()) { 580 | if (data.call_triggers) { 581 | Triggers::Data ldata(proj); 582 | Triggers::eval(&ldata, Triggers::Event::ProjAppeared, proj, target); 583 | } 584 | 585 | data.functions.call(proj, target); 586 | } 587 | } 588 | } 589 | } 590 | 591 | void apply(Triggers::Data* ldata, uint32_t ind) 592 | { 593 | using namespace Casting; 594 | 595 | CastData current_CD; 596 | current_CD.start_pos = ldata->pos; 597 | current_CD.parallel_rot = ldata->rot; 598 | switch (ldata->type) { 599 | case Triggers::Data::Type::Arrow: 600 | { 601 | assert(ldata->weap); 602 | assert(ldata->ammo); 603 | current_CD.spellarrow_data = CastData::ArrowData{}; 604 | auto& arrow_data = std::get(current_CD.spellarrow_data); 605 | arrow_data.weap = ldata->weap; 606 | arrow_data.ammo = ldata->ammo; 607 | break; 608 | } 609 | case Triggers::Data::Type::Spell: 610 | { 611 | assert(ldata->spel); 612 | current_CD.spellarrow_data = CastData::SpellData{}; 613 | auto& spell_data = std::get(current_CD.spellarrow_data); 614 | spell_data.spel = ldata->spel; 615 | break; 616 | } 617 | case Triggers::Data::Type::None: 618 | default: 619 | // "Current" disallowed 620 | current_CD.spellarrow_data = CastData::SpellData{}; 621 | break; 622 | } 623 | 624 | auto& data = Storage::get_data(ind); 625 | for (const auto& spawn_data : data) { 626 | multiCastGroup(current_CD, spawn_data, ldata->shooter, ldata->shooter); 627 | } 628 | } 629 | 630 | void install() {} 631 | 632 | void clear_keys() 633 | { 634 | SpawnGroupStorage::clear_keys(); 635 | Storage::clear_keys(); 636 | } 637 | void clear() 638 | { 639 | SpawnGroupStorage::clear(); 640 | Storage::clear(); 641 | } 642 | 643 | void init(const std::string& filename, const Json::Value& json_root) 644 | { 645 | if (json_root.isMember("MulticastSpawnGroups")) { 646 | SpawnGroupStorage::init(filename, json_root["MulticastSpawnGroups"]); 647 | } 648 | if (json_root.isMember("MulticastData")) { 649 | Storage::init(filename, json_root["MulticastData"]); 650 | } 651 | } 652 | 653 | void init_keys(const std::string& filename, const Json::Value& json_root) 654 | { 655 | if (json_root.isMember("MulticastSpawnGroups")) { 656 | SpawnGroupStorage::init_keys(filename, json_root["MulticastSpawnGroups"]); 657 | } 658 | if (json_root.isMember("MulticastData")) { 659 | Storage::init_keys(filename, json_root["MulticastData"]); 660 | } 661 | } 662 | } 663 | -------------------------------------------------------------------------------- /src/Multicast.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct Ldata; 4 | 5 | namespace Multicast 6 | { 7 | void apply(Triggers::Data* ldata, uint32_t ind); 8 | void install(); 9 | void init(const std::string& filename, const Json::Value& json_root); 10 | void clear(); 11 | void clear_keys(); 12 | void init_keys(const std::string& filename, const Json::Value& json_root); 13 | uint32_t get_key_ind(const std::string& filename, const std::string& key); 14 | } 15 | -------------------------------------------------------------------------------- /src/PCH.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | //#define DEBUG 4 | 5 | #pragma warning(push) 6 | #pragma warning(disable: 4702) 7 | #include "RE/Skyrim.h" 8 | #include "SKSE/SKSE.h" 9 | #pragma warning(pop) 10 | 11 | #pragma warning(push) 12 | #ifdef DEBUG 13 | # include 14 | #else 15 | # include 16 | #endif 17 | #pragma warning(pop) 18 | 19 | using namespace std::literals; 20 | 21 | namespace logger = SKSE::log; 22 | 23 | #define DLLEXPORT __declspec(dllexport) 24 | 25 | #include "Version.h" 26 | #ifdef USELESS_UTILS 27 | # include "UselessFenixUtils.h" 28 | #endif 29 | -------------------------------------------------------------------------------- /src/Positioning.cpp: -------------------------------------------------------------------------------- 1 | #include "Positioning.h" 2 | 3 | namespace Positioning 4 | { 5 | RE::NiPoint3 Pattern::GetPosition_Single(const Plane& plane, size_t) const { return plane.startPos; } 6 | RE::NiPoint3 Pattern::GetPosition_Line(const Plane& plane, size_t ind) const 7 | { 8 | if (count == 1) { 9 | return plane.startPos; 10 | } 11 | 12 | auto from = plane.startPos - plane.right_dir * (size * 0.5f); 13 | float d = size / (count - 1); 14 | return from + (plane.right_dir * (d * ind)); 15 | } 16 | RE::NiPoint3 Pattern::GetPosition_Circle(const Plane& plane, size_t ind) const 17 | { 18 | float alpha = 2 * 3.1415926f / count * ind; 19 | return plane.startPos + (plane.right_dir * cos(alpha) + plane.up_dir * sin(alpha)) * size; 20 | } 21 | RE::NiPoint3 Pattern::GetPosition_HalfCircle(const Plane& plane, size_t ind) const 22 | { 23 | if (count == 1) { 24 | return plane.startPos; 25 | } 26 | 27 | float alpha = 3.1415926f / (count - 1) * ind; 28 | return plane.startPos + (plane.right_dir * cos(alpha) + plane.up_dir * sin(alpha)) * size; 29 | } 30 | RE::NiPoint3 Pattern::GetPosition_FillSquare(const Plane& plane, size_t _ind) const 31 | { 32 | if (count == 1) { 33 | return plane.startPos; 34 | } 35 | 36 | uint32_t m = static_cast(sqrt(count)); 37 | uint32_t rest = count - m * m; 38 | bool has_right = rest >= m; 39 | bool has_up = rest != 0 && rest != m; 40 | 41 | uint32_t w = has_right ? m + 1 : m; 42 | uint32_t h = has_up ? m + 1 : m; 43 | 44 | float dx = size / (w - 1); 45 | float dy = h == 1 ? 0 : size / (h - 1); 46 | 47 | uint32_t ind = _ind % count; 48 | 49 | if (ind < w * m) { 50 | uint32_t x = ind % w; 51 | uint32_t y = ind / w; 52 | 53 | auto from = plane.startPos - (plane.right_dir + plane.up_dir) * (size * 0.5f); 54 | return from + plane.right_dir * (dx * x) + plane.up_dir * (dy * y); 55 | } else { 56 | ind -= w * m; 57 | uint32_t up_size = rest >= m ? rest - m : rest; 58 | 59 | uint32_t x = ind; 60 | auto from = plane.startPos - plane.right_dir * ((up_size - 1) * 0.5f * dx) - plane.up_dir * (size * 0.5f - dy * m); 61 | return from + plane.right_dir * (dx * x); 62 | } 63 | } 64 | RE::NiPoint3 Pattern::GetPosition_FillCircle(const Plane& plane, size_t ind) const 65 | { 66 | float c = size / sqrtf(static_cast(count)); 67 | auto alpha = 2.3999632297286533222f * ind; 68 | float r = c * sqrtf(static_cast(ind)); 69 | 70 | return plane.startPos + (plane.right_dir * cos(alpha) + plane.up_dir * sin(alpha)) * r; 71 | } 72 | RE::NiPoint3 Pattern::GetPosition_FillHalfCircle(const Plane& plane, size_t ind) const 73 | { 74 | float c = size / sqrtf(static_cast(count)); 75 | float alpha = 0.5f * 2.3999632297286533222f * ind; 76 | const float pi = 3.141592653589793f; 77 | while (alpha >= 2 * pi) 78 | alpha -= 2 * pi; 79 | if (alpha >= pi) 80 | alpha = alpha - pi; 81 | float r = c * sqrtf(static_cast(ind)); 82 | return plane.startPos + (plane.right_dir * cos(alpha) + plane.up_dir * sin(alpha)) * r; 83 | } 84 | RE::NiPoint3 Pattern::GetPosition_Sphere(const Plane& plane, size_t ind) const 85 | { 86 | if (count == 1) { 87 | return plane.startPos; 88 | } 89 | 90 | float c = size; 91 | float phi = 3.883222077450933f; 92 | float y = 1 - (ind / (count - 1.0f)) * 2; 93 | float radius = sqrt(1 - y * y); 94 | float theta = phi * ind; 95 | float x = cos(theta) * radius; 96 | float z = sin(theta) * radius; 97 | 98 | auto forward_dir = plane.up_dir.UnitCross(plane.right_dir); 99 | 100 | return plane.startPos + (plane.right_dir * x + plane.up_dir * z + forward_dir * y) * c; 101 | } 102 | RE::NiPoint3 Pattern::GetPosition_HalfSphere(const Plane& plane, size_t ind) const 103 | { 104 | if (count == 1) { 105 | return plane.startPos; 106 | } 107 | 108 | float c = size; 109 | float phi = 3.883222077450933f; 110 | float z = 1 - (ind / (count - 1.0f)); 111 | float radius = sqrt(1 - z * z); 112 | float theta = phi * ind; 113 | float x = cos(theta) * radius; 114 | float y = sin(theta) * radius; 115 | 116 | auto forward_dir = plane.up_dir.UnitCross(plane.right_dir); 117 | 118 | return plane.startPos + (plane.right_dir * x + plane.up_dir * z + forward_dir * y) * c; 119 | } 120 | RE::NiPoint3 Pattern::GetPosition_Cylinder(const Plane& plane, size_t ind) const 121 | { 122 | if (count == 1) { 123 | return plane.startPos; 124 | } 125 | 126 | float c = size; 127 | float phi = 3.883222077450933f; 128 | float y = 1 - (ind / (count - 1.0f)) * 2; 129 | float theta = phi * ind; 130 | float x = cos(theta); 131 | float z = sin(theta); 132 | 133 | auto forward_dir = plane.up_dir.UnitCross(plane.right_dir); 134 | 135 | return plane.startPos + (plane.right_dir * x + plane.up_dir * z + forward_dir * y) * c; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Positioning.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "JsonUtils.h" 4 | 5 | namespace Positioning 6 | { 7 | enum class Shape : uint32_t 8 | { 9 | Single, 10 | Line, 11 | Circle, 12 | HalfCircle, 13 | FillSquare, 14 | FillCircle, 15 | FillHalfCircle, 16 | Sphere, 17 | HalfSphere, 18 | Cylinder, 19 | 20 | Total 21 | }; 22 | 23 | struct Plane 24 | { 25 | RE::NiPoint3 startPos, right_dir, up_dir; 26 | 27 | Plane(RE::NiPoint3 startPos, const RE::NiPoint3& cast_dir) : startPos(std::move(startPos)) 28 | { 29 | right_dir = RE::NiPoint3(0, 0, -1).UnitCross(cast_dir); 30 | if (right_dir.SqrLength() == 0) 31 | right_dir = { 1, 0, 0 }; 32 | up_dir = right_dir.Cross(cast_dir); 33 | } 34 | 35 | RE::NiPoint2 project(const RE::NiPoint3& P) const { return { P.Dot(right_dir), P.Dot(up_dir) }; } 36 | 37 | RE::NiPoint3 unproject(const RE::NiPoint2& P) const { return right_dir * P.x + up_dir * P.y + startPos; } 38 | }; 39 | 40 | struct Pattern 41 | { 42 | explicit Pattern(const Json::Value& item) : 43 | origin(JsonUtils::mb_getString(item, "origin")), 44 | normal(JsonUtils::mb_getPoint3(item, "normal")), 45 | rotate_alpha(JsonUtils::mb_getFloat(item, "planeRotate") * 3.14159265358f / 180.0f), 46 | pos_offset(JsonUtils::mb_getPoint3(item, "posOffset")), 47 | normalDependsX(JsonUtils::mb_read_field(item, "xDepends")), 48 | shape(JsonUtils::read_enum(item["Figure"], "shape")), 49 | count(JsonUtils::mb_read_field<1u>(item["Figure"], "count")), 50 | size(shape != Shape::Single ? static_cast(JsonUtils::mb_read_field<0u>(item["Figure"], "size")) : 0) 51 | {} 52 | 53 | static RE::NiPoint3 rotateDependsX(const RE::NiPoint3& A, RE::Projectile::ProjectileRot parallel_rot, bool dependsX) 54 | { 55 | return FenixUtils::Geom::rotate(A, RE::NiPoint3(dependsX ? parallel_rot.x : 0, 0, parallel_rot.z)); 56 | } 57 | 58 | RE::NiPoint3 rotateDependsX(const RE::NiPoint3& A, RE::Projectile::ProjectileRot parallel_rot) const 59 | { 60 | return rotateDependsX(A, parallel_rot, normalDependsX); 61 | } 62 | 63 | // By default center is in getposition. 64 | // Use bone position if possible, as wel as shift it to pos_offset 65 | void initCenter(RE::NiPoint3& center, const RE::Projectile::ProjectileRot& rot, RE::TESObjectREFR* origin_refr) const 66 | { 67 | if (!origin.empty()) { 68 | auto root = origin_refr->Get3D1(origin_refr->IsPlayerRef() && !origin_refr->Is3rdPersonVisible()); 69 | if (auto bone = root->GetObjectByName(origin)) { 70 | center = bone->world.translate; 71 | } 72 | } 73 | center += rotateDependsX(pos_offset, rot); 74 | } 75 | 76 | // Get actual pattern direction, uses normal to rotate initial cast direction 77 | RE::NiPoint3 getCastDir(const RE::Projectile::ProjectileRot& parallel_rot) const 78 | { 79 | return rotateDependsX(normal, parallel_rot); 80 | } 81 | 82 | private: 83 | Shape shape: 4; 84 | uint32_t count: 27; 85 | uint32_t normalDependsX: 1; // 2C used for armageddon 86 | float size; 87 | RE::BSFixedString origin; // 08 node name of origin, getposition otherwise 88 | RE::NiPoint3 normal; // 10 determines a pane of SP 89 | float rotate_alpha; // 1C rotate everything along the plane normal 90 | RE::NiPoint3 pos_offset; // 20 offset of SP center from actual cast pos 91 | 92 | RE::NiPoint3 GetPosition_Single(const Plane& plane, size_t) const; 93 | RE::NiPoint3 GetPosition_Line(const Plane& plane, size_t ind) const; 94 | RE::NiPoint3 GetPosition_Circle(const Plane& plane, size_t ind) const; 95 | RE::NiPoint3 GetPosition_HalfCircle(const Plane& plane, size_t ind) const; 96 | RE::NiPoint3 GetPosition_FillSquare(const Plane& plane, size_t ind) const; 97 | RE::NiPoint3 GetPosition_FillCircle(const Plane& plane, size_t ind) const; 98 | RE::NiPoint3 GetPosition_FillHalfCircle(const Plane& plane, size_t ind) const; 99 | RE::NiPoint3 GetPosition_Sphere(const Plane& plane, size_t ind) const; 100 | RE::NiPoint3 GetPosition_HalfSphere(const Plane& plane, size_t ind) const; 101 | RE::NiPoint3 GetPosition_Cylinder(const Plane& plane, size_t) const; 102 | 103 | // Rotate point of the figure 104 | RE::NiPoint3 rotateFigure(const RE::NiPoint3& P, const RE::NiPoint3& O, const RE::NiPoint3& axis) const 105 | { 106 | if (rotate_alpha != 0.0f) { 107 | return FenixUtils::Geom::rotate(P, rotate_alpha, O, axis); 108 | } 109 | return P; 110 | } 111 | 112 | RE::NiPoint3 GetPosition_(const Plane& plane, size_t ind) const 113 | { 114 | switch (shape) { 115 | case Positioning::Shape::Line: 116 | return GetPosition_Line(plane, ind); 117 | case Positioning::Shape::Circle: 118 | return GetPosition_Circle(plane, ind); 119 | case Positioning::Shape::HalfCircle: 120 | return GetPosition_HalfCircle(plane, ind); 121 | case Positioning::Shape::FillSquare: 122 | return GetPosition_FillSquare(plane, ind); 123 | case Positioning::Shape::FillCircle: 124 | return GetPosition_FillCircle(plane, ind); 125 | case Positioning::Shape::FillHalfCircle: 126 | return GetPosition_FillHalfCircle(plane, ind); 127 | case Positioning::Shape::Sphere: 128 | return GetPosition_Sphere(plane, ind); 129 | case Positioning::Shape::HalfSphere: 130 | return GetPosition_HalfSphere(plane, ind); 131 | case Positioning::Shape::Cylinder: 132 | return GetPosition_Cylinder(plane, ind); 133 | case Positioning::Shape::Single: 134 | case Positioning::Shape::Total: 135 | default: 136 | return GetPosition_Single(plane, ind); 137 | } 138 | } 139 | 140 | public: 141 | RE::NiPoint3 GetPosition(const RE::NiPoint3& start_pos, const RE::NiPoint3& cast_dir, size_t ind) const 142 | { 143 | return rotateFigure(GetPosition_(Plane(start_pos, cast_dir), ind), start_pos, cast_dir); 144 | } 145 | 146 | RE::NiPoint3 GetPosition(const Plane& plane, const RE::NiPoint3& cast_dir, size_t ind) const 147 | { 148 | return rotateFigure(GetPosition_(plane, ind), plane.startPos, cast_dir); 149 | } 150 | 151 | std::vector GetPositions(const RE::NiPoint3& start_pos, const RE::NiPoint3& cast_dir) const 152 | { 153 | std::vector ans; 154 | Plane plane(start_pos, cast_dir); 155 | for (size_t i = 0; i < count; i++) { 156 | ans.push_back(GetPosition(plane, cast_dir, i)); 157 | } 158 | return ans; 159 | } 160 | 161 | bool xDepends() const { return normalDependsX; } 162 | 163 | uint32_t getSize() const { return count; } 164 | 165 | bool isShapeless() const { return shape == Shape::Single; } 166 | }; 167 | static_assert(sizeof(Pattern) == 0x30); 168 | } 169 | -------------------------------------------------------------------------------- /src/RuntimeData.cpp: -------------------------------------------------------------------------------- 1 | #include "RuntimeData.h" 2 | 3 | struct FenixProjsRuntimeData 4 | { 5 | void set_NormalType() { (uint32_t&)data = 0; } 6 | 7 | struct Indexes 8 | { 9 | uint32_t homing: 6; 10 | uint32_t emitter: 6; 11 | uint32_t emitter_rest: 5; 12 | uint32_t follower: 6; 13 | uint32_t follower_shape_ind: 8; 14 | uint32_t unused: 1; 15 | }; 16 | static_assert(sizeof(Indexes) == 4); 17 | 18 | void set_homing_ind(uint32_t ind) { data.homing = ind; } 19 | uint32_t get_homing_ind() { return data.homing; } 20 | 21 | void set_emitter_ind(uint32_t ind) { data.emitter = ind; } 22 | uint32_t get_emitter_ind() { return data.emitter; } 23 | void set_emitter_rest(uint32_t count) { data.emitter_rest = count; } 24 | uint32_t get_emitter_rest() { return data.emitter_rest; } 25 | 26 | void set_follower_ind(uint32_t ind) { data.follower = ind; } 27 | uint32_t get_follower_ind() { return data.follower; } 28 | void set_follower_shape_ind(uint32_t ind) { data.follower_shape_ind = ind; } 29 | uint32_t get_follower_shape_ind() { return data.follower_shape_ind; } 30 | 31 | Indexes data; 32 | }; 33 | static_assert(sizeof(FenixProjsRuntimeData) == 4); 34 | 35 | FenixProjsRuntimeData& get_runtime_data(RE::Projectile* proj) { return (FenixProjsRuntimeData&)(uint32_t&)proj->pad164; } 36 | 37 | void init_NormalType(RE::Projectile* proj) { get_runtime_data(proj).set_NormalType(); } 38 | 39 | void set_homing_ind(RE::Projectile* proj, uint32_t ind) { get_runtime_data(proj).set_homing_ind(ind); } 40 | uint32_t get_homing_ind(RE::Projectile* proj) { return get_runtime_data(proj).get_homing_ind(); } 41 | 42 | void set_emitter_ind(RE::Projectile* proj, uint32_t ind) { get_runtime_data(proj).set_emitter_ind(ind); } 43 | uint32_t get_emitter_ind(RE::Projectile* proj) { return get_runtime_data(proj).get_emitter_ind(); } 44 | void set_emitter_rest(RE::Projectile* proj, uint32_t count) { get_runtime_data(proj).set_emitter_rest(count); } 45 | uint32_t get_emitter_rest(RE::Projectile* proj) { return get_runtime_data(proj).get_emitter_rest(); } 46 | 47 | void set_follower_ind(RE::Projectile* proj, uint32_t ind) { get_runtime_data(proj).set_follower_ind(ind); } 48 | uint32_t get_follower_ind(RE::Projectile* proj) { return get_runtime_data(proj).get_follower_ind(); } 49 | void set_follower_shape_ind(RE::Projectile* proj, uint32_t ind) { get_runtime_data(proj).set_follower_shape_ind(ind); } 50 | uint32_t get_follower_shape_ind(RE::Projectile* proj) { return get_runtime_data(proj).get_follower_shape_ind(); } 51 | 52 | bool allows_multiple_beams(RE::Projectile* proj) 53 | { 54 | auto spell = proj->spell; 55 | return spell && spell->GetCastingType() == RE::MagicSystem::CastingType::kFireAndForget && proj->IsBeamProjectile() && 56 | proj->flags.all(RE::Projectile::Flags::kUseOrigin) && !proj->flags.any(RE::Projectile::Flags::kAutoAim); 57 | } 58 | 59 | bool allows_detach_beam(RE::MagicItem* spel) 60 | { 61 | return spel && spel->GetCastingType() == RE::MagicSystem::CastingType::kFireAndForget; 62 | } 63 | -------------------------------------------------------------------------------- /src/RuntimeData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void init_NormalType(RE::Projectile* proj); 4 | 5 | bool allows_multiple_beams(RE::Projectile* proj); 6 | bool allows_detach_beam(RE::MagicItem* proj); 7 | void set_homing_ind(RE::Projectile* proj, uint32_t ind); 8 | uint32_t get_homing_ind(RE::Projectile* proj); 9 | 10 | void set_emitter_ind(RE::Projectile* proj, uint32_t ind); 11 | uint32_t get_emitter_ind(RE::Projectile* proj); 12 | void set_emitter_rest(RE::Projectile* proj, uint32_t count); 13 | uint32_t get_emitter_rest(RE::Projectile* proj); 14 | 15 | void set_follower_ind(RE::Projectile* proj, uint32_t ind); 16 | uint32_t get_follower_ind(RE::Projectile* proj); 17 | void set_follower_shape_ind(RE::Projectile* proj, uint32_t ind); 18 | uint32_t get_follower_shape_ind(RE::Projectile* proj); 19 | -------------------------------------------------------------------------------- /src/TriggerFunctions.cpp: -------------------------------------------------------------------------------- 1 | #include "TriggerFunctions.h" 2 | 3 | #include "JsonUtils.h" 4 | 5 | #include "Homing.h" 6 | #include "Emitters.h" 7 | #include "Followers.h" 8 | #include "Multicast.h" 9 | #include "Triggers.h" 10 | 11 | namespace TriggerFunctions 12 | { 13 | Function::NumberFunctionData::NumberFunctionData(const Json::Value& data) : 14 | type(JsonUtils::read_enum(data, "type")), value(JsonUtils::getFloat(data, "value")) 15 | {} 16 | 17 | Function::NumberFunctionData::NumberFunctionData(const RE::NiPoint3& linVel) 18 | { 19 | memcpy(&type, &linVel.y, 4); 20 | value = linVel.z; 21 | } 22 | 23 | void Function::eval_SetRotationHoming(RE::Projectile* proj, RE::Actor* targetOverride) const 24 | { 25 | Homing::applyRotate(proj, ind, targetOverride); 26 | } 27 | void Function::eval_SetRotationToSight(RE::Projectile* proj) const 28 | { 29 | if (auto caster = proj->shooter.get().get(); caster && caster->As()) { 30 | FenixUtils::Geom::Projectile::aimToPoint(proj, FenixUtils::Geom::Actor::raycast(caster->As())); 31 | } 32 | } 33 | void Function::eval_SetHoming(RE::Projectile* proj, RE::Actor* targetOverride) const 34 | { 35 | Homing::apply(proj, ind, targetOverride); 36 | } 37 | void Function::eval_DisableHoming(RE::Projectile* proj) const { Homing::disable(proj); } 38 | void Function::eval_SetEmitter(RE::Projectile* proj) const { Emitters::apply(proj, ind); } 39 | void Function::eval_DisableEmitter(RE::Projectile* proj) const { Emitters::disable(proj); } 40 | void Function::eval_SetFollower(RE::Projectile* proj) const { Followers::apply(proj, ind); } 41 | void Function::eval_DisableFollower(RE::Projectile* proj) const { Followers::disable(proj, restore_speed); } 42 | void Function::eval_ChangeSpeed(RE::Projectile* proj) const 43 | { 44 | if (!proj->flags.any(RE::Projectile::Flags::kInited)) { 45 | // x for col_layer 46 | assert(proj->linearVelocity.y == 0 && proj->linearVelocity.z == 0); 47 | 48 | memcpy(&proj->linearVelocity.y, &numb.type, 4); 49 | proj->linearVelocity.z = numb.value; 50 | } else { 51 | float cur_speed = proj->linearVelocity.Length(); 52 | float old_speed = numb.apply(cur_speed); 53 | proj->linearVelocity *= cur_speed / old_speed; 54 | } 55 | } 56 | void Function::eval_ChangeRange(RE::Projectile* proj) const { numb.apply(proj->range); } 57 | void Function::eval_ApplyMultiCast(Triggers::Data* data) const { Multicast::apply(data, ind); } 58 | void Function::eval_Placeatme(Triggers::Data* data) const 59 | { 60 | RE::TESDataHandler::GetSingleton()->CreateReferenceAtLocation(form->As(), data->pos, 61 | RE::NiPoint3(data->rot.x, 0, data->rot.z), data->shooter->GetParentCell(), data->shooter->GetWorldspace(), nullptr, 62 | nullptr, RE::ObjectRefHandle(), false, true); 63 | } 64 | void Function::eval_SendAnimEvent(Triggers::Data* data) const { data->shooter->NotifyAnimationGraph(event); } 65 | void Function::eval_Explode(Triggers::Data* data) const 66 | { 67 | RE::NiMatrix3 M; 68 | M.EulerAnglesToAxesZXY(data->rot.x, 0, data->rot.z); 69 | RE::Explosion::SpawnExplosionData expldata{ form->As(), data->shooter->GetParentCell(), data->shooter, 70 | nullptr, nullptr, nullptr, nullptr, 0, data->pos, M, 1, 0 }; 71 | RE::Explosion::SpawnExplosion(expldata); 72 | } 73 | void Function::eval_SetColLayer(RE::Projectile* proj) const 74 | { 75 | if (!proj->flags.any(RE::Projectile::Flags::kInited)) { 76 | // y, z for speed 77 | assert(proj->linearVelocity.x == 0); 78 | 79 | memcpy(&proj->linearVelocity.x, &layer, 4); 80 | } else { 81 | FenixUtils::Projectile__set_collision_layer(proj, layer); 82 | } 83 | } 84 | 85 | void Function::eval_impl(Triggers::Data* data, RE::Projectile* proj, RE::Actor* targetOverride) const 86 | { 87 | switch (type) { 88 | case Type::SetRotationToSight: 89 | if (proj) 90 | eval_SetRotationToSight(proj); 91 | break; 92 | case Type::SetRotationHoming: 93 | if (proj) 94 | eval_SetRotationHoming(proj, targetOverride); 95 | break; 96 | case Type::SetHoming: 97 | if (proj) 98 | eval_SetHoming(proj, targetOverride); 99 | break; 100 | case Type::SetEmitter: 101 | if (proj) 102 | eval_SetEmitter(proj); 103 | break; 104 | case Type::SetFollower: 105 | if (proj) 106 | eval_SetFollower(proj); 107 | break; 108 | case Type::ChangeSpeed: 109 | if (proj) 110 | eval_ChangeSpeed(proj); 111 | break; 112 | case Type::ChangeRange: 113 | if (proj) 114 | eval_ChangeRange(proj); 115 | break; 116 | case Type::ApplyMultiCast: 117 | eval_ApplyMultiCast(data); 118 | break; 119 | case Type::DisableFollower: 120 | if (proj) 121 | eval_DisableFollower(proj); 122 | break; 123 | case Type::DisableEmitter: 124 | if (proj) 125 | eval_DisableEmitter(proj); 126 | break; 127 | case Type::DisableHoming: 128 | if (proj) 129 | eval_DisableHoming(proj); 130 | break; 131 | case Type::Placeatme: 132 | eval_Placeatme(data); 133 | break; 134 | case Type::SendAnimEvent: 135 | eval_SendAnimEvent(data); 136 | break; 137 | case Type::Explode: 138 | eval_Explode(data); 139 | break; 140 | case Type::SetColLayer: 141 | if (proj) 142 | eval_SetColLayer(proj); 143 | break; 144 | default: 145 | return; 146 | } 147 | } 148 | 149 | void Function::eval(Triggers::Data* data, RE::Projectile* proj, RE::Actor* targetOverride) const 150 | { 151 | if (on_follower) { 152 | Followers::forEachFollower(data->shooter, [this, data, targetOverride](RE::Projectile* proj_follower) { 153 | data->pos = proj_follower->GetPosition(); 154 | data->rot = { proj_follower->GetAngleX(), proj_follower->GetAngleZ() }; 155 | eval_impl(data, proj_follower, targetOverride); 156 | return Followers::forEachRes::kContinue; 157 | }); 158 | } else { 159 | eval_impl(data, proj, targetOverride); 160 | } 161 | } 162 | 163 | uint32_t Function::get_homing_ind(bool rotation) const 164 | { 165 | if (!rotation && type == Type::SetHoming || rotation && type == Type::SetRotationHoming) 166 | return ind; 167 | 168 | return 0; 169 | } 170 | 171 | Function::Function(const std::string& filename, const Json::Value& function) : 172 | type(JsonUtils::read_enum(function, "type")), on_follower(JsonUtils::mb_read_field(function, "on_followers")) 173 | { 174 | switch (type) { 175 | case Type::SetRotationToSight: 176 | break; 177 | case Type::SetRotationHoming: 178 | ind = Homing::get_key_ind(filename, JsonUtils::getString(function, "id")); 179 | break; 180 | case Type::SetHoming: 181 | ind = Homing::get_key_ind(filename, JsonUtils::getString(function, "id")); 182 | break; 183 | case Type::SetEmitter: 184 | ind = Emitters::get_key_ind(filename, JsonUtils::getString(function, "id")); 185 | break; 186 | case Type::SetFollower: 187 | ind = Followers::get_key_ind(filename, JsonUtils::getString(function, "id")); 188 | break; 189 | case Type::ApplyMultiCast: 190 | ind = Multicast::get_key_ind(filename, JsonUtils::getString(function, "id")); 191 | break; 192 | case Type::ChangeSpeed: 193 | case Type::ChangeRange: 194 | numb = NumberFunctionData(function["data"]); 195 | break; 196 | case Type::DisableFollower: 197 | restore_speed = JsonUtils::mb_read_field(function, "restore_speed"); 198 | break; 199 | case Type::DisableEmitter: 200 | case Type::DisableHoming: 201 | break; 202 | case Type::Placeatme: 203 | form = RE::TESForm::LookupByID(JsonUtils::get_formid(filename, JsonUtils::getString(function, "form"))); 204 | break; 205 | case Type::SendAnimEvent: 206 | memset(&event, 0, 8); 207 | event = JsonUtils::getString(function, "event"); 208 | break; 209 | case Type::Explode: 210 | form = RE::TESForm::LookupByID(JsonUtils::get_formid(filename, JsonUtils::getString(function, "explosion"))); 211 | break; 212 | case Type::SetColLayer: 213 | layer = Followers::layer2layer(JsonUtils::read_enum(function, "layer")); 214 | break; 215 | default: 216 | assert(false); 217 | break; 218 | } 219 | } 220 | 221 | Function::Function(const RE::NiPoint3& linVel) : type(Type::ChangeSpeed), on_follower(false) 222 | { 223 | numb = NumberFunctionData(linVel); 224 | } 225 | 226 | Function::Function(const Function& other) : type(other.type), on_follower(other.on_follower) 227 | { 228 | if (type == Type::SendAnimEvent) { 229 | memset(&event, 0, 8); 230 | event = other.event; 231 | return; 232 | } 233 | 234 | memcpy(this, &other, sizeof(Function)); 235 | } 236 | 237 | Functions::Functions(const std::string& filename, const Json::Value& json_TriggerFunctions) : 238 | disable_origin(JsonUtils::mb_read_field(json_TriggerFunctions, "disableOrigin")) 239 | { 240 | const auto& json_functions = json_TriggerFunctions["functions"]; 241 | for (size_t i = 0; i < json_functions.size(); i++) { 242 | functions.emplace_back(filename, json_functions[(int)i]); 243 | } 244 | } 245 | 246 | void Functions::call(Triggers::Data* data, RE::Projectile* proj, RE::Actor* targetOverride) const 247 | { 248 | for (auto& func : functions) { 249 | func.eval(data, proj, targetOverride); 250 | } 251 | } 252 | 253 | uint32_t Functions::get_homing_ind(bool rotation) const 254 | { 255 | for (const auto& function : functions) { 256 | if (auto ind = function.get_homing_ind(rotation)) { 257 | return ind; 258 | } 259 | } 260 | return 0; 261 | } 262 | 263 | void Functions::call(RE::Projectile* proj, RE::Actor* targetOverride) const 264 | { 265 | Triggers::Data trigger_data(proj); 266 | call(&trigger_data, proj, targetOverride); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/TriggerFunctions.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "json/json.h" 4 | 5 | namespace Triggers 6 | { 7 | struct Data; 8 | } 9 | 10 | namespace TriggerFunctions 11 | { 12 | struct Function 13 | { 14 | enum class Type : uint32_t 15 | { 16 | SetRotationHoming, 17 | SetRotationToSight, 18 | SetHoming, 19 | SetEmitter, 20 | SetFollower, 21 | ChangeSpeed, 22 | ChangeRange, 23 | ApplyMultiCast, 24 | DisableHoming, 25 | DisableFollower, 26 | DisableEmitter, 27 | Placeatme, 28 | SendAnimEvent, 29 | Explode, 30 | SetColLayer 31 | }; 32 | 33 | private: 34 | Type type: 8; 35 | uint32_t on_follower: 1; 36 | 37 | struct NumberFunctionData 38 | { 39 | enum class NumberFunctions : uint32_t 40 | { 41 | Add, 42 | Mul, 43 | Set 44 | } type; 45 | float value; 46 | 47 | NumberFunctionData() : type(NumberFunctions::Add), value(0) {} 48 | explicit NumberFunctionData(const Json::Value& data); 49 | explicit NumberFunctionData(const RE::NiPoint3& linVel); // For ChangeSpeed (triggers only on 3dLoaded) 50 | 51 | float apply(float& val) const 52 | { 53 | float ans = val; 54 | switch (type) { 55 | case NumberFunctions::Set: 56 | val = value; 57 | break; 58 | case NumberFunctions::Add: 59 | val += value; 60 | break; 61 | case NumberFunctions::Mul: 62 | val *= value; 63 | break; 64 | default: 65 | break; 66 | } 67 | return ans; 68 | } 69 | }; 70 | static_assert(sizeof(NumberFunctionData) == 0x8); 71 | 72 | union 73 | { 74 | uint32_t ind; 75 | NumberFunctionData numb; 76 | bool restore_speed; 77 | RE::TESForm* form; 78 | RE::BSFixedString event; 79 | RE::COL_LAYER layer; 80 | }; 81 | 82 | void eval_SetRotationToSight(RE::Projectile* proj) const; 83 | void eval_SetRotationHoming(RE::Projectile* proj, RE::Actor* targetOverride) const; 84 | void eval_SetHoming(RE::Projectile* proj, RE::Actor* targetOverride) const; 85 | void eval_DisableHoming(RE::Projectile* proj) const; 86 | void eval_SetEmitter(RE::Projectile* proj) const; 87 | void eval_DisableEmitter(RE::Projectile* proj) const; 88 | void eval_SetFollower(RE::Projectile* proj) const; 89 | void eval_DisableFollower(RE::Projectile* proj) const; 90 | void eval_ChangeSpeed(RE::Projectile* proj) const; 91 | void eval_ChangeRange(RE::Projectile* proj) const; 92 | void eval_ApplyMultiCast(Triggers::Data* data) const; 93 | void eval_Placeatme(Triggers::Data* data) const; 94 | void eval_SendAnimEvent(Triggers::Data* data) const; 95 | void eval_Explode(Triggers::Data* data) const; 96 | void eval_SetColLayer(RE::Projectile* proj) const; 97 | 98 | void eval_impl(Triggers::Data* data, RE::Projectile* proj, RE::Actor* targetOverride = nullptr) const; 99 | 100 | public: 101 | void eval(Triggers::Data* data, RE::Projectile* proj, RE::Actor* targetOverride = nullptr) const; 102 | uint32_t get_homing_ind(bool rotation) const; 103 | 104 | Function() : type(Type::ChangeSpeed), numb() {} 105 | Function(const std::string& filename, const Json::Value& function); 106 | explicit Function(const RE::NiPoint3& linVel); // For ChangeSpeed (triggers only on 3dLoaded) 107 | Function(const Function& other); 108 | 109 | ~Function() 110 | { 111 | switch (type) { 112 | case TriggerFunctions::Function::Type::SendAnimEvent: 113 | event.~BSFixedString(); 114 | break; 115 | default: 116 | break; 117 | } 118 | } 119 | }; 120 | 121 | struct Functions 122 | { 123 | private: 124 | std::vector functions; 125 | uint32_t disable_origin: 1; 126 | 127 | public: 128 | Functions() = default; 129 | explicit Functions(const std::string& filename, const Json::Value& json_TriggerFunctions); 130 | 131 | void call(RE::Projectile* proj, RE::Actor* targetOverride = nullptr) const; 132 | void call(Triggers::Data* data, RE::Projectile* proj, RE::Actor* targetOverride) const; 133 | 134 | uint32_t get_homing_ind(bool rotation) const; 135 | 136 | bool should_disable_origin() const { return disable_origin; } 137 | }; 138 | } 139 | -------------------------------------------------------------------------------- /src/Triggers.cpp: -------------------------------------------------------------------------------- 1 | #include "Triggers.h" 2 | #include "TriggerFunctions.h" 3 | #include "JsonUtils.h" 4 | 5 | namespace Triggers 6 | { 7 | struct Condition 8 | { 9 | enum class Hand : uint32_t 10 | { 11 | Both, 12 | Left, 13 | Right 14 | }; 15 | 16 | enum class Type : uint32_t 17 | { 18 | Hand, 19 | ProjBaseIsFormID, 20 | EffectIsFormID, 21 | EffectHasKwd, 22 | EffectsIsFormID, 23 | EffectsHasKwd, 24 | SpellHasKwd, 25 | SpellIsFormID, 26 | CasterIsFormID, 27 | CasterBaseIsFormID, 28 | CasterHasKwd, 29 | WeaponBaseIsFormID, 30 | WeaponHasKwd 31 | }; 32 | 33 | Type type: 30; 34 | uint32_t invert: 1; 35 | uint32_t OR: 1; 36 | 37 | union 38 | { 39 | Hand hand; 40 | RE::FormID formid; 41 | }; 42 | 43 | private: 44 | bool eval_Hand(RE::MagicSystem::CastingSource source) const 45 | { 46 | using Src = RE::MagicSystem::CastingSource; 47 | return hand == Hand::Both || hand == Hand::Left && source == Src::kLeftHand || 48 | hand == Hand::Right && (source == Src::kRightHand || source == Src::kInstant || source == Src::kOther); 49 | } 50 | bool eval_BaseIsFormID(RE::BGSProjectile* bproj) const { return bproj && bproj->formID == formid; } 51 | bool eval_EffectsHasKwd(RE::MagicItem* spel) const 52 | { 53 | if (spel) { 54 | for (auto eff : spel->effects) { 55 | if (eff->baseEffect->HasKeywordID(formid)) 56 | return true; 57 | } 58 | } 59 | return false; 60 | } 61 | bool eval_EffectsIsFormID(RE::MagicItem* spel) const 62 | { 63 | if (spel) { 64 | for (auto eff : spel->effects) { 65 | if (eff->baseEffect->formID == formid) 66 | return true; 67 | } 68 | } 69 | return false; 70 | } 71 | bool eval_EffectHasKwd(RE::EffectSetting* mgef) const { return mgef ? mgef->HasKeywordID(formid) : false; } 72 | bool eval_EffectIsFormID(RE::EffectSetting* mgef) const { return mgef ? mgef->formID == formid : false; } 73 | bool eval_SpellHasKwd(RE::MagicItem* spel) const { return spel ? spel->HasKeywordID(formid) : false; } 74 | bool eval_SpellIsFormID(RE::MagicItem* spel) const { return spel ? spel->formID == formid : false; } 75 | bool eval_CasterIsFormID(RE::TESObjectREFR* caster) const { return caster && caster->formID == formid; } 76 | bool eval_CasterBaseIsFormID(RE::TESObjectREFR* caster) const 77 | { 78 | return caster && caster->GetBaseObject() && caster->GetBaseObject()->formID == formid; 79 | } 80 | bool eval_CasterHasKwd(RE::TESObjectREFR* caster_) const 81 | { 82 | if (caster_) { 83 | if (auto caster = caster_->As()) { 84 | if (auto kwd = RE::TESForm::LookupByID(formid)) { 85 | return caster->HasKeyword(kwd) || (caster->GetActorBase() && caster->GetActorBase()->HasKeyword(kwd)) || 86 | FenixUtils::TESObjectREFR__HasEffectKeyword(caster, kwd); 87 | } 88 | } 89 | } 90 | return false; 91 | } 92 | bool eval_WeaponBaseIsFormID(RE::TESObjectWEAP* weap) const { return weap && weap->formID == formid; } 93 | bool eval_WeaponHasKwd(RE::TESObjectWEAP* weap) const { return weap && weap->HasKeywordID(formid); } 94 | 95 | public: 96 | Condition(const std::string& filename, const Json::Value& json_condition) : 97 | type(JsonUtils::string2enum(JsonUtils::getString(json_condition, "type"))) 98 | { 99 | switch (type) { 100 | break; 101 | case Type::ProjBaseIsFormID: 102 | case Type::EffectIsFormID: 103 | case Type::EffectHasKwd: 104 | case Type::EffectsIsFormID: 105 | case Type::EffectsHasKwd: 106 | case Type::SpellHasKwd: 107 | case Type::SpellIsFormID: 108 | case Type::CasterIsFormID: 109 | case Type::CasterBaseIsFormID: 110 | case Type::CasterHasKwd: 111 | case Type::WeaponBaseIsFormID: 112 | case Type::WeaponHasKwd: 113 | formid = JsonUtils::get_formid(filename, JsonUtils::getString(json_condition, "formID")); 114 | break; 115 | case Type::Hand: 116 | hand = JsonUtils::read_enum(json_condition, "hand"); 117 | break; 118 | default: 119 | assert(false); 120 | break; 121 | } 122 | 123 | invert = JsonUtils::mb_getBool(json_condition, "invert"); 124 | OR = JsonUtils::mb_getBool(json_condition, "OR"); 125 | } 126 | 127 | bool eval(Data* data) const 128 | { 129 | switch (type) { 130 | case Type::WeaponBaseIsFormID: 131 | return eval_WeaponBaseIsFormID(data->weap); 132 | case Type::WeaponHasKwd: 133 | return eval_WeaponHasKwd(data->weap); 134 | case Type::CasterIsFormID: 135 | return eval_CasterIsFormID(data->shooter); 136 | case Type::CasterBaseIsFormID: 137 | return eval_CasterBaseIsFormID(data->shooter); 138 | case Type::CasterHasKwd: 139 | return eval_CasterHasKwd(data->shooter); 140 | case Type::ProjBaseIsFormID: 141 | return eval_BaseIsFormID(data->bproj); 142 | case Type::SpellIsFormID: 143 | return eval_SpellIsFormID(data->spel); 144 | case Type::SpellHasKwd: 145 | return eval_SpellHasKwd(data->spel); 146 | case Type::EffectIsFormID: 147 | return eval_EffectIsFormID(data->mgef); 148 | case Type::EffectHasKwd: 149 | return eval_EffectHasKwd(data->mgef); 150 | case Type::EffectsIsFormID: 151 | return eval_EffectsIsFormID(data->spel); 152 | case Type::EffectsHasKwd: 153 | return eval_EffectsHasKwd(data->spel); 154 | case Type::Hand: 155 | return eval_Hand(data->hand); 156 | default: 157 | assert(false); 158 | return false; 159 | } 160 | } 161 | }; 162 | static_assert(sizeof(Condition) == 0x8); 163 | 164 | struct Trigger 165 | { 166 | private: 167 | std::vector conditions; 168 | TriggerFunctions::Functions functions; 169 | 170 | void call_functions(Data* data, RE::Projectile* proj, RE::Actor* targetOverride) const 171 | { 172 | functions.call(data, proj, targetOverride); 173 | } 174 | 175 | // state x val x OR 176 | static inline int transitions[4][2][2] = { { { 3, 1 }, { 0, 2 } }, { { 3, 1 }, { 0, 2 } }, { { 0, 2 }, { 0, 2 } }, 177 | { { 3, 3 }, { 3, 3 } } }; 178 | 179 | bool call_conditions(Data* data) const 180 | { 181 | uint32_t state = 0; 182 | 183 | if (conditions.empty()) 184 | return false; 185 | 186 | for (const auto& cond : conditions) { 187 | state = transitions[state][cond.eval(data) != static_cast(cond.invert)][cond.OR]; 188 | } 189 | return state == 0 || state == 2; 190 | } 191 | 192 | public: 193 | Trigger(const std::string& filename, const Json::Value& json_trigger) : 194 | functions(filename, json_trigger["TriggerFunctions"]) 195 | { 196 | if (json_trigger.isMember("conditions")) { 197 | auto& json_conditions = json_trigger["conditions"]; 198 | for (size_t i = 0; i < json_conditions.size(); i++) { 199 | const auto& condition = json_conditions[(int)i]; 200 | 201 | conditions.emplace_back(filename, condition); 202 | } 203 | } 204 | } 205 | 206 | void eval(Data* data, RE::Projectile* proj, RE::Actor* targetOverride) const 207 | { 208 | if (call_conditions(data)) 209 | call_functions(data, proj, targetOverride); 210 | } 211 | 212 | bool should_disable_origin(Data* data) const { return call_conditions(data) && functions.should_disable_origin(); } 213 | }; 214 | 215 | class Triggers 216 | { 217 | static inline std::array, (uint32_t)Event::Total> triggers; 218 | 219 | public: 220 | static void clear() 221 | { 222 | for (auto& cur_triggers : triggers) { 223 | cur_triggers.clear(); 224 | } 225 | } 226 | 227 | static void init(const std::string& filename, const Json::Value& json_triggers) 228 | { 229 | for (size_t i = 0; i < json_triggers.size(); i++) { 230 | auto& trigger = json_triggers[(int)i]; 231 | 232 | auto type = JsonUtils::read_enum(trigger, "event"); 233 | triggers[(uint32_t)type].emplace_back(filename, trigger); 234 | } 235 | } 236 | 237 | static void eval(Data* data, Event e, RE::Projectile* proj, RE::Actor* targetOverride) 238 | { 239 | for (const auto& trigger : triggers[(uint32_t)e]) { 240 | trigger.eval(data, proj, targetOverride); 241 | } 242 | } 243 | 244 | // Called on ProjAppeared 245 | static bool should_disable_origin(Data* data) 246 | { 247 | std::pair ans{ true, false }; 248 | for (const auto& trigger : triggers[(uint32_t)Event::ProjAppeared]) { 249 | if (trigger.should_disable_origin(data)) 250 | return true; 251 | } 252 | return false; 253 | } 254 | }; 255 | 256 | void clear() { Triggers::clear(); } 257 | 258 | void init(const std::string& filename, const Json::Value& json_root) { return Triggers::init(filename, json_root["Triggers"]); } 259 | 260 | void eval(Data* data, Event e, RE::Projectile* proj, RE::Actor* targetOverride) 261 | { 262 | Triggers::eval(data, e, proj, targetOverride); 263 | } 264 | 265 | namespace Hooks 266 | { 267 | class ApplyTriggersHook 268 | { 269 | public: 270 | static void Hook() 271 | { 272 | auto& trmp = SKSE::GetTrampoline(); 273 | 274 | // arrow->unk140 = 0i64; with arrow=nullptr 275 | FenixUtils::writebytes<17693, 0xefa>("\x0F\x1F\x80\x00\x00\x00\x00"sv); 276 | // SkyrimSE.exe+2360C2 -- TESObjectWEAP::Fire_140235240 277 | _LaunchArrow = trmp.write_call<5>(REL::ID(17693).address() + 0xe82, LaunchArrow); 278 | 279 | // 1405504F5 -- MagicCaster::FireProjectileFromSource 280 | _FireProjectile1 = trmp.write_call<5>(REL::ID(33670).address() + 0x575, FireProjectile1); 281 | // SkyrimSE.exe+5504F5 -- MagicCaster::FireProjectile_0 282 | _FireProjectile2 = trmp.write_call<5>(REL::ID(33671).address() + 0x125, FireProjectile2); 283 | 284 | // 140628dd7 Actor::CombatHit 285 | _InitializeHitData = trmp.write_call<5>(REL::ID(37673).address() + 0x1b7, InitializeHitData); 286 | 287 | // 1407211ea HitFrameHandler::Handle 288 | _DoMeleeAttack = trmp.write_call<5>(REL::ID(41747).address() + 0x3a, DoMeleeAttack); 289 | 290 | _EffectAddedC = REL::Relocation(RE::VTABLE_Character[4]).write_vfunc(0x8, EffectAddedC); 291 | _EffectAddedP = REL::Relocation(RE::VTABLE_PlayerCharacter[4]).write_vfunc(0x8, EffectAddedP); 292 | _EffectRemovedC = REL::Relocation(RE::VTABLE_Character[4]).write_vfunc(0x9, EffectRemovedC); 293 | _EffectRemovedP = REL::Relocation(RE::VTABLE_PlayerCharacter[4]).write_vfunc(0x9, EffectRemovedP); 294 | 295 | // 140550a37 MagicCaster::FireProjectile 296 | _Launch1 = trmp.write_call<5>(REL::ID(33672).address() + 0x377, Launch1); 297 | 298 | // 140550a37 TESObjectWEAP::Fire 299 | _Launch2 = trmp.write_call<5>(REL::ID(17693).address() + 0xe82, Launch2); 300 | 301 | // SkyrimSE.exe+754bd8 302 | _CalcVelocityVector = trmp.write_call<5>(REL::ID(43030).address() + 0x3b8, CalcVelocityVector); 303 | 304 | // SkyrimSE.exe+74bc21 Proj::Kill 305 | _ClearFollowedObject = trmp.write_call<5>(REL::ID(42930).address() + 0x21, ClearFollowedObject); 306 | 307 | // SkyrimSE.exe+7478cc MissileProj::AddImpact 308 | _AddImpact1 = trmp.write_call<5>(REL::ID(42866).address() + 0xbc, AddImpact1); 309 | // SkyrimSE.exe+735b06 ConeProj::AddImpact 310 | _AddImpact2 = trmp.write_call<5>(REL::ID(42633).address() + 0x66, AddImpact2); 311 | // SkyrimSE.exe+734232 BeamProj::AddImpact 312 | _AddImpact3 = trmp.write_call<5>(REL::ID(42594).address() + 0xc2, AddImpact3); 313 | // SkyrimSE.exe+73e757 FlameProj::AddImpact 314 | _AddImpact4 = trmp.write_call<5>(REL::ID(42736).address() + 0x167, AddImpact4); 315 | // SkyrimSE.exe+73fc9a GrenadeProj::AddImpact 316 | _AddImpact5 = trmp.write_call<5>(REL::ID(42768).address() + 0x8a, AddImpact5); 317 | } 318 | 319 | private: 320 | static bool FireProjectile1(RE::MagicCaster* a, RE::BGSProjectile* bproj, RE::TESObjectREFR* a_char, 321 | RE::CombatController* a4, RE::NiPoint3* startPos, float rotationZ, float rotationX, uint32_t area, void* a9) 322 | { 323 | Data data(nullptr, a->GetCasterAsActor(), bproj, a->currentSpell, 324 | a->currentSpell ? a->currentSpell->GetAVEffect() : nullptr, nullptr, a->GetCastingSource(), Data::Type::Spell, 325 | { rotationX, rotationZ }, *startPos); 326 | 327 | if (Triggers::should_disable_origin(&data)) { 328 | eval(&data, Event::ProjAppeared, nullptr); 329 | return false; 330 | } else { 331 | return _FireProjectile1(a, bproj, a_char, a4, startPos, rotationZ, rotationX, area, a9); 332 | } 333 | } 334 | static bool FireProjectile2(RE::MagicCaster* a, RE::BGSProjectile* bproj, RE::TESObjectREFR* a_char, 335 | RE::CombatController* a4, RE::NiPoint3* startPos, float rotationZ, float rotationX, uint32_t area, void* a9) 336 | { 337 | Data data(nullptr, a->GetCasterAsActor(), bproj, a->currentSpell, 338 | a->currentSpell ? a->currentSpell->GetAVEffect() : nullptr, nullptr, a->GetCastingSource(), Data::Type::Spell, 339 | { rotationX, rotationZ }, *startPos); 340 | 341 | if (Triggers::should_disable_origin(&data)) { 342 | eval(&data, Event::ProjAppeared, nullptr); 343 | return false; 344 | } else { 345 | return _FireProjectile2(a, bproj, a_char, a4, startPos, rotationZ, rotationX, area, a9); 346 | } 347 | } 348 | 349 | static RE::ProjectileHandle* LaunchArrow(RE::ProjectileHandle* handle, RE::Projectile::LaunchData* a_ldata) 350 | { 351 | Data data(Data::Type::Arrow, a_ldata); 352 | if (Triggers::should_disable_origin(&data)) { 353 | eval(&data, Event::ProjAppeared, nullptr); 354 | handle->reset(); 355 | return handle; 356 | } else { 357 | return _LaunchArrow(handle, a_ldata); 358 | } 359 | } 360 | 361 | static RE::ProjectileHandle* Launch1(RE::ProjectileHandle* handle, RE::Projectile::LaunchData* ldata) 362 | { 363 | auto ans = _Launch1(handle, ldata); 364 | 365 | Data data(Data::Type::Spell, ldata); 366 | if (auto proj = handle->get().get()) { 367 | eval(&data, Event::ProjAppeared, proj); 368 | } 369 | 370 | return ans; 371 | } 372 | static RE::ProjectileHandle* Launch2(RE::ProjectileHandle* handle, RE::Projectile::LaunchData* ldata) 373 | { 374 | auto ans = _Launch2(handle, ldata); 375 | 376 | Data data(Data::Type::Arrow, ldata); 377 | if (auto proj = handle->get().get()) { 378 | eval(&data, Event::ProjAppeared, proj); 379 | } 380 | 381 | return ans; 382 | } 383 | 384 | static void InitializeHitData(RE::HitData* hitdata, RE::Actor* attacker, RE::Actor* victim, 385 | RE::InventoryEntryData* weapitem, bool left) 386 | { 387 | _InitializeHitData(hitdata, attacker, victim, weapitem, left); 388 | 389 | Data data(weapitem ? weapitem->object->As() : nullptr, attacker, nullptr, nullptr, nullptr, 390 | nullptr, left ? RE::MagicSystem::CastingSource::kLeftHand : RE::MagicSystem::CastingSource::kRightHand, 391 | Data::Type::None, FenixUtils::Geom::rot_at(hitdata->hitDirection), hitdata->hitPosition); 392 | 393 | eval(&data, Event::HitMelee, nullptr); 394 | data.shooter = victim; 395 | eval(&data, Event::HitByMelee, nullptr); 396 | } 397 | 398 | static void DoMeleeAttack(RE::Actor* a, bool left, char a3) 399 | { 400 | _DoMeleeAttack(a, left, a3); 401 | 402 | RE::MagicSystem::CastingSource hand = 403 | left ? RE::MagicSystem::CastingSource::kLeftHand : RE::MagicSystem::CastingSource::kRightHand; 404 | 405 | auto caster = a->GetMagicCaster(hand); 406 | if (!caster) { 407 | return; 408 | } 409 | 410 | auto invweap = a->GetAttackingWeapon(); 411 | auto weap = invweap ? invweap->object->As() : nullptr; 412 | 413 | RE::NiPoint3 pos; 414 | if (auto node = caster->GetMagicNode()) { 415 | pos = node->world.translate; 416 | } else { 417 | pos = a->GetPosition(); 418 | pos.z += (a->GetBoundMax().z - a->GetBoundMin().z) * 0.7f; 419 | } 420 | 421 | Data data(weap, a, nullptr, nullptr, nullptr, nullptr, 422 | left ? RE::MagicSystem::CastingSource::kLeftHand : RE::MagicSystem::CastingSource::kRightHand, 423 | Data::Type::None, { a->GetAimAngle(), a->GetAimHeading() }, std::move(pos)); 424 | 425 | eval(&data, Event::Swing, nullptr); 426 | } 427 | 428 | static void EffectAdded(RE::MagicTarget* _this, RE::ActiveEffect* a_effect) 429 | { 430 | auto a = (RE::Actor*)((char*)_this - 0x98); 431 | 432 | auto effsetting = a_effect->GetBaseObject(); 433 | 434 | 435 | Data data(nullptr, a, effsetting ? effsetting->data.projectileBase : nullptr, a_effect->spell, effsetting, 436 | nullptr, a_effect->castingSource, Data::Type::None, { a->GetAimAngle(), a->GetAimHeading() }, 437 | a->GetPosition()); 438 | 439 | eval(&data, Event::EffectStart, nullptr); 440 | } 441 | 442 | static void EffectAddedP(RE::MagicTarget* _this, RE::ActiveEffect* a_effect) 443 | { 444 | _EffectAddedP(_this, a_effect); 445 | if (a_effect) { 446 | EffectAdded(_this, a_effect); 447 | } 448 | } 449 | 450 | static void EffectAddedC(RE::MagicTarget* _this, RE::ActiveEffect* a_effect) 451 | { 452 | _EffectAddedC(_this, a_effect); 453 | if (a_effect) { 454 | EffectAdded(_this, a_effect); 455 | } 456 | } 457 | 458 | static void EffectRemoved(RE::MagicTarget* _this, RE::ActiveEffect* a_effect) 459 | { 460 | auto a = (RE::Actor*)((char*)_this - 0x98); 461 | 462 | auto effsetting = a_effect->GetBaseObject(); 463 | 464 | Data data(nullptr, a, effsetting ? effsetting->data.projectileBase : nullptr, a_effect->spell, effsetting, 465 | nullptr, a_effect->castingSource, Data::Type::None, { a->GetAimAngle(), a->GetAimHeading() }, 466 | a->GetPosition()); 467 | 468 | eval(&data, Event::EffectEnd, nullptr); 469 | } 470 | 471 | static void EffectRemovedP(RE::MagicTarget* _this, RE::ActiveEffect* a_effect) 472 | { 473 | if (a_effect) { 474 | EffectRemoved(_this, a_effect); 475 | } 476 | _EffectRemovedP(_this, a_effect); 477 | } 478 | 479 | static void EffectRemovedC(RE::MagicTarget* _this, RE::ActiveEffect* a_effect) 480 | { 481 | if (a_effect) { 482 | EffectRemoved(_this, a_effect); 483 | } 484 | _EffectRemovedC(_this, a_effect); 485 | } 486 | 487 | static void CalcVelocityVector(RE::Projectile* proj) 488 | { 489 | if (proj->linearVelocity.x != 0 || proj->linearVelocity.y != 0 || proj->linearVelocity.z != 0) { 490 | if (proj->linearVelocity.x != 0) { 491 | RE::COL_LAYER layer = RE::COL_LAYER::kSpell; 492 | memcpy(&layer, &proj->linearVelocity.x, 4); 493 | FenixUtils::Projectile__set_collision_layer(proj, layer); 494 | } 495 | 496 | // if both 0, then it is Add 0, so fine if it is skipped 497 | if (proj->linearVelocity.y != 0 || proj->linearVelocity.z != 0) { 498 | TriggerFunctions::Function changeVel(proj->linearVelocity); 499 | _CalcVelocityVector(proj); 500 | Data data(proj); 501 | changeVel.eval(&data, proj); 502 | } else { 503 | _CalcVelocityVector(proj); 504 | } 505 | 506 | } else { 507 | _CalcVelocityVector(proj); 508 | } 509 | } 510 | 511 | static void ClearFollowedObject(RE::BSSoundHandle* shandle) 512 | { 513 | auto proj = (RE::Projectile*)((char*)shandle - 0x128); 514 | 515 | Data data(proj); 516 | eval(&data, Event::ProjDestroyed, nullptr); 517 | 518 | _ClearFollowedObject(shandle); 519 | } 520 | 521 | static RE::Projectile::ImpactData* OnAddImpact(RE::Projectile* proj, RE::Projectile::ImpactData* ans) 522 | { 523 | if (ans) { 524 | Data data(proj); 525 | data.pos = ans->desiredTargetLoc; 526 | data.rot = FenixUtils::Geom::rot_at(-ans->negativeVelocity); 527 | eval(&data, Event::ProjImpact, nullptr); 528 | if (auto target = ans->collidee.get().get(); target && target->As()) { 529 | eval(&data, Event::HitProjectile, nullptr); 530 | data.shooter = target; 531 | eval(&data, Event::HitByProjectile, nullptr); 532 | } 533 | } 534 | return ans; 535 | } 536 | static RE::Projectile::ImpactData* AddImpact1(RE::Projectile* proj, RE::TESObjectREFR* refr, RE::NiPoint3& targetLoc, 537 | RE::NiPoint3* velocity_or_normal, RE::hkpCollidable* collidable, uint32_t shape_key, bool hit_happend) 538 | { 539 | return OnAddImpact(proj, 540 | _AddImpact1(proj, refr, targetLoc, velocity_or_normal, collidable, shape_key, hit_happend)); 541 | } 542 | static RE::Projectile::ImpactData* AddImpact2(RE::Projectile* proj, RE::TESObjectREFR* refr, RE::NiPoint3& targetLoc, 543 | RE::NiPoint3* velocity_or_normal, RE::hkpCollidable* collidable, uint32_t shape_key, bool hit_happend) 544 | { 545 | return OnAddImpact(proj, 546 | _AddImpact2(proj, refr, targetLoc, velocity_or_normal, collidable, shape_key, hit_happend)); 547 | } 548 | static RE::Projectile::ImpactData* AddImpact3(RE::Projectile* proj, RE::TESObjectREFR* refr, RE::NiPoint3& targetLoc, 549 | RE::NiPoint3* velocity_or_normal, RE::hkpCollidable* collidable, uint32_t shape_key, bool hit_happend) 550 | { 551 | return OnAddImpact(proj, 552 | _AddImpact3(proj, refr, targetLoc, velocity_or_normal, collidable, shape_key, hit_happend)); 553 | } 554 | static RE::Projectile::ImpactData* AddImpact4(RE::Projectile* proj, RE::TESObjectREFR* refr, RE::NiPoint3& targetLoc, 555 | RE::NiPoint3* velocity_or_normal, RE::hkpCollidable* collidable, uint32_t shape_key, bool hit_happend) 556 | { 557 | return OnAddImpact(proj, 558 | _AddImpact4(proj, refr, targetLoc, velocity_or_normal, collidable, shape_key, hit_happend)); 559 | } 560 | static RE::Projectile::ImpactData* AddImpact5(RE::Projectile* proj, RE::TESObjectREFR* refr, RE::NiPoint3& targetLoc, 561 | RE::NiPoint3* velocity_or_normal, RE::hkpCollidable* collidable, uint32_t shape_key, bool hit_happend) 562 | { 563 | return OnAddImpact(proj, 564 | _AddImpact5(proj, refr, targetLoc, velocity_or_normal, collidable, shape_key, hit_happend)); 565 | } 566 | 567 | static inline REL::Relocation _AddImpact1; 568 | static inline REL::Relocation _AddImpact2; 569 | static inline REL::Relocation _AddImpact3; 570 | static inline REL::Relocation _AddImpact4; 571 | static inline REL::Relocation _AddImpact5; 572 | static inline REL::Relocation _ClearFollowedObject; 573 | static inline REL::Relocation _CalcVelocityVector; 574 | static inline REL::Relocation _InitializeHitData; 575 | static inline REL::Relocation _DoMeleeAttack; 576 | static inline REL::Relocation _EffectAddedC; 577 | static inline REL::Relocation _EffectAddedP; 578 | static inline REL::Relocation _EffectRemovedC; 579 | static inline REL::Relocation _EffectRemovedP; 580 | static inline REL::Relocation _LaunchArrow; 581 | static inline REL::Relocation _FireProjectile1; 582 | static inline REL::Relocation _FireProjectile2; 583 | static inline REL::Relocation _Launch1; 584 | static inline REL::Relocation _Launch2; 585 | }; 586 | } 587 | 588 | void install() 589 | { 590 | using namespace Hooks; 591 | 592 | ApplyTriggersHook::Hook(); 593 | } 594 | } 595 | -------------------------------------------------------------------------------- /src/Triggers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "json/json.h" 4 | 5 | namespace Triggers 6 | { 7 | enum class Event : uint32_t 8 | { 9 | ProjAppeared, 10 | Swing, 11 | HitMelee, 12 | HitByMelee, 13 | HitProjectile, 14 | HitByProjectile, 15 | Cast, 16 | EffectStart, 17 | EffectEnd, 18 | ProjDestroyed, 19 | ProjImpact, 20 | 21 | Total // for std::array 22 | }; 23 | 24 | struct Data 25 | { 26 | RE::TESObjectWEAP* weap; 27 | RE::TESObjectREFR* shooter; 28 | RE::BGSProjectile* bproj; 29 | RE::MagicItem* spel; 30 | RE::EffectSetting* mgef; 31 | RE::TESAmmo* ammo; 32 | RE::MagicSystem::CastingSource hand; 33 | 34 | enum class Type : uint32_t 35 | { 36 | Spell, 37 | Arrow, 38 | None 39 | } type; 40 | 41 | RE::Projectile::ProjectileRot rot; 42 | RE::NiPoint3 pos; 43 | 44 | Data(Type type, RE::Projectile::LaunchData* ldata) : 45 | weap(ldata->weaponSource), shooter(ldata->shooter), bproj(ldata->projectileBase), spel(ldata->spell), 46 | mgef((ldata->spell && ldata->spell->As()) ? ldata->spell->GetAVEffect() : nullptr), 47 | ammo(ldata->ammoSource), hand(ldata->castingSource), type(type), rot({ ldata->angleX, ldata->angleZ }), 48 | pos(ldata->origin) 49 | {} 50 | 51 | explicit Data(RE::Projectile* proj) : 52 | weap(proj->weaponSource), shooter(proj->shooter.get().get()), bproj(proj->GetProjectileBase()), spel(proj->spell), 53 | mgef(proj->spell ? proj->spell->GetAVEffect() : nullptr), ammo(proj->ammoSource), hand(proj->castingSource), 54 | type(proj->weaponSource ? Type::Arrow : (proj->spell ? Type::Spell : Type::None)), 55 | rot({ proj->GetAngleX(), proj->GetAngleZ() }), pos(proj->GetPosition()) 56 | {} 57 | 58 | Data(RE::TESObjectWEAP* weap, RE::TESObjectREFR* shooter, RE::BGSProjectile* bproj, RE::MagicItem* spel, 59 | RE::EffectSetting* mgef, RE::TESAmmo* ammo, RE::MagicSystem::CastingSource hand, Type type, 60 | RE::Projectile::ProjectileRot rot, RE::NiPoint3 pos) : 61 | weap(weap), 62 | shooter(shooter), bproj(bproj), spel(spel), mgef(mgef), ammo(ammo), hand(hand), type(type), rot(std::move(rot)), 63 | pos(std::move(pos)) 64 | {} 65 | }; 66 | 67 | void init(const std::string& filename, const Json::Value& json_root); 68 | void clear(); 69 | 70 | // targetOverride used only for Multicast::Evenly support 71 | void eval(Data* data, Event e, RE::Projectile* proj, RE::Actor* targetOverride = nullptr); 72 | 73 | void install(); 74 | } 75 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | extern "C" DLLEXPORT bool SKSEAPI SKSEPlugin_Query(const SKSE::QueryInterface* a_skse, SKSE::PluginInfo* a_info) 2 | { 3 | #ifndef DEBUG 4 | auto sink = std::make_shared(); 5 | #else 6 | auto path = logger::log_directory(); 7 | if (!path) { 8 | return false; 9 | } 10 | 11 | *path /= Version::PROJECT; 12 | *path += ".log"sv; 13 | auto sink = std::make_shared(path->string(), true); 14 | #endif 15 | 16 | auto log = std::make_shared("global log"s, std::move(sink)); 17 | 18 | #ifndef DEBUG 19 | log->set_level(spdlog::level::err); 20 | log->flush_on(spdlog::level::err); 21 | #else 22 | log->set_level(spdlog::level::info); 23 | log->flush_on(spdlog::level::info); 24 | #endif 25 | 26 | spdlog::set_default_logger(std::move(log)); 27 | spdlog::set_pattern("%g(%#): [%^%l%$] %v"s); 28 | 29 | logger::info(FMT_STRING("{} v{}"), Version::PROJECT, Version::NAME); 30 | 31 | a_info->infoVersion = SKSE::PluginInfo::kVersion; 32 | a_info->name = Version::PROJECT.data(); 33 | a_info->version = Version::MAJOR; 34 | 35 | if (a_skse->IsEditor()) { 36 | logger::critical("Loaded in editor, marking as incompatible"sv); 37 | return false; 38 | } 39 | 40 | const auto ver = a_skse->RuntimeVersion(); 41 | if (ver < SKSE::RUNTIME_1_5_39) { 42 | logger::critical(FMT_STRING("Unsupported runtime version {}"), ver.string()); 43 | return false; 44 | } 45 | 46 | return true; 47 | } 48 | 49 | #ifdef WITH_IMGUI 50 | namespace Gui 51 | { 52 | namespace Impl 53 | { 54 | const uint32_t enable_hotkey = 199; // home 55 | const uint32_t hide_hotkey = 207; // end 56 | 57 | bool is_hide_hotkey(RE::ButtonEvent* b) { return b->GetIDCode() == hide_hotkey; } 58 | bool is_enable_hotkey(RE::ButtonEvent* b) { return b->GetIDCode() == enable_hotkey; } 59 | 60 | void show() { ImGui::ShowDemoWindow(); } 61 | } 62 | 63 | void init() 64 | { 65 | using ImGuiHelper = ImguiUtils::ImGuiHelper; 66 | 67 | ImGuiHelper::Initialize(); 68 | } 69 | } 70 | #endif // WITH_IMGUI 71 | 72 | #include "Hooks.h" 73 | #include "json/json.h" 74 | #include 75 | #include "Triggers.h" 76 | #include "Multicast.h" 77 | #include "Homing.h" 78 | #include "Emitters.h" 79 | #include "Followers.h" 80 | 81 | #ifdef VALIDATE 82 | 83 | # define PY_SSIZE_T_CLEAN 84 | # include 85 | 86 | void before_all() 87 | { 88 | logger::info("before before_all"); 89 | 90 | logger::info("before Py_Initialize"); 91 | Py_Initialize(); 92 | logger::info("Py_Initialize OK"); 93 | 94 | logger::info("before import sys"); 95 | PyRun_SimpleString("import sys"); 96 | logger::info("before sys.path.append"); 97 | PyRun_SimpleString("sys.path.append(\"./Data/skse/plugins/\")"); 98 | logger::info("before_all OK"); 99 | } 100 | 101 | void after_all() 102 | { 103 | logger::info("before after_all"); 104 | if (Py_FinalizeEx() < 0) { 105 | logger::error("Finalize error\n"); 106 | } 107 | logger::info("after_all OK"); 108 | } 109 | 110 | bool validate_folder(const std::string& path) 111 | { 112 | logger::info("before validate_folder"); 113 | 114 | std::string schema = path + "/schema.json"; 115 | 116 | std::string ans; 117 | 118 | PyObject *pName, *pModule, *pFunc; 119 | PyObject *pArgs, *pValue; 120 | 121 | const char* pythonfile = "multiply"; 122 | const char* funcname = "validate_all"; 123 | 124 | pName = PyUnicode_DecodeFSDefault(pythonfile); 125 | 126 | pModule = PyImport_Import(pName); 127 | Py_DECREF(pName); 128 | 129 | if (!pModule) { 130 | logger::error("Failed to load {}\n", pythonfile); 131 | return false; 132 | } 133 | 134 | pFunc = PyObject_GetAttrString(pModule, funcname); 135 | 136 | if (!pFunc || !PyCallable_Check(pFunc)) { 137 | logger::error("Cannot find function {}\n", funcname); 138 | Py_XDECREF(pFunc); 139 | Py_DECREF(pModule); 140 | return false; 141 | } 142 | 143 | pArgs = PyTuple_New(2); 144 | 145 | pValue = PyUnicode_FromString(path.c_str()); 146 | if (!pValue) { 147 | Py_DECREF(pArgs); 148 | Py_DECREF(pModule); 149 | logger::error("Cannot convert argument\n"); 150 | return false; 151 | } 152 | PyTuple_SetItem(pArgs, 0, pValue); 153 | 154 | pValue = PyUnicode_FromString(schema.c_str()); 155 | if (!pValue) { 156 | Py_DECREF(pArgs); 157 | Py_DECREF(pModule); 158 | logger::error("Cannot convert argument\n"); 159 | return false; 160 | } 161 | PyTuple_SetItem(pArgs, 1, pValue); 162 | 163 | pValue = PyObject_CallObject(pFunc, pArgs); 164 | Py_DECREF(pArgs); 165 | 166 | if (!pValue) { 167 | Py_DECREF(pFunc); 168 | Py_DECREF(pModule); 169 | PyErr_Print(); 170 | logger::error("Call failed\n"); 171 | return false; 172 | } 173 | 174 | ans = PyBytes_AsString(PyUnicode_AsUTF8String(pValue)); 175 | if (!ans.empty()) 176 | logger::error("{}", ans); 177 | 178 | Py_DECREF(pValue); 179 | Py_XDECREF(pFunc); 180 | Py_DECREF(pModule); 181 | 182 | logger::info("validate_folder OK"); 183 | 184 | return ans.empty(); 185 | } 186 | #endif // VALIDATE 187 | 188 | void read_json() 189 | { 190 | #ifdef VALIDATE 191 | before_all(); 192 | bool valid = validate_folder("./Data/HomingProjectiles"); 193 | after_all(); 194 | 195 | if (!valid) { 196 | logger::error("Some jsons are invalid, skipping"); 197 | return; 198 | } 199 | #endif 200 | 201 | JsonUtils::FormIDsMap::clear(); 202 | 203 | Homing::clear(); 204 | Multicast::clear(); 205 | Emitters::clear(); 206 | Followers::clear(); 207 | 208 | Triggers::clear(); 209 | 210 | namespace fs = std::filesystem; 211 | 212 | for (const auto& entry : fs::directory_iterator("Data/HomingProjectiles")) { 213 | Json::Value json_root; 214 | std::ifstream ifs; 215 | const auto& path = entry.path(); 216 | if (path.extension() == ".json" && path.filename() != "schema.json") { 217 | ifs.open(entry); 218 | ifs >> json_root; 219 | ifs.close(); 220 | 221 | auto filename = path.filename().string(); 222 | 223 | JsonUtils::FormIDsMap::init(filename, json_root); 224 | 225 | Homing::init_keys(filename, json_root); 226 | Multicast::init_keys(filename, json_root); 227 | Emitters::init_keys(filename, json_root); 228 | Followers::init_keys(filename, json_root); 229 | 230 | Homing::init(filename, json_root); 231 | Multicast::init(filename, json_root); 232 | Emitters::init(filename, json_root); 233 | Followers::init(filename, json_root); 234 | 235 | Triggers::init(filename, json_root); 236 | } 237 | } 238 | 239 | // Used only while reading json 240 | Homing::clear_keys(); 241 | Multicast::clear_keys(); 242 | Emitters::clear_keys(); 243 | Followers::clear_keys(); 244 | } 245 | 246 | void reset_json() 247 | { 248 | read_json(); 249 | } 250 | 251 | class Settings : public SettingsBase 252 | { 253 | static constexpr auto path = "Data/HomingProjectiles/settings.ini"; 254 | 255 | public: 256 | 257 | class ReloadHotkey 258 | { 259 | enum class AddKeys : uint32_t 260 | { 261 | Shift, 262 | Ctrl, 263 | Alt, 264 | 265 | Total 266 | }; 267 | static inline constexpr size_t AddKeysTotal = static_cast(AddKeys::Total); 268 | 269 | static inline std::array additionals = { {} }; 270 | static inline int key = 71; 271 | 272 | static void strip(std::string& str) 273 | { 274 | if (str.length() == 0) { 275 | return; 276 | } 277 | 278 | auto start_it = str.begin(); 279 | auto end_it = str.rbegin(); 280 | while (std::isspace(*start_it)) { 281 | ++start_it; 282 | if (start_it == str.end()) 283 | break; 284 | } 285 | while (std::isspace(*end_it)) { 286 | ++end_it; 287 | if (end_it == str.rend()) 288 | break; 289 | } 290 | auto start_pos = start_it - str.begin(); 291 | auto end_pos = end_it.base() - str.begin(); 292 | str = start_pos <= end_pos ? std::string(start_it, end_it.base()) : ""; 293 | } 294 | 295 | static void load_key(std::string s) 296 | { 297 | using K = RE::BSKeyboardDevice::Key; 298 | 299 | strip(s); 300 | int keycode = std::stoi(s); 301 | 302 | AddKeys type = AddKeys::Total; 303 | switch (keycode) { 304 | case K::kRightAlt: 305 | case K::kLeftAlt: 306 | type = AddKeys::Alt; 307 | break; 308 | case K::kLeftControl: 309 | case K::kRightControl: 310 | type = AddKeys::Ctrl; 311 | break; 312 | case K::kLeftShift: 313 | case K::kRightShift: 314 | type = AddKeys::Shift; 315 | break; 316 | default: 317 | break; 318 | } 319 | 320 | if (type != AddKeys::Total) { 321 | additionals[static_cast(type)] = keycode; 322 | } else { 323 | key = keycode; 324 | } 325 | } 326 | 327 | static bool isPressed_adds() 328 | { 329 | bool ans = true; 330 | for (int k : additionals) { 331 | ans = ans && (k == 0 || RE::BSInputDeviceManager::GetSingleton()->GetKeyboard()->IsPressed(k)); 332 | } 333 | return ans; 334 | } 335 | 336 | public: 337 | static void load(const CSimpleIniA& ini) { 338 | std::string keys; 339 | if (ReadString(ini, "General", "reload_key", keys)) { 340 | size_t pos = 0; 341 | std::string token; 342 | while ((pos = keys.find('+')) != std::string::npos) { 343 | load_key(keys.substr(0, pos)); 344 | keys.erase(0, pos + 1); 345 | } 346 | load_key(keys); 347 | } 348 | } 349 | 350 | static bool isPressed(int k) { return k == key && isPressed_adds(); } 351 | }; 352 | 353 | static void load() { 354 | CSimpleIniA ini; 355 | ini.LoadFile(path); 356 | 357 | ReloadHotkey::load(ini); 358 | } 359 | }; 360 | 361 | class InputHandler : public RE::BSTEventSink 362 | { 363 | public: 364 | static InputHandler* GetSingleton() 365 | { 366 | static InputHandler singleton; 367 | return std::addressof(singleton); 368 | } 369 | 370 | RE::BSEventNotifyControl ProcessEvent(RE::InputEvent* const* evns, RE::BSTEventSource*) override 371 | { 372 | if (!*evns) 373 | return RE::BSEventNotifyControl::kContinue; 374 | 375 | for (RE::InputEvent* e = *evns; e; e = e->next) { 376 | if (auto buttonEvent = e->AsButtonEvent(); buttonEvent && buttonEvent->HasIDCode() && buttonEvent->IsDown()) { 377 | if (Settings::ReloadHotkey::isPressed(buttonEvent->GetIDCode())) { 378 | reset_json(); 379 | } 380 | } 381 | } 382 | return RE::BSEventNotifyControl::kContinue; 383 | } 384 | 385 | void enable() 386 | { 387 | if (auto input = RE::BSInputDeviceManager::GetSingleton()) { 388 | input->AddEventSink(this); 389 | } 390 | } 391 | }; 392 | 393 | static void SKSEMessageHandler(SKSE::MessagingInterface::Message* message) 394 | { 395 | #ifdef WITH_DRAWING 396 | DebugRenderUtils::OnMessage(message); 397 | #endif // WITH_DRAWING 398 | 399 | switch (message->type) { 400 | case SKSE::MessagingInterface::kPostLoad: 401 | Hooks::PaddingsProjectileHook::Hook(); 402 | break; 403 | 404 | case SKSE::MessagingInterface::kDataLoaded: 405 | Hooks::MultipleBeamsHook::Hook(); 406 | Hooks::NormLightingsHook::Hook(); 407 | Triggers::install(); 408 | Homing::install(); 409 | Multicast::install(); 410 | Emitters::install(); 411 | Followers::install(); 412 | read_json(); 413 | InputHandler::GetSingleton()->enable(); 414 | Settings::load(); 415 | 416 | #ifdef WITH_IMGUI 417 | Gui::init(); 418 | #endif // WITH_IMGUI 419 | break; 420 | } 421 | } 422 | 423 | extern "C" DLLEXPORT bool SKSEAPI SKSEPlugin_Load(const SKSE::LoadInterface* a_skse) 424 | { 425 | auto g_messaging = reinterpret_cast(a_skse->QueryInterface(SKSE::LoadInterface::kMessaging)); 426 | if (!g_messaging) { 427 | logger::critical("Failed to load messaging interface! This error is fatal, plugin will not load."); 428 | return false; 429 | } 430 | 431 | logger::info("loaded"); 432 | 433 | SKSE::Init(a_skse); 434 | SKSE::AllocTrampoline(1 << 10); 435 | 436 | #ifdef WITH_DRAWING 437 | DebugRenderUtils::UpdateHooks::Hook(); 438 | #endif // WITH_DRAWING 439 | 440 | g_messaging->RegisterListener("SKSE", SKSEMessageHandler); 441 | 442 | return true; 443 | } 444 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "templateplugin", 3 | "version-string": "1.0.0", 4 | "description": "Here should be description", 5 | "homepage": "https://github.com/Ryan-rsm-McKenzie/ExamplePlugin-CommonLibSSE", 6 | "license": "MIT", 7 | "dependencies": [ 8 | "boost-atomic", 9 | "boost-stl-interfaces", 10 | "glm", 11 | { 12 | "name" : "imgui", 13 | "features": [ "win32-binding", "dx11-binding" ] 14 | }, 15 | "jsoncpp", 16 | "magic-enum", 17 | "rsm-binary-io", 18 | "simpleini", 19 | "span-lite", 20 | "spdlog", 21 | "xbyak" 22 | ] 23 | } 24 | --------------------------------------------------------------------------------