├── !Update.ps1
├── .clang-format
├── .clang-format-ignore
├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ ├── deploy.yml
│ └── maintenance.yml
├── .gitignore
├── .vscode
└── settings.json
├── CMakeLists.txt
├── CMakePresets.json
├── LICENSE
├── README.md
├── build-release.ps1
├── cmake
├── Plugin.h.in
├── config.cmake.in
└── version.rc.in
├── docs
├── .vitepress
│ └── config.ts
├── config
│ ├── config-workflow.md
│ ├── data-types.md
│ ├── examples.md
│ ├── file-helpers.md
│ ├── references.md
│ ├── schema-whatnot.md
│ ├── setup-proxy.md
│ └── snippets
│ │ ├── NoEnchantmentRestrictionRemake.md
│ │ ├── SF_NativeAutoHDR.md
│ │ └── YouCanSleepRemake.md
├── dkutil
│ ├── about.md
│ ├── compilation.md
│ └── references.md
├── extra
│ └── skse.md
├── hooks
│ ├── address-fetching.md
│ ├── asm-patch.md
│ ├── callsite-logging.md
│ ├── cave-hook.md
│ ├── hook-handles.md
│ ├── hooking-with-dkutil.md
│ ├── iat-swap.md
│ ├── lto-enabled-hook.md
│ ├── memory-edit.md
│ ├── references.md
│ ├── relocation.md
│ ├── trampoline.md
│ ├── useful-helpers.md
│ └── vtable-swap.md
├── index.md
├── logger
│ ├── macros.md
│ └── setup.md
└── utils
│ ├── enumeration.md
│ ├── numbers.md
│ ├── string.md
│ └── templates.md
├── format-all.ps1
├── generate-sln.ps1
├── include
├── DKUtil
│ ├── Config.hpp
│ ├── Extra.hpp
│ ├── Hook.hpp
│ ├── Impl
│ │ ├── Config
│ │ │ ├── Data.hpp
│ │ │ ├── Ini.hpp
│ │ │ ├── Json.hpp
│ │ │ ├── Proxy.hpp
│ │ │ ├── Schema.hpp
│ │ │ ├── Shared.hpp
│ │ │ └── Toml.hpp
│ │ ├── Extra
│ │ │ ├── Serialization
│ │ │ │ ├── exception.hpp
│ │ │ │ ├── interface.hpp
│ │ │ │ ├── mock.hpp
│ │ │ │ ├── resolver.hpp
│ │ │ │ ├── shared.hpp
│ │ │ │ └── variant.hpp
│ │ │ ├── xconsole.hpp
│ │ │ └── xserialize.hpp
│ │ ├── Hook
│ │ │ ├── API.hpp
│ │ │ ├── Assembly.hpp
│ │ │ ├── Internal.hpp
│ │ │ ├── Internal
│ │ │ │ ├── ASMPatch.hpp
│ │ │ │ ├── CaveHook.hpp
│ │ │ │ ├── IATHook.hpp
│ │ │ │ ├── RelHook.hpp
│ │ │ │ └── VMTHook.hpp
│ │ │ ├── JIT.hpp
│ │ │ ├── Shared.hpp
│ │ │ ├── Shared_Compat.hpp
│ │ │ └── Trampoline.hpp
│ │ ├── PCH.hpp
│ │ └── Utility
│ │ │ ├── enumeration.hpp
│ │ │ ├── numbers.hpp
│ │ │ ├── shared.hpp
│ │ │ ├── string.hpp
│ │ │ └── templates.hpp
│ ├── Logger.hpp
│ └── Utility.hpp
└── external
│ └── toml.hpp
├── package.json
├── pnpm-lock.yaml
├── test
├── ConfigTest.h
├── HookTest.h
├── LoggerTest.h
├── PCH.h
├── PluginTest.cpp
├── SSEExtraTest.h
├── UtilityTest.h
└── configs
│ ├── AnotherBiteTheConfig.ini
│ ├── DKUtilDebugger.ini
│ ├── DKUtilDebugger.json
│ ├── DKUtilDebugger.toml
│ └── Schema_SC.txt
└── vcpkg.json
/.clang-format:
--------------------------------------------------------------------------------
1 | ---
2 | AccessModifierOffset: -4
3 | AlignAfterOpenBracket: DontAlign
4 | AlignConsecutiveAssignments: false
5 | AlignConsecutiveBitFields: false
6 | AlignConsecutiveDeclarations: true
7 | AlignConsecutiveMacros: false
8 | AlignEscapedNewlines: Left
9 | AlignOperands: Align
10 | AlignTrailingComments: true
11 | AllowAllArgumentsOnNextLine: false
12 | AllowAllParametersOfDeclarationOnNextLine: false
13 | AllowShortBlocksOnASingleLine: Empty
14 | AllowShortCaseLabelsOnASingleLine: false
15 | AllowShortEnumsOnASingleLine: true
16 | AllowShortFunctionsOnASingleLine: All
17 | AllowShortIfStatementsOnASingleLine: Never
18 | AllowShortLambdasOnASingleLine: All
19 | AllowShortLoopsOnASingleLine: true
20 | AlwaysBreakAfterReturnType: None
21 | AlwaysBreakBeforeMultilineStrings: true
22 | AlwaysBreakTemplateDeclarations: Yes
23 | BinPackArguments: true
24 | BinPackParameters: true
25 | BitFieldColonSpacing: After
26 | BraceWrapping:
27 | AfterCaseLabel: true
28 | AfterClass: true
29 | AfterControlStatement: false
30 | AfterEnum: true
31 | AfterFunction: true
32 | AfterNamespace: true
33 | AfterStruct: true
34 | AfterUnion: true
35 | AfterExternBlock: true
36 | BeforeCatch: false
37 | BeforeElse: false
38 | BeforeLambdaBody: false
39 | BeforeWhile: false
40 | IndentBraces: false
41 | SplitEmptyFunction: false
42 | SplitEmptyRecord: false
43 | SplitEmptyNamespace: false
44 | BreakBeforeBinaryOperators: None
45 | BreakBeforeBraces: Custom
46 | BreakBeforeTernaryOperators: false
47 | BreakConstructorInitializers: AfterColon
48 | BreakInheritanceList: AfterColon
49 | BreakStringLiterals: true
50 | ColumnLimit: 0
51 | CompactNamespaces: false
52 | ConstructorInitializerIndentWidth: 4
53 | ContinuationIndentWidth: 4
54 | Cpp11BracedListStyle: false
55 | DerivePointerAlignment: false
56 | DisableFormat: false
57 | FixNamespaceComments: false
58 | IncludeBlocks: Preserve
59 | IndentCaseBlocks: true
60 | IndentCaseLabels: false
61 | IndentExternBlock: Indent
62 | IndentGotoLabels: false
63 | IndentPPDirectives: AfterHash
64 | IndentWidth: 4
65 | IndentWrappedFunctionNames: true
66 | KeepEmptyLinesAtTheStartOfBlocks: false
67 | Language: Cpp
68 | MaxEmptyLinesToKeep: 1
69 | NamespaceIndentation: All
70 | PointerAlignment: Left
71 | ReflowComments: false
72 | SortIncludes: true
73 | SortUsingDeclarations: true
74 | SpaceAfterCStyleCast: false
75 | SpaceAfterLogicalNot: false
76 | SpaceAfterTemplateKeyword: true
77 | SpaceBeforeAssignmentOperators: true
78 | SpaceBeforeCpp11BracedList: false
79 | SpaceBeforeCtorInitializerColon: true
80 | SpaceBeforeInheritanceColon: true
81 | SpaceBeforeParens: ControlStatements
82 | SpaceBeforeRangeBasedForLoopColon: true
83 | SpaceBeforeSquareBrackets: false
84 | SpaceInEmptyBlock: false
85 | SpaceInEmptyParentheses: false
86 | SpacesBeforeTrailingComments: 2
87 | SpacesInAngles: false
88 | SpacesInCStyleCastParentheses: false
89 | SpacesInConditionalStatement: false
90 | SpacesInContainerLiterals: true
91 | SpacesInParentheses: false
92 | SpacesInSquareBrackets: false
93 | Standard: Latest
94 | TabWidth: 4
95 | UseTab: AlignWithSpaces
96 | LineEnding: CRLF
97 | PackConstructorInitializers: BinPack
98 |
--------------------------------------------------------------------------------
/.clang-format-ignore:
--------------------------------------------------------------------------------
1 | test/*
2 | include/external/*
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset = utf-8
3 | insert_final_newline = true
4 |
5 | [*.{asm,c,cc,cpp,cxx,h,hpp,hxx,inc,inl,ixx}]
6 | indent_style = tab
7 | indent_size = 4
8 |
9 | [*.{ini,json,toml,xml}]
10 | indent_style = space
11 | indent_size = 2
12 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | \!Update.ps1 linguist-vendored
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy GH Pages
2 |
3 | on:
4 | push:
5 | branches: master
6 | paths:
7 | - "docs/**"
8 | workflow_dispatch:
9 |
10 | permissions:
11 | contents: read
12 | pages: write
13 | id-token: write
14 |
15 | concurrency:
16 | group: pages
17 | cancel-in-progress: false
18 |
19 | jobs:
20 | build:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v4
25 | with:
26 | fetch-depth: 0
27 | - uses: pnpm/action-setup@v3
28 | with:
29 | version: 8
30 | - name: Setup Node
31 | uses: actions/setup-node@v4
32 | with:
33 | node-version: 20
34 | cache: pnpm
35 | - name: Setup Pages
36 | uses: actions/configure-pages@v4
37 | - name: Install dependencies
38 | run: pnpm i
39 | - name: Build VitePress
40 | run: pnpm docs:build
41 | - name: Upload artifact
42 | uses: actions/upload-pages-artifact@v3
43 | with:
44 | path: docs/.vitepress/dist
45 |
46 | deploy:
47 | environment:
48 | name: github-pages
49 | url: ${{ steps.deployment.outputs.page_url }}
50 | needs: build
51 | runs-on: ubuntu-latest
52 | name: Deploy
53 | steps:
54 | - name: Deploy Pages
55 | id: deployment
56 | uses: actions/deploy-pages@v4
57 |
--------------------------------------------------------------------------------
/.github/workflows/maintenance.yml:
--------------------------------------------------------------------------------
1 | name: Maintenance
2 |
3 | on:
4 | push:
5 | branches: master
6 | paths:
7 | - "include/**"
8 | workflow_dispatch:
9 |
10 | jobs:
11 | maintenance:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - uses: DoozyX/clang-format-lint-action@v0.16.2
18 | with:
19 | source: '.'
20 | exclude: './docs'
21 | extensions: 'c,cc,cpp,cppm,cxx,h,hpp,hxx,inl,inc,ixx,mxx'
22 | clangFormatVersion: 16
23 | inplace: True
24 |
25 | - uses: EndBug/add-and-commit@v9
26 | with:
27 | committer_name: clang-format
28 | committer_email: 41898282+github-actions[bot]@users.noreply.github.com
29 | message: 'chore: style formatting'
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | node_modules/
3 | docs/.vitepress/dist
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cmake.configureOnOpen": false
3 | }
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.19)
2 |
3 | # singleton target across multiple projects
4 | if(TARGET DKUtil)
5 | return()
6 | endif()
7 |
8 | # standards & flags
9 | set(CMAKE_CXX_STANDARD 23)
10 | set(CMAKE_CXX_STANDARD_REQUIRED ON)
11 | set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
12 | set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_DEBUG OFF)
13 | set_property(GLOBAL PROPERTY USE_FOLDERS ON)
14 | set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE FILEPATH "")
15 |
16 | include(GNUInstallDirs)
17 |
18 | if(NOT DKUTIL_DEBUG_BUILD)
19 |
20 | # info
21 | project(
22 | DKUtil
23 | VERSION 2.0.0
24 | LANGUAGES CXX
25 | )
26 |
27 | # out-of-source builds only
28 | if(${PROJECT_SOURCE_DIR} STREQUAL ${PROJECT_BINARY_DIR})
29 | message(FATAL_ERROR "In-source builds are not allowed.")
30 | endif()
31 |
32 | # dependencies
33 | find_package(nlohmann_json CONFIG REQUIRED)
34 | find_package(spdlog CONFIG REQUIRED)
35 | find_package(xbyak CONFIG REQUIRED)
36 |
37 | # header only library
38 | add_library(
39 | ${PROJECT_NAME}
40 | INTERFACE
41 | )
42 |
43 | add_library(
44 | ${PROJECT_NAME}::${PROJECT_NAME}
45 | ALIAS
46 | ${PROJECT_NAME}
47 | )
48 |
49 | target_include_directories(
50 | ${PROJECT_NAME}
51 | INTERFACE
52 | $
53 | )
54 |
55 | # install
56 | install(
57 | TARGETS ${PROJECT_NAME}
58 | EXPORT ${PROJECT_NAME}-targets
59 | )
60 |
61 | install(
62 | EXPORT ${PROJECT_NAME}-targets
63 | NAMESPACE ${PROJECT_NAME}::
64 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
65 | )
66 |
67 | configure_file(
68 | cmake/config.cmake.in
69 | ${PROJECT_NAME}Config.cmake
70 | @ONLY
71 | )
72 |
73 | install(
74 | FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
75 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
76 | )
77 |
78 | install(
79 | DIRECTORY include
80 | DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/include
81 | )
82 |
83 | else()
84 |
85 | # info
86 | project(
87 | DKUtilDebugger
88 | VERSION 2.0.0
89 | LANGUAGES CXX
90 | )
91 |
92 | # out-of-source builds only
93 | if(${PROJECT_SOURCE_DIR} STREQUAL ${PROJECT_BINARY_DIR})
94 | message(FATAL_ERROR "In-source builds are not allowed.")
95 | endif()
96 |
97 | # in-place configuration
98 | configure_file(
99 | ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Plugin.h.in
100 | ${CMAKE_CURRENT_BINARY_DIR}/include/Plugin.h
101 | @ONLY
102 | )
103 |
104 | configure_file(
105 | ${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.rc.in
106 | ${CMAKE_CURRENT_BINARY_DIR}/version.rc
107 | @ONLY
108 | )
109 |
110 | # source files
111 | execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/!Update.ps1" "SOURCEGEN" "${PROJECT_VERSION}" "${CMAKE_CURRENT_BINARY_DIR}")
112 | include(${CMAKE_CURRENT_BINARY_DIR}/sourcelist.cmake)
113 | source_group(
114 | TREE ${CMAKE_CURRENT_SOURCE_DIR}
115 | FILES ${SOURCES}
116 | )
117 |
118 | source_group(
119 | TREE ${CMAKE_CURRENT_BINARY_DIR}
120 | FILES ${CMAKE_CURRENT_BINARY_DIR}/include/Plugin.h
121 | )
122 |
123 | # dependencies
124 | add_subdirectory($ENV{CommonLibSSEPath} CommonLibSSE)
125 | find_package(nlohmann_json CONFIG REQUIRED)
126 | find_package(spdlog CONFIG REQUIRED)
127 | find_package(xbyak CONFIG REQUIRED)
128 |
129 | # runtime
130 | if(DKU_CONSOLE)
131 | add_executable(
132 | ${PROJECT_NAME}
133 | ${SOURCES}
134 | ${CMAKE_CURRENT_BINARY_DIR}/include/Plugin.h
135 | ${CMAKE_CURRENT_BINARY_DIR}/version.rc
136 | .clang-format
137 | vcpkg.json
138 | )
139 | target_compile_definitions(
140 | ${PROJECT_NAME}
141 | PRIVATE
142 | DKU_CONSOLE=1
143 | )
144 | else()
145 | add_library(
146 | ${PROJECT_NAME}
147 | SHARED
148 | ${SOURCES}
149 | ${CMAKE_CURRENT_BINARY_DIR}/include/Plugin.h
150 | ${CMAKE_CURRENT_BINARY_DIR}/version.rc
151 | .clang-format
152 | vcpkg.json
153 | )
154 | endif()
155 |
156 | # include dir
157 | target_include_directories(
158 | ${PROJECT_NAME}
159 | PRIVATE
160 | ${CMAKE_CURRENT_BINARY_DIR}/include
161 | ${CMAKE_CURRENT_SOURCE_DIR}/include
162 | ${CMAKE_CURRENT_SOURCE_DIR}/src
163 | )
164 |
165 | # linkage
166 | target_link_libraries(
167 | ${PROJECT_NAME}
168 | PRIVATE
169 | CommonLibSSE::CommonLibSSE
170 | nlohmann_json::nlohmann_json
171 | spdlog::spdlog
172 | xbyak::xbyak
173 | )
174 |
175 | # compiler def
176 | if (MSVC)
177 | add_compile_definitions(_UNICODE)
178 |
179 | target_compile_options(
180 | ${PROJECT_NAME}
181 | PRIVATE
182 | /MP
183 | /await
184 | /W0
185 | /WX
186 | /permissive-
187 | /Zc:alignedNew
188 | /Zc:auto
189 | /Zc:__cplusplus
190 | /Zc:externC
191 | /Zc:externConstexpr
192 | /Zc:forScope
193 | /Zc:hiddenFriend
194 | /Zc:implicitNoexcept
195 | /Zc:lambda
196 | /Zc:noexceptTypes
197 | /Zc:preprocessor
198 | /Zc:referenceBinding
199 | /Zc:rvalueCast
200 | /Zc:sizedDealloc
201 | /Zc:strictStrings
202 | /Zc:ternary
203 | /Zc:threadSafeInit
204 | /Zc:trigraphs
205 | /Zc:wchar_t
206 | /wd4200 # nonstandard extension used : zero-sized array in struct/union
207 | )
208 | endif()
209 |
210 | # PCH
211 | target_precompile_headers(
212 | ${PROJECT_NAME}
213 | PRIVATE
214 | test/PCH.h
215 | )
216 |
217 | if(NOT DKU_CONSOLE)
218 | # post build event
219 | add_custom_command(
220 | TARGET
221 | ${PROJECT_NAME}
222 | POST_BUILD
223 | COMMAND
224 | powershell -NoProfile -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/!Update.ps1" "COPY" "${PROJECT_VERSION}" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$(ConfigurationName)" "${PROJECT_NAME}" "${ANNIVERSARY_EDITION}"
225 | COMMENT
226 | "Updating project :))"
227 | )
228 | else()
229 | add_custom_command(
230 | TARGET
231 | ${PROJECT_NAME}
232 | POST_BUILD
233 | COMMAND
234 | ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/test/configs" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$(ConfigurationName)"
235 | )
236 | endif()
237 |
238 | # called via !Rebuild.ps1
239 | if(DEFINED GROUP)
240 | set_target_properties(
241 | ${PROJECT_NAME}
242 | PROPERTIES
243 | FOLDER
244 | ${GROUP}
245 | )
246 | endif()
247 |
248 | set(DKUTIL_DEBUG_BUILD OFF)
249 | add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR} DKUtil)
250 |
251 | endif()
252 |
--------------------------------------------------------------------------------
/CMakePresets.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "cmakeMinimumRequired": {
4 | "major": 3,
5 | "minor": 21,
6 | "patch": 0
7 | },
8 | "configurePresets": [
9 | {
10 | "name": "common",
11 | "hidden": true,
12 | "cacheVariables": {
13 | "F4SE_SUPPORT_XBYAK": "ON",
14 | "SKSE_SUPPORT_XBYAK": "ON",
15 | "SFSE_SUPPORT_XBYAK": "ON",
16 | "SKSEPluginAuthor": "$env{SKSEPluginAuthor}"
17 | }
18 | },
19 | {
20 | "name": "vcpkg",
21 | "hidden": true,
22 | "cacheVariables": {
23 | "CMAKE_TOOLCHAIN_FILE": {
24 | "type": "STRING",
25 | "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
26 | },
27 | "VCPKG_TARGET_TRIPLET": "x64-windows-static-md"
28 | }
29 | },
30 | {
31 | "name": "win64",
32 | "hidden": true,
33 | "architecture": "x64",
34 | "cacheVariables": {
35 | "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded$<$:Debug>DLL"
36 | }
37 | },
38 | {
39 | "name": "msvc",
40 | "hidden": true,
41 | "cacheVariables": {
42 | "CMAKE_CXX_FLAGS": "/EHsc /MP /W4 /WX $penv{CXXFLAGS}"
43 | },
44 | "generator": "Visual Studio 17 2022",
45 | "vendor": {
46 | "microsoft.com/VisualStudioSettings/CMake/1.0": {
47 | "intelliSenseMode": "windows-msvc-x64",
48 | "enableMicrosoftCodeAnalysis": true,
49 | "enableClangTidyCodeAnalysis": true
50 | }
51 | }
52 | },
53 | {
54 | "name": "AE",
55 | "cacheVariables": {
56 | "ENABLE_SKYRIM_AE": "ON",
57 | "ENABLE_SKYRIM_SE": "OFF",
58 | "ENABLE_SKYRIM_VR": "OFF"
59 | },
60 | "inherits": [
61 | "common",
62 | "vcpkg",
63 | "win64",
64 | "msvc"
65 | ]
66 | },
67 | {
68 | "name": "SE",
69 | "cacheVariables": {
70 | "ENABLE_SKYRIM_AE": "OFF",
71 | "ENABLE_SKYRIM_SE": "ON",
72 | "ENABLE_SKYRIM_VR": "OFF"
73 | },
74 | "inherits": [
75 | "common",
76 | "vcpkg",
77 | "win64",
78 | "msvc"
79 | ]
80 | },
81 | {
82 | "name": "VR",
83 | "cacheVariables": {
84 | "ENABLE_SKYRIM_AE": "OFF",
85 | "ENABLE_SKYRIM_SE": "OFF",
86 | "ENABLE_SKYRIM_VR": "ON"
87 | },
88 | "inherits": [
89 | "common",
90 | "vcpkg",
91 | "win64",
92 | "msvc"
93 | ]
94 | },
95 | {
96 | "name": "ALL",
97 | "cacheVariables": {
98 | "ENABLE_SKYRIM_AE": "ON",
99 | "ENABLE_SKYRIM_SE": "ON",
100 | "ENABLE_SKYRIM_VR": "ON"
101 | },
102 | "inherits": [
103 | "common",
104 | "vcpkg",
105 | "win64",
106 | "msvc"
107 | ]
108 | },
109 | {
110 | "name": "PRE-AE",
111 | "cacheVariables": {
112 | "ENABLE_SKYRIM_AE": "OFF",
113 | "ENABLE_SKYRIM_SE": "ON",
114 | "ENABLE_SKYRIM_VR": "ON"
115 | },
116 | "inherits": [
117 | "common",
118 | "vcpkg",
119 | "win64",
120 | "msvc"
121 | ]
122 | },
123 | {
124 | "name": "FLATRIM",
125 | "cacheVariables": {
126 | "ENABLE_SKYRIM_AE": "ON",
127 | "ENABLE_SKYRIM_SE": "ON",
128 | "ENABLE_SKYRIM_VR": "OFF"
129 | },
130 | "inherits": [
131 | "common",
132 | "vcpkg",
133 | "win64",
134 | "msvc"
135 | ]
136 | }
137 | ]
138 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 DK
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | DKUtil
2 |
3 | Some utility headers to help with x64 native plugin development
4 |
5 | # Documentation
6 | See [wiki here!](https://gottyduke.github.io/DKUtil/)
7 |
8 | # Implementations
9 | 
10 | 
11 | 
12 | 
13 | 
14 |
15 | + Config
16 | - abstracted and contained config layer
17 | - `ini`, `toml`, `json` file support
18 | - `bool`, `int64_t`, `double`, `string` type support
19 | - built in array support
20 | - multiple file loads & generate default file
21 | - custom formatted string parser to c++ structure
22 | + Hook
23 | - pattern scanner
24 | - asm patch
25 | - cave hook
26 | - virtual method table swap
27 | - import address table swap
28 | - simple function hook (write_call/write_branch)
29 | - non-volatile call (LTO enabled hooks)
30 | - various usefully gathered utils
31 | + Logge
32 | - logging macros
33 | + Utility
34 | + function
35 | + `consteval` helper functions retrieving the argument count of a function.
36 | + model
37 | + `Singleton` data model abstract class to save boilerplate code.
38 | + `enumeration` addition to the original `RE::stl::enumeration`.
39 | + static reflection for enum name, type name and value name, support value_type(`n`) and flag_type(`1<MIT License, 2020-present DK
56 |
--------------------------------------------------------------------------------
/build-release.ps1:
--------------------------------------------------------------------------------
1 | Remove-Item $PSScriptRoot/build -Recurse -Force -ErrorAction:SilentlyContinue -Confirm:$False | Out-Null
2 | & cmake -B $PSScriptRoot/build -S $PSScriptRoot/BG3WASD --preset=REL -DPLUGIN_MODE:BOOL=TRUE
3 | & cmake --build $PSScriptRoot/build --config Release
--------------------------------------------------------------------------------
/cmake/Plugin.h.in:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 |
4 | namespace Plugin
5 | {
6 | inline constexpr auto NAME = "@PROJECT_NAME@"sv;
7 | inline constexpr auto AUTHOR = "@SKSEPluginAuthor@"sv;
8 | inline constexpr REL::Version Version
9 | {
10 | @PROJECT_VERSION_MAJOR@u,
11 | @PROJECT_VERSION_MINOR@u,
12 | @PROJECT_VERSION_PATCH@u
13 | };
14 | }
15 |
16 |
17 | DLLEXPORT constinit auto SKSEPlugin_Version = []() noexcept
18 | {
19 | SKSE::PluginVersionData data{};
20 |
21 | data.PluginVersion(Plugin::Version);
22 | data.PluginName(Plugin::NAME);
23 | data.AuthorName(Plugin::AUTHOR);
24 | data.UsesAddressLibrary(true);
25 | data.HasNoStructUse(true);
26 |
27 | return data;
28 | }();
29 |
30 |
31 | DLLEXPORT bool SKSEAPI SKSEPlugin_Query(const SKSE::QueryInterface*, SKSE::PluginInfo* pluginInfo)
32 | {
33 | pluginInfo->name = SKSEPlugin_Version.pluginName;
34 | pluginInfo->infoVersion = SKSE::PluginInfo::kVersion;
35 | pluginInfo->version = SKSEPlugin_Version.pluginVersion;
36 |
37 | return true;
38 | }
39 |
--------------------------------------------------------------------------------
/cmake/config.cmake.in:
--------------------------------------------------------------------------------
1 | include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@-targets.cmake")
2 | include(CMakeFindDependencyMacro)
3 |
4 | find_path(SIMPLEINI_INCLUDE_DIRS "SimpleIni.h")
5 | find_dependency(imgui CONFIG)
6 | find_dependency(nlohmann_json CONFIG)
7 | find_dependency(spdlog CONFIG)
8 | find_dependency(tomlplusplus CONFIG)
9 | find_dependency(xbyak CONFIG)
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, type DefaultTheme } from 'vitepress'
2 |
3 | const commit = await (await fetch("https://api.github.com/repos/gottyduke/dkutil/commits/master")).json()
4 | const lastUpdate = new Date(commit.commit.author.date).toISOString().slice(0, 10)
5 |
6 | export default defineConfig({
7 | base: '/DKUtil/',
8 |
9 | title: 'DKUtil',
10 | description: 'Some utility headers to help with windows x64 native plugin development',
11 | lang: 'en-US',
12 |
13 | lastUpdated: true,
14 | cleanUrls: true,
15 | metaChunk: true,
16 |
17 | themeConfig: {
18 | socialLinks: [
19 | { icon: 'github', link: 'https://github.com/gottyduke' }
20 | ],
21 |
22 | search: {
23 | provider: 'local',
24 | },
25 |
26 | nav: makeNavBar(),
27 |
28 | sidebar: {
29 | '/dkutil/': { base: '/dkutil/', items: makeSidebarDKUtil() },
30 | '/logger/': { base: '/logger/', items: makeSidebarLogger() },
31 | '/config/': { base: '/config/', items: makeSidebarConfig() },
32 | '/hooks/': { base: '/hooks/', items: makeSidebarHooks() },
33 | '/utils/': { base: '/utils/', items: makeSidebarUtils() },
34 | '/extra/': { base: '/extra/', items: makeSidebarExtra() },
35 | },
36 |
37 | editLink: {
38 | pattern: 'https://github.com/gottyduke/dkutil/edit/master/docs/:path',
39 | text: 'Edit current page on GitHub'
40 | },
41 |
42 | footer: {
43 | message: 'Released under the MIT License',
44 | copyright: 'Copyright © 2020-present DK'
45 | },
46 | }
47 | })
48 |
49 | function makeNavBar(): DefaultTheme.NavItem[] {
50 | return [
51 | {
52 | text: `DKUtil(${lastUpdate})`,
53 | items: [
54 | {
55 | text: 'About this Project',
56 | link: '/dkutil/about',
57 | activeMatch: '/dkutil/'
58 | },
59 | {
60 | text: 'Projects with DKUtil',
61 | link: '/dkutil/references',
62 | activeMatch: '/dkutil/'
63 | },
64 | ]
65 | },
66 | {
67 | text: 'Logger',
68 | link: '/logger/setup',
69 | activeMatch: '/logger/'
70 | },
71 | {
72 | text: 'Config',
73 | link: '/config/config-workflow',
74 | activeMatch: '/config/'
75 | },
76 | {
77 | text: 'Hook',
78 | link: '/hooks/hooking-with-dkutil',
79 | activeMatch: '/hooks/'
80 | },
81 | {
82 | text: 'Utils',
83 | link: '/utils/templates',
84 | activeMatch: '/utils/'
85 | },
86 | {
87 | text: 'Extra',
88 | link: '/extra/skse',
89 | activeMatch: '/extra/'
90 | },
91 | ]
92 | }
93 |
94 | function makeSidebarDKUtil(): DefaultTheme.SidebarItem[] {
95 | return [
96 | {
97 | text: 'DKUtil',
98 | collapsed: false,
99 | items: [
100 | { text: 'About this Project', link: 'about' },
101 | { text: 'Compilation Flags', link: 'compilation' },
102 | { text: 'Projects with DKUtil', link: 'references' },
103 | ]
104 | },
105 | ]
106 | }
107 |
108 | function makeSidebarLogger(): DefaultTheme.SidebarItem[] {
109 | return [
110 | {
111 | text: 'Logger',
112 | collapsed: false,
113 | items: [
114 | { text: 'Setup', link: 'setup' },
115 | { text: 'Macros', link: 'macros' },
116 | ]
117 | },
118 | ]
119 | }
120 |
121 | function makeSidebarConfig(): DefaultTheme.SidebarItem[] {
122 | return [
123 | {
124 | text: 'Config',
125 | collapsed: false,
126 | items: [
127 | { text: 'Configure How-to', link: 'config-workflow' },
128 | { text: 'Data Types', link: 'data-types' },
129 | { text: 'Setup Proxy', link: 'setup-proxy' },
130 | { text: 'Schema Parser', link: 'schema-whatnot' },
131 | { text: 'File Helper', link: 'file-helpers' },
132 | ]
133 | },
134 | {
135 | text: 'Examples',
136 | collapsed: false,
137 | items: [
138 | { text: 'Singleton Settings', link: 'examples' },
139 | { text: 'External References', link: 'references' },
140 | ]
141 | },
142 | ]
143 | }
144 |
145 | function makeSidebarHooks(): DefaultTheme.SidebarItem[] {
146 | return [
147 | {
148 | text: 'Hooks & Memory',
149 | collapsed: false,
150 | items: [
151 | { text: 'Hooking with DKUtil', link: 'hooking-with-dkutil' },
152 | { text: 'Memory Edit', link: 'memory-edit' },
153 | { text: 'Address Fetching', link: 'address-fetching' },
154 | { text: 'Hook Handles', link: 'hook-handles' },
155 | { text: 'Trampoline', link: 'trampoline' },
156 | ]
157 | },
158 | {
159 | text: 'API',
160 | collapsed: false,
161 | items: [
162 | { text: 'Relocation', link: 'relocation' },
163 | { text: 'ASM Patch', link: 'asm-patch' },
164 | { text: 'Cave Hook', link: 'cave-hook' },
165 | { text: 'VTable Swap', link: 'vtable-swap' },
166 | { text: 'IAT Swap', link: 'iat-swap' },
167 | { text: 'LTO-Enabled Hook', link: 'lto-enabled-hook' },
168 | ]
169 | },
170 | {
171 | text: 'Examples',
172 | collapsed: false,
173 | items: [
174 | { text: 'Useful Helpers', link: 'useful-helpers' },
175 | { text: 'Callsite Logging', link: 'callsite-logging' },
176 | { text: 'External References', link: 'references' },
177 | ]
178 | },
179 | ]
180 | }
181 |
182 | function makeSidebarUtils(): DefaultTheme.SidebarItem[] {
183 | return [
184 | {
185 | text: 'Utils',
186 | collapsed: false,
187 | items: [
188 | { text: 'Templates', link: 'templates' },
189 | { text: 'Enumeration', link: 'enumeration' },
190 | { text: 'String', link: 'string' },
191 | { text: 'Numbers', link: 'numbers' },
192 | ]
193 | },
194 | ]
195 | }
196 |
197 | function makeSidebarExtra(): DefaultTheme.SidebarItem[] {
198 | return [
199 | {
200 | text: 'Extra',
201 | collapsed: false,
202 | items: [
203 | { text: 'Serializable', link: 'skse' },
204 | ]
205 | },
206 | ]
207 | }
--------------------------------------------------------------------------------
/docs/config/config-workflow.md:
--------------------------------------------------------------------------------
1 | # DKUtil::Config
2 |
3 | An all purpose configuration module for x64 native plugin projects, supports `ini`, `json` and `toml` file types out of box. No more handling these settings on our own, use simplied API from this library!
4 |
5 | ## Workflow
6 |
7 | The configuration module in DKUtil works as "proxy per file", each `Proxy` represents one specific configuration file and manages the subsequent `Bind`, `Load`, `Write`, and `Generate` calls to this file and its bound `Data`.
8 |
9 | 1. Declare `Data` with key names.
10 | 2. Initialize a `Proxy` for a config file on system.
11 | 3. Binds the `Data` to `Proxy` with default values.
12 | 4. Use `Proxy` to load/regenerate/output the bound `Data`.
13 | 5. Use updated `Data` in code.
14 |
15 | ## Usage
16 |
17 | The entire module is behind the namespace `DKUtil::Config` or `dku::Config`. To use, include the header:
18 |
19 | ```cpp
20 | #include "DKUtil/Config.hpp"
21 | ```
22 |
23 | ## Config Lookup Path
24 |
25 | By default the config file lookup directory is the current working path.
26 | The relative path `CONFIG_ENTRY` can be defined before including.
27 |
28 | ```cpp
29 | #define CONFIG_ENTRY "configs\\"
30 | // this translates to "process_cwd/configs/"
31 | #include "DKUtil/Config.hpp"
32 | ```
33 |
--------------------------------------------------------------------------------
/docs/config/data-types.md:
--------------------------------------------------------------------------------
1 | # Data Types
2 |
3 | `DKUtil::Config` supports 3 primitive data types, string variants and their array type(collection).
4 |
5 | + Integer
6 | + Double
7 | + Boolean
8 | + String
9 |
10 | ## Declaring
11 |
12 | To declare configuration data, initialize them with key field and optional section field.
13 |
14 | ```cpp
15 | using namespace DKUtil::Alias; // type alias
16 |
17 | Integer myInt64Data{ "MyIntDataKey", "General" }; // std::int64_t
18 | Double myDoubleData{ "MyDoubleKey", "General" }; // double
19 | Boolean myBoolData{ "MyBoolKey" }; // bool
20 | String myStringData{ "MyStringKey" }; // std::basic_string
21 | ```
22 |
23 | ::: tip Section
24 | Section field is optional and unnamed sections will be put under `Global`. Section is also ignored for JSON.
25 | :::
26 |
27 | ## Accessing
28 |
29 | The data can be accessed by operator `*` as if it were a pointer, or the const getter method `get_data()`/`get_collection()`.
30 |
31 | ::: warning Collection on Singular Data
32 | When the data is singular but called with `get_collection()`, a size-1 collection will be returned with the singular data only.
33 | :::
34 |
35 | If the data is a collection, the reference of its members can be accessed by operator `[]` with index. There are two exceptions to this:
36 |
37 | + The **first** element will be returned if the data is not a collection.
38 | + The **last** element will be returned if the index is out of bound.
39 |
40 | `is_collection()` can be used, or `get_size()` which will return `0` if it's not a collection.
41 |
42 | ::: warning
43 | This behavior is planned to change with internal exception handling
44 | :::
45 |
--------------------------------------------------------------------------------
/docs/config/examples.md:
--------------------------------------------------------------------------------
1 | # Config Examples
2 |
3 | Given configuration file `MyConfigFile.toml`:
4 |
5 | ```toml
6 | [SectionA]
7 | MyIntData = 10086
8 | MyIntArray = [99, 96, 92, 87, 71]
9 | MyDouble = 114.514
10 | MyBool = true
11 | MyString = "Hello toml"
12 | MyStringArray = [
13 | "First",
14 | "Second",
15 | "LastButNotLeast"
16 | ]
17 | ```
18 |
19 | ::: code-group
20 |
21 | ```cpp [Load Data]
22 | #include "DKUtil/Config.hpp"
23 |
24 | using namespace DKUtil::Alias; // For type alias
25 |
26 | class Settings : dku::model::Singleton
27 | {
28 | public:
29 | Integer MyInt64Data{ "MyIntData" };
30 | Integer MyInt64ArrayData{ "MyIntArray" };
31 | Double MyDoubleData{ "MyDouble" };
32 | Boolean MyBoolData{ "MyBool" };
33 | String MyStringData{ "MyString" };
34 | String MyStringArrayData{ "MyStringArray" };
35 |
36 | void Load() noexcept
37 | {
38 | _config.Bind<0, 100>(MyInt64Data, 10);
39 | _config.Bind<-360, 360>(MyInt64ArrayData, 10, 78, 314, 996);
40 | _config.Bind(MyStringData, "Hello DKUtil");
41 | _config.Bind(MyStringArrayData, "First", "Second", "Third");
42 | _config.Bind(MyBoolData, true);
43 | _config.Bind<0.0, 10.0>(MyDoubleData, 3.14154);
44 |
45 | _config.Load();
46 | }
47 |
48 | private:
49 | // cannot use auto for class member declaration
50 | TomlConfig _config = COMPILE_PROXY("MyConfigFile.toml"sv);
51 | }
52 | ```
53 |
54 | ```cpp [Use Data]
55 | auto* setting = Settings::GetSingleton();
56 |
57 | // access
58 | if (setting->MyBoolData.get_data()) {
59 | // true
60 | }
61 |
62 | // access data
63 | INFO("{} {} {}", *setting->MyInt64Data, *setting->MyStringData, *setting->MyDoubleData);
64 |
65 | // traverse the collection
66 | for (auto& str : setting->MyStringArrayData.get_collection()) {
67 | INFO(str);
68 | }
69 |
70 | // modify
71 | auto& data = *setting->MyInt64Data;
72 | int result = SomeCalc(data);
73 | data *= result;
74 | ```
75 |
76 | :::
77 |
78 | ::: tip
79 | Wrapping in a static Settings class is recommended but not required.
80 | :::
81 |
82 | Also see [external references](references)
--------------------------------------------------------------------------------
/docs/config/file-helpers.md:
--------------------------------------------------------------------------------
1 | # File Helpers
2 |
3 | `DKUtil::Config` provides some helper functions for files:
4 |
5 | + To get full path of config file, relative or not:
6 | ```cpp
7 | std::string GetPath(const std::string_view a_file);
8 | ```
9 |
10 | + To list all files fulfilling the criterion:
11 | ```cpp
12 | template
13 | std::vector GetAllFiles(
14 | std::string_view a_dir = {},
15 | std::string_view a_ext = {},
16 | std::string_view a_prefix = {},
17 | std::string_view a_suffix = {});
18 | ```
--------------------------------------------------------------------------------
/docs/config/references.md:
--------------------------------------------------------------------------------
1 | # Snippets
2 |
3 | Some code snippets that utilizes DKUtil::Config.
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/config/schema-whatnot.md:
--------------------------------------------------------------------------------
1 | # Schema Parser
2 |
3 | `Config::Schema` is a special type of `Proxy` that allows you to parse strings into custom c++ data structure.
4 |
5 | ## Example
6 |
7 | Given a struct `CustomData`:
8 |
9 | ```cpp
10 | struct CustomData
11 | {
12 | std::uint64_t form;
13 | std::string name;
14 | std::string payload;
15 | bool excluded;
16 | };
17 | ```
18 |
19 | And expect schema string with delimiter `|`:
20 |
21 | ```
22 | "0x12345|Random|None|true"
23 | ```
24 |
25 | ## One Liner
26 |
27 | For the sake of ease of use, `DKUtil::Config` provides static function to parse strings as you go:
28 |
29 | ```cpp
30 | std::string s{ "0x12345|Random|None|true" };
31 |
32 | CustomData data = dku::Config::ParseSchemaString(s, "|");
33 | INFO("{} {} {} {}", data.form, data.name, data.payload, data.excluded);
34 | ```
35 |
36 | ## Use Proxy
37 |
38 | To parse the entire schema file that consists lines of schema strings:
39 |
40 | ```cpp
41 | auto schema = SCHEMA_PROXY("Schema_SC.txt");
42 | schema.Load();
43 | auto& parser = schema.get_parser();
44 |
45 | std::vector data;
46 | while (true) {
47 | auto entry = parser.ParseNextLine("|");
48 | if (!entry.has_value()) {
49 | break;
50 | }
51 |
52 | data.push_back(entry.value());
53 | }
54 | ```
55 |
56 | ::: tip
57 | You can also utilize [`dku::Config::GetAllFiles(...)`](file-helpers) to collect files at runtime.
58 | :::
59 |
60 | ## Aggregate Conversion
61 |
62 | All schema functions support user defined type and varadic template arguments:
63 |
64 | ```cpp
65 | std::string s{ "0x12345|1.5|true" };
66 |
67 | auto [a,b,c] = dku::Config::ParseSchemaString(s, "|");
68 | ```
69 |
70 | ## Special Values
71 |
72 | **spaces**: all whitespaces for data type except `std::string` will be trimmed before parsing.
73 |
74 | ```cpp
75 | " 1000 " == "1000 " == "1000"
76 | ```
77 |
78 | **bool**: `true`/`false` is case insensitive, `1`/`0` is also accepted.
79 |
80 | ```cpp
81 | " True " == "True" == "true" == "1"
82 | ```
83 |
84 | **hex**: to guarantee a hex number conversion, regardless of prefix `0x` in string segment, use `dku::numbers::hex` in place of `std::uint64_t`.
85 |
86 | ```cpp
87 | dku::numbers::hex formID; // special hex number holder
88 | formID = "0x14"; // 0x14
89 | formID = "100"; // 0x100
90 | formID = 100; // 0x64
91 | std::uint64_t num = formID; // implicit conversion to numbers
92 | fmt::format("{} {}", formID, num); // formats to "0x64 100"
93 | ```
94 |
--------------------------------------------------------------------------------
/docs/config/setup-proxy.md:
--------------------------------------------------------------------------------
1 | # Proxy
2 |
3 | `Proxy` supports `json`, `ini`, and `toml` files. Its type can be known at compile time and/or runtime determined for the given file.
4 |
5 | If the file name and type is already known at the compile time, use `COMPILE_PROXY` macro to get the corresponding parser for such file type.
6 |
7 | If the file name is runtime generated, such as file iterator from a directory, use `RUNTIME_PROXY` macro for file type/name that is unknown at the compile time.
8 |
9 | ```cpp
10 | #include "DKUtil/Config.hpp"
11 |
12 | JsonConfig MainConfig = COMPILE_PROXY("MyConfigFile.json"sv);
13 | // or to omit the type
14 | auto MainConfig = COMPILE_PROXY("MyConfigFile.json"sv);
15 |
16 | std::string runtimeFileName = SomeLoadFileCall();
17 | auto RuntimeConfig = RUNTIME_PROXY(runtimeFile);
18 | ```
19 |
20 | ## Bind
21 |
22 | `Proxy::Bind` is used for binding data to proxy **with** default value.
23 |
24 | ```cpp
25 | Integer myInt64Data{ "MyIntData" };
26 |
27 | auto Proxy = COMPILE_PROXY("MyConfigFile.toml"sv);
28 |
29 | Proxy.Bind(myInt64Data, 100);
30 | ```
31 |
32 | To bind data with default collection value, pass in multiple values:
33 |
34 | ```cpp
35 | Proxy.Bind(myInt64Data, 100, 200, 300);
36 | ```
37 |
38 | To bind data with a min/max numeric range, pass in template parameters. Range limits collection values as well.
39 |
40 | ```cpp
41 | Proxy.Bind<-100, 100>(myInt64Data, -20, 50);
42 | ```
43 |
44 | ::: tip Mismatch
45 | The actual data value/collection will be updated after `Proxy::Load`, a default singular value will become a collection of values if the actual config is array.
46 | :::
47 |
48 | ## Load
49 |
50 | After binding data to proxy, `Proxy::Load` can be used to load, parse, and update bound data values.
51 |
52 | ::: code-group
53 |
54 | ```cpp [File]
55 | Proxy.Load();
56 | ```
57 |
58 | ```cpp [String]
59 | std::ifstream reader{ filename };
60 | std::string file{};
61 | reader >> file;
62 |
63 | Proxy.Load(file.data());
64 | ```
65 |
66 | :::
67 |
68 | ::: warning Missing File
69 | If the file cannot be found, a default configuration file with default values will be written in place.
70 | :::
71 |
72 | ## Write
73 |
74 | `Proxy::Write` can be used to output to the file with current bound data values.
75 |
76 | ```cpp
77 | Proxy.Write()
78 | ```
79 |
80 | `Proxy::Write` also accepts an optional output filename parameter instead.
81 |
82 | ## Append
83 |
84 | To append new data entries to an existing config file(retroactive update):
85 |
86 | ```cpp
87 | Proxy.Load();
88 | // generate new entries with default value
89 | Proxy.Generate();
90 | // write new config file
91 | Proxy.Write();
92 | ```
93 |
--------------------------------------------------------------------------------
/docs/config/snippets/NoEnchantmentRestrictionRemake.md:
--------------------------------------------------------------------------------
1 | [NoEnchantmentRestrictionRemake](https://github.com/gottyduke/NoEnchantmentRestrictionRemake)
2 |
3 | ::: code-group
4 |
5 | ```cpp [Config.h]
6 | #pragma once
7 | #include "DKUtil/Config.hpp"
8 |
9 | namespace Config
10 | {
11 | using namespace DKUtil::Alias;
12 |
13 | extern Boolean EnableDE;
14 | extern Boolean EnableUE;
15 | extern Integer UEMax;
16 | extern String Exclusion;
17 |
18 | void Load();
19 | }
20 | ```
21 |
22 | ```cpp [Config.cpp]
23 | #include "Config.h"
24 |
25 | namespace Config
26 | {
27 | Boolean EnableDE{ "EnableDisenchantEverything" };
28 | Boolean EnableUE{ "EnableUnlimitedEnchantment" };
29 | Integer UEMax{ "EnchantmentMaxSlots" };
30 | String Exclusion{ "ExcludedEnchantments" };
31 |
32 | void Load()
33 | {
34 | auto mainConfig = COMPILE_PROXY("NoEnchantmentRestrictionRemake.toml"sv);
35 |
36 | mainConfig.Bind(EnableDE, true);
37 | mainConfig.Bind(EnableUE, true);
38 | mainConfig.Bind(UEMax, 3);
39 | mainConfig.Bind(Exclusion, "0|Example.esp");
40 |
41 | mainConfig.Load();
42 |
43 | INFO("Config Loaded"sv);
44 | }
45 | }
46 | ```
47 |
48 | ```cpp [Usage]
49 | DKUtil::Hook::WriteImm(_Hook_UES->TramPtr, static_cast(*Config::UEMax)); // [!code focus]
50 |
51 | void Validator::ResolveExclusion()
52 | {
53 | auto* dataHandler = RE::TESDataHandler::GetSingleton();
54 |
55 | for (auto& ex : Config::Exclusion.get_collection()) { // [!code focus]
56 | auto list = dku::string::split(ex, "|"sv); // [!code focus]
57 | if (list.size() < 2) {
58 | continue;
59 | }
60 | }
61 | }
62 | ```
63 |
64 | :::
65 |
--------------------------------------------------------------------------------
/docs/config/snippets/SF_NativeAutoHDR.md:
--------------------------------------------------------------------------------
1 | [SF_NativeAutoHDR](https://github.com/ersh1/SF_NativeAutoHDR)
2 |
3 | ::: code-group
4 |
5 | ```cpp [Settings.h]
6 | #pragma once
7 | #include "DKUtil/Config.hpp"
8 |
9 | namespace Settings
10 | {
11 | using namespace DKUtil::Alias;
12 |
13 | class Main : public DKUtil::model::Singleton
14 | {
15 | public:
16 | Integer ImageSpaceBufferFormat{ "ImageSpaceBufferFormat", "Main" };
17 | Boolean UpgradeUIRenderTarget{ "UpgradeUIRenderTarget", "Main" };
18 | Integer UpgradeRenderTargets{ "UpgradeRenderTargets", "Main" };
19 |
20 | Integer FrameBufferFormat{ "FrameBufferFormat", "HDR" };
21 |
22 | String RenderTargetsToUpgrade{ "RenderTargetsToUpgrade", "RenderTargets" };
23 |
24 | void Load() noexcept;
25 |
26 | private:
27 | TomlConfig config = COMPILE_PROXY("NativeAutoHDR.toml"sv);
28 | };
29 | }
30 | ```
31 |
32 | ```cpp [Settings.cpp]
33 | #include "Settings.h"
34 |
35 | namespace Settings
36 | {
37 | void Main::Load() noexcept
38 | {
39 | static std::once_flag ConfigInit;
40 | std::call_once(ConfigInit, [&]() {
41 | config.Bind(ImageSpaceBufferFormat, 0);
42 | config.Bind(UpgradeUIRenderTarget, true);
43 | config.Bind(UpgradeRenderTargets, 2);
44 | config.Bind(FrameBufferFormat, 0);
45 | config.Bind(RenderTargetsToUpgrade, "SF_ColorBuffer", "HDRImagespaceBuffer", "ImageSpaceHalfResBuffer", "ImageProcessColorTarget", "ImageSpaceBufferB10G11R11", "ImageSpaceBufferE5B9G9R9", "TAA_idTech7HistoryColorTarget", "EnvBRDF", "ImageSpaceBufferR10G10B10A2");
46 | });
47 |
48 | config.Load();
49 |
50 | INFO("Config loaded"sv)
51 | }
52 | }
53 | ```
54 |
55 | ```cpp [Usage]
56 | const auto settings = Settings::Main::GetSingleton();
57 |
58 | if (*settings->FrameBufferFormat == 2) { // [!code focus]
59 | SetBufferFormat(RE::Buffers::FrameBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT);
60 | } else {
61 | SetBufferFormat(RE::Buffers::FrameBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM);
62 | }
63 |
64 | switch (*settings->ImageSpaceBufferFormat) { // [!code focus]
65 | case 1:
66 | SetBufferFormat(RE::Buffers::ImageSpaceBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM);
67 | break;
68 | case 2:
69 | SetBufferFormat(RE::Buffers::ImageSpaceBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT);
70 | break;
71 | }
72 |
73 | if (*settings->UpgradeUIRenderTarget) { // [!code focus]
74 | SetBufferFormat(RE::Buffers::ScaleformCompositeBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT);
75 | }
76 | ```
77 |
78 | :::
79 |
--------------------------------------------------------------------------------
/docs/config/snippets/YouCanSleepRemake.md:
--------------------------------------------------------------------------------
1 | [YouCanSleepRemake](https://github.com/gottyduke/YouCanSleepRemake)
2 |
3 | ::: code-group
4 |
5 | ```cpp [Config.h]
6 | #pragma once
7 | #include "DKUtil/Config.hpp"
8 |
9 | namespace Config
10 | {
11 | using namespace DKUtil::Alias;
12 |
13 | extern Boolean EnableSleepWait[8];
14 |
15 | void Load();
16 | }
17 | ```
18 |
19 | ```cpp [Config.cpp]
20 | #include "Config.h"
21 |
22 | namespace Config
23 | {
24 | Boolean EnableSleepWait[8] = {
25 | { "InAir" },
26 | { "Trespassing" },
27 | { "AskedToLeave" },
28 | { "GuardsPursuing" },
29 | { "EnemiesNearby" },
30 | { "TakingHealthDamage" },
31 | { "Owned" },
32 | { "InUse" },
33 | };
34 |
35 | void Load()
36 | {
37 | auto main = COMPILE_PROXY("YouCanSleepRemake.toml"sv);
38 |
39 | for (auto index = 0; index < std::extent_v; ++index) {
40 | main.Bind(EnableSleepWait[index], true);
41 | }
42 |
43 | main.Load();
44 |
45 | INFO("Config Loaded"sv);
46 | }
47 | }
48 | ```
49 |
50 | ```cpp [Usage]
51 | std::ptrdiff_t OffsetTbl[8] = {
52 | 0x2E, // You cannot sleep in the air.
53 | 0x89, // You cannot sleep while trespassing.
54 | 0xB1, // You cannot sleep while being asked to leave.
55 | 0xF6, // You cannot sleep while guards are pursuing you.
56 | 0x11F, // You cannot sleep when enemies are nearby.
57 | 0x146, // You cannot sleep while taking health damage.
58 | 0x1BB, // This object is already in use by someone else.
59 | REL::Relocate(0x3BC, 0x3C0) // You cannot sleep in an owned bed.
60 | };
61 |
62 | const auto funcAddr = DKUtil::Hook::IDToAbs(AE_FuncID, SE_FuncID);
63 |
64 | for (auto index = 0; index < std::extent_v; ++index) {
65 | if (*Config::EnableSleepWait[index]) { // [!code focus]
66 | DKUtil::Hook::WriteImm(funcAddr + OffsetTbl[index], JmpShort); // [!code focus]
67 | INFO("Enabled SleepWait {} at 0x{:X}", Config::EnableSleepWait[index].get_key(), OffsetTbl[index]); // [!code focus]
68 | }
69 | }
70 |
71 | // loop check for in air position
72 | if (*Config::EnableSleepWait[0]) { // [!code focus]
73 | DKUtil::Hook::WriteData(funcAddr + InAirLoopOffset, &InAirHookNop, sizeof(InAirHookNop));
74 | }
75 |
76 | INFO("Hooks installed"sv);
77 | ```
78 |
79 | :::
80 |
--------------------------------------------------------------------------------
/docs/dkutil/about.md:
--------------------------------------------------------------------------------
1 | # DKUtil
2 |
3 | Started as a helper header for personal projects, improved on demand with more and more features.
4 |
5 | **platform**: Windows
6 | **standard**: c++20
7 |
8 | ## Usage Requirement
9 |
10 | + [CMake](https://cmake.org)
11 | + [vcpkg](https://github.com/microsoft/vcpkg/releases)
12 | + `/std:c++20` or `/std:latest`
13 | + Config
14 | + [SimpleIni](https://github.com/brofield/simpleini)
15 | + [nlohmann-json](https://github.com/nlohmann/json)
16 | + Hook
17 | + [xbyak](https://github.com/herumi/xbyak)
18 | + Logger
19 | + [spdlog](https://github.com/gabime/spdlog)
20 | + Extra(For SKSE)
21 | + [CommonLibSSE](https://github.com/Ryan-rsm-McKenzie/CommonLibSSE)
22 |
23 | ::: warning Dependencies
24 | All dependencies will be handled by vcpkg & CMake.
25 | :::
26 |
27 | ## Installation
28 |
29 | ::: code-group
30 | ```ps [local copy]
31 | git clone https://github.com/gottyduke/DKUtil.git
32 | ```
33 |
34 | ```ps [git submodule]
35 | git submodule add https://github.com/gottyduke/DKUtil.git extern/DKUtil
36 | git submodule update --init --recursive -f
37 | git submodule update --remote -f
38 | ```
39 | :::
40 |
41 | ::: tip Environment
42 | Adding path to DKUtil to environment variable `DKUtilPath` can help with consuming the library.
43 | :::
44 |
45 | ::: info PluginTemplate
46 | If this is your first time using DKUtil and starting a fresh plugin project, consider using [PluginTemplate](https://github.com/gottyduke/plugintemplate) and skip DKUtil setup process.
47 | :::
48 |
49 | ## Consume in Projects
50 |
51 | ### CMake
52 |
53 | Add to your target project's `CMakeLists.txt`:
54 |
55 | ::: code-group
56 | ```CMake [local tree]
57 | add_subdirectory("Path/To/DKUtil" DKUtil)
58 | ```
59 |
60 | ```CMake{33} [global dependency]
61 | # dependency macros
62 | macro(find_dependency_path DEPENDENCY FILE)
63 | # searches extern for dependencies and if not checks the environment variable
64 | if(NOT ${DEPENDENCY} STREQUAL "")
65 | # Check extern
66 | message(
67 | STATUS
68 | "Searching for ${DEPENDENCY} using file ${FILE}"
69 | )
70 | find_path("${DEPENDENCY}Path"
71 | ${FILE}
72 | PATHS "extern/${DEPENDENCY}"
73 | )
74 |
75 | if("${${DEPENDENCY}Path}" STREQUAL "${DEPENDENCY}Path-NOTFOUND")
76 | # Check path
77 | message(
78 | STATUS
79 | "Getting environment for ${DEPENDENCY}Path: $ENV{${DEPENDENCY}Path}"
80 | )
81 | set("${DEPENDENCY}Path" "$ENV{${DEPENDENCY}Path}")
82 | endif()
83 |
84 | message(
85 | STATUS
86 | "Found ${DEPENDENCY} in ${${DEPENDENCY}Path}; adding"
87 | )
88 | add_subdirectory("${${DEPENDENCY}Path}" ${DEPENDENCY})
89 | endif()
90 | endmacro()
91 |
92 | # find DKUtil with environment DKUtilPath or "extern/DKUtil"
93 | find_dependency_path(DKUtil include/DKUtil/Logger.hpp)
94 | ```
95 | :::
96 |
97 | ### VCPKG Port
98 |
99 | Planned, low priority due to no demand
100 |
--------------------------------------------------------------------------------
/docs/dkutil/compilation.md:
--------------------------------------------------------------------------------
1 | # Compilation
2 |
3 | DKUtil is a header only library (so far), when incorporated into CMake projects, header only libraries (a.k.a INTERFACE target) won't be visible in generated msvc solution.
4 |
5 | There are sneaky build config to enable DKUtilDebugger target in CMake, however, currently it requires SKSE workspace set up. If you are not a user of [SKSEPlugins](https://github.com/gottyduke/SKSEPlugins), it will be difficult to build this way.
6 |
7 | ```ps
8 | cmake -B build -S . --preset=REL -DPLUGIN_MODE:BOOL=TRUE -DDKUTIL_DEBUG_BUILD:BOOL=TRUE
9 | ```
--------------------------------------------------------------------------------
/docs/dkutil/references.md:
--------------------------------------------------------------------------------
1 | # External References
2 |
3 | Open source game plugin projects that utilize DKUtil functions! DKUtil improves with these amazing developers' feedback and contributions.
4 |
5 | ## Baldur's Gate 3:
6 |
7 | + [BG3WASD](https://github.com/Ch4nKyy/BG3WASD)
8 | + [BG3_AchievementEnabler](https://github.com/gottyduke/BG3_AchievementEnabler)
9 | + [BG3_NativeCameraTweaks](https://github.com/ersh1/BG3_NativeCameraTweaks)
10 |
11 | ## Skyrim Special Edition:
12 |
13 | + [Armor Rating Rescaled SKSE](https://github.com/gottyduke/ArmorRatingRescaledRemake)
14 | + [BehaviorDataInjector](https://github.com/max-su-2019/BehaviorDataInjector)
15 | + [CombatPathingRevolution](https://github.com/max-su-2019/CombatPathingRevolution)
16 | + [Individual Shout Cooldown](https://github.com/gottyduke/IndividualShoutCooldownRemake)
17 | + [MaxsuDetectionMeter](https://github.com/max-su-2019/MaxsuDetectionMeter)
18 | + [No Enchantment Restriction SKSE](https://github.com/gottyduke/NoEnchantmentRestrictionRemake)
19 | + [Plugin Tutorial CN](https://github.com/gottyduke/PluginTutorialCN)
20 | + [SCAR](https://github.com/max-su-2019/SCAR)
21 | + [SKSEPlugins](https://github.com/gottyduke/SKSEPlugins)
22 | + [Speed Casting SKSE](https://github.com/gottyduke/SpeedCastingRemake)
23 | + [StaggerEffectFix](https://github.com/max-su-2019/StaggerEffectFix)
24 | + [You Can Sleep SKSE](https://github.com/gottyduke/YouCanSleepRemake)
25 | + ...
26 |
27 | ## Starfield:
28 |
29 | + [SF_NativeAutoHDR](https://github.com/ersh1/SF_NativeAutoHDR)
30 | + [Starfield-Luma](https://github.com/EndlesslyFlowering/Starfield-Luma)
31 | + [StrippingArmor](https://github.com/cotyounoyume/StrippingArmor)
32 | + [ClassicSprintingStarfieldv2](https://github.com/MCPC10/ClassicSprintingStarfieldv2)
33 | + [BakaAchievementEnabler](https://github.com/shad0wshayd3/BakaAchievementEnabler)
34 | + [SF_PluginTemplate](https://github.com/gottyduke/SF_PluginTemplate)
35 | + [BakaKillMyGames](https://github.com/shad0wshayd3/BakaKillMyGames)
36 | + [BakaQuickFullSaves](https://github.com/shad0wshayd3/BakaQuickFullSaves)
37 | + [BakaQuitGameFix](https://github.com/shad0wshayd3/BakaQuitGameFix)
38 | + [Disable-Dialogue-Camera](https://github.com/AntoniX35/Disable-Dialogue-Camera)
39 | + [UltimateBooster](https://github.com/cotyounoyume/UltimateBooster)
40 | + [SFSE-Save-Unfinished-Ships](https://github.com/kvnmtz/SFSE-Save-Unfinished-Ships)
41 | + [Sprint-Stuttering-Fix](https://github.com/AntoniX35/Sprint-Stuttering-Fix)
42 | + [Weapon-Swap-Stuttering-Fix](https://github.com/AntoniX35/Weapon-Swap-Stuttering-Fix)
43 | + [NonPlayableUnlockerSFSE](https://github.com/cotyounoyume/NonPlayableUnlockerSFSE)
44 | + ...
45 |
46 | ## Others:
47 |
48 | + [MHWI_CNTextColor](https://github.com/gottyduke/MHW-I_CNTextColor)
49 |
50 | ... and many more from the thriving community.
--------------------------------------------------------------------------------
/docs/extra/skse.md:
--------------------------------------------------------------------------------
1 | # DKUtil::Extra
2 |
3 | On expanding small additions that requires `CommonLibSSE` to function.
4 | + `CONSOLE` logging macro but for in-game console.
5 | + `serializable` painless, all-in-one serialization solution for SKSE plugins.
6 |
7 | ## CONSOLE
8 | logging macro for in game console
9 | ```C++
10 | CONSOLE("{} {} {}", a, b, c);
11 | CONSOLE("This will be logged to in game console", a, b, c);
12 | ```
13 |
14 | ## serializable
15 | All in one, painless serialization solution. All serializables are automatically registered to internal handler, and unregistered upon destruction(although probably not needed). All reasonable data types are supported, they are flattened first with internal resolvers, then serialized with SKSE.
16 | Common types:
17 | + STL containers
18 | + user defined aggregate types
19 | + all trivial types
20 | + string
21 |
22 | ### declaration
23 | Wrap the data type in `dku::serializable<>`, with the second template parameter giving it a unique hash identifier. This identifier is used in the future for variant updating/backwards compatibility feature.
24 | ```C++
25 | struct MyDataType
26 | {
27 | int a;
28 | bool b;
29 | char c;
30 | std::string s;
31 | std::vector arr;
32 | };
33 | using MyComplexDataType = std::map;
34 |
35 | // empty
36 | dku::serializable mySimpleData;
37 | // or initializer list
38 | dku::serializable mySimpleData(
39 | {1, true, 'a', "Elo", {1,2,3,4,5}});
40 | // or assignment
41 | dku::serializable mySimpleData = MyDataType{
42 | {1, true, 'a', "Elo", {1,2,3,4,5}}};
43 |
44 | dku::serializable myDataMap;
45 | // ...
46 | ```
47 |
48 | ### using, modifying
49 | The data can be accessed by prepending the asterisk symbol `*` or using arrow operator `->` as if it were a pointer type, or the getter method `get()`.
50 | ```C++
51 | for (auto& [id, data] : *myDataMap) {
52 | // ...
53 | }
54 |
55 | myDataMap->emplace(...);
56 | ```
57 |
58 | ### registering
59 | During plugin load, simply call it once and you are done:
60 | ```C++
61 | DLLEXPORT bool SKSEAPI SKSEPlugin_Load(const SKSE::LoadInterface* a_skse)
62 | {
63 | DKUtil::Logger::Init(Plugin::NAME, REL::Module::get().version().string());
64 |
65 | INFO("{} v{} loaded", Plugin::NAME, Plugin::Version);
66 |
67 | // this
68 | dku::serialization::api::RegisterSerializable();
69 |
70 | return true;
71 | }
72 | ```
73 | You can also pass in optional plugin identifier, although the internal hash function already makes a unique id.
74 |
75 | ### custom resolver
76 | You can attach custom resolver callbacks to the `dku::serializable<>` types and they'll be called during `Save`, `Load`, `Revert` state, **after** they are resolved.
77 | ```C++
78 | dku::serializable myDataMap;
79 | using type = decltype(myDataMap)::type;
80 |
81 | // resolver function takes its underlying data
82 | void CustomCallback(type& a_data, dku::serialization::ResolveOrder a_order)
83 | {
84 | if (a_order == dku::serialization::ResolveOrder::kSave) {
85 | INFO("Saved");
86 | } else if (a_order == dku::serialization::ResolveOrder::kLoad) {
87 | INFO("Loaded");
88 | for (auto& [f, d] : a_data) {
89 | INFO("{} {}", f, d.s);
90 | }
91 | } else {
92 | INFO("Reverted");
93 | }
94 | }
95 |
96 | myDataMap.add_resolver(CustomCallback);
97 | ```
98 |
99 | ### flattened layout map
100 | Tweak the preprocessor definitions a bit, define `DKU_X_MOCK` will redirect all read/write calls to its internal buffer for testing, and generate a flattened layout map of the data type it's operating on.
101 |
102 | ---
103 |
--------------------------------------------------------------------------------
/docs/hooks/address-fetching.md:
--------------------------------------------------------------------------------
1 | # Address Fetching
2 |
3 | `DKUtil::Hook` provides some address fetching helpers at runtime.
4 |
5 | ## Pattern Scan
6 |
7 | To do a pattern scan using KMP, given string pattern:
8 |
9 | ```cpp
10 | std::string pattern{ "40 57 48 83 EC 30 48 8B 0D ?? ?? ?? ??" };
11 | ```
12 |
13 | ### Syntax
14 |
15 | ```cpp
16 | void* search_pattern(
17 | std::string_view pattern,
18 | std::uintptr_t base = 0,
19 | std::size_t size = 0
20 | );
21 | ```
22 |
23 | ### Parameter
24 |
25 | + `pattern` : string pattern to search, spacing is optional, each byte must be two characters.
26 | + `base` : address of memory block to search, defaults to module.textx.
27 | + `size` : size of memory block to search, defaults to module.textx.size.
28 |
29 | Returns `nullptr` if not found. Otherwise return the first match.
30 |
31 | ### Linear Search
32 |
33 | To use pattern at compile time, use specialized template version:
34 |
35 | ```cpp
36 | void* search_pattern<"40 57 48 83 EC 30 48 8B 0D ?? ?? ?? ??">(base = 0, size = 0);
37 | ```
38 |
39 | This template version performs a linear search instead of default KMP.
40 |
41 | ## Rip Addressing
42 |
43 | To get the actual address of a rip-relative displacement used in an instruction.
44 |
45 | Given target assembly:
46 |
47 | ```asm
48 | 0x141234567: call [rip + 0x30]
49 | 0x14123456D: lea rax, ds: [rip + 0x1110]
50 | 0x141234574: mov rax, ds: [rip + 0x114514]
51 | ```
52 |
53 | We want the final address of these rip displacements:
54 |
55 | ```cpp
56 | std::uintptr_t funcAddr = dku::Hook::GetDisp(0x141234567);
57 | auto actorSingleton = dku::Hook::GetDisp(0x14123456D);
58 | bool significance = *dku::Hook::GetDisp(0x141234574);
59 | ```
60 |
61 | ## Adjust Pointer
62 |
63 | Offset a pointer with type cast.
64 |
65 | ```cpp
66 | // read bool member value at 0x220 from a class pointer
67 | auto& member = *dku::Hook::adjust_pointer(actor, 0x220);
68 | ```
69 |
70 | ## Module IAT
71 |
72 | Get import address of method in a library loaded by module.
73 |
74 | ```cpp
75 | void* GetImportAddress(
76 | std::string_view moduleName,
77 | std::string_view libraryName,
78 | std::string_view importName)
79 | ```
80 |
81 | ## Class VTable
82 |
83 | Get the address of n-th function in class virtual function table.
84 |
85 | ```cpp
86 | size_t n = 0x8; // get 8th function
87 | Actor* actor = new Actor(); // class pointer, also vptr
88 | auto func = dku::Hook::TblToAbs(actor, n);
89 | ```
90 |
--------------------------------------------------------------------------------
/docs/hooks/asm-patch.md:
--------------------------------------------------------------------------------
1 | # Assembly Patch
2 |
3 | Replace a block of target memory with patch.
4 |
5 | ## Syntax
6 |
7 | ```cpp
8 | ASMPatchHandle AddASMPatch(
9 | std::uintptr_t address,
10 | offset_pair offsets,
11 | Patch* patch,
12 | bool forward = true
13 | );
14 | ```
15 |
16 | ## Paramter
17 |
18 | + `address` : target instruction address.
19 | + `offsets` : pair containing the {begin, end} offsets from target instruction to patch.
20 | + `patch` : pointer to the memory patch data structure(see also: [patch structure](memory-edit#patch-structure)).
21 | + `forward` : **optional**, whether to skip the rest of `NOP` fillers.
22 |
23 | ## HookHandle
24 |
25 | A `ASMPatchHandle` object will be returned:
26 |
27 | ```cpp
28 | class ASMPatchHandle
29 | {
30 | const offset_pair Offset;
31 | const std::size_t PatchSize;
32 | std::vector OldBytes{};
33 | std::vector PatchBuf{};
34 | };
35 | ```
36 |
37 | ## Example
38 |
39 | Prepare the hook:
40 |
41 | ```cpp
42 | using namespace DKUtil::Alias;
43 |
44 | std::uintptr_t mem_addr = 0x7FF712345678;
45 | // or offset from module base
46 | std::uintptr_t mem_addr = dku::Hook::Module::get().base() + 0x345678;
47 | ```
48 |
49 | Mark the begin and the end of target code region to patch:
50 |
51 | ```cpp
52 | // starts at mem_addr + 0x120
53 | // ends at mem_addr + 0x130
54 | // entire memory region size to patch is 0x10
55 | auto offset = std::make_pair(0x120, 0x130);
56 | ```
57 |
58 | ::: tip Offset Pair
59 | Sometimes your patch begins at your memory address, which is a pair of `{0x0, size}`.
60 | :::
61 |
62 | Commit the hook:
63 |
64 | ::: code-group
65 |
66 | ```cpp [Raw Patch]
67 | OpCode AsmSrc[]{
68 | 0xB8, // mov eax,
69 | 0x00, 0x00, 0x00, 0x00, // Imm32
70 | 0x89, 0XC1, // mov ecx, eax
71 | };
72 |
73 | auto Hook = DKUtil::Hook::AddASMPatch(funcAddr, offset, { &AsmPatch, sizeof(AsmSrc) });
74 | Hook->Enable();
75 | ```
76 |
77 | ```cpp [DKUtil Patch]
78 | Patch AsmSrc{
79 | "\xB8" // mov eax
80 | "\x00\x00\x00\x00" // Imm32
81 | "\x89\xC1", // mov ecx eax
82 | 7
83 | };
84 |
85 | auto Hook = DKUtil::Hook::AddASMPatch(funcAddr, offset, &AsmSrc);
86 | Hook->Enable();
87 | ```
88 |
89 | ```cpp [Xbyak]
90 | struct ChangeEcxPatch :
91 | public Xbyak::CodeGenerator
92 | {
93 | ChangeEcxPatch()
94 | {
95 | mov(eax, static_cast(0x0));
96 | mov(ecx, eax);
97 | }
98 | };
99 |
100 | ChangeEcxPatch patch{};
101 | patch.ready();
102 |
103 | auto Hook = DKUtil::Hook::AddASMPatch(funcAddr, offset, &patch);
104 | Hook->Enable();
105 | ```
106 |
107 | :::
108 |
109 | ## Auto Trampoline
110 |
111 | If the given target memory region size defined by `offsets` is less than the size of assembly patch, a trampoline will be utilized to fulfill the patch and setup the auto detour/return. This action requires a minimal target memory space of `0x5`.
112 |
--------------------------------------------------------------------------------
/docs/hooks/callsite-logging.md:
--------------------------------------------------------------------------------
1 | # Callsite Logging
2 |
3 | Sometimes during plugin development, you may want to track down callers to specific function during runtime, and especially when it's a virtual function/member function, that makes it even harder to find.
4 |
5 | Here's a common example of intercepting a virtual function at runtime and log its callers(more specifically, its return address):
6 |
7 | ## Target Function
8 |
9 | ```cpp
10 | void TESObjectREFR::SetStartingPosition(const NiPoint3& a_pos); // 0x54 in vtbl
11 | // this function is a virtual member function of class TESObjectREFR,
12 | // first we need to translate it to __cdecl function:
13 | void SetStartingPosition(TESObjectREFR* a_this, const NiPoint3& a_pos);
14 | // per x64 calling convention, *this pointer is implicitly passed as first argument
15 | ```
16 |
17 | ## Target Assembly
18 |
19 | ```asm
20 | 40:56 | push rsi
21 | 57 | push rdi
22 | 41:56 | push r14
23 | ```
24 |
25 | We only need 5 bytes minimum to setup a cave hook, so these three lines are sufficient.
26 |
27 | ## Example
28 |
29 | ```cpp
30 | using namespace DKUtil::Alias;
31 |
32 | // assume we acquired an instance of the class TESObjectREFR
33 | TESObjectREFR* refr_instance = 0x123456;
34 |
35 | class SetStartingPositionEx :
36 | public Xbyak::CodeGenerator
37 | {
38 | // because vptr is at 0x0 of class instance, so a pointer to class is a pointer to vptr
39 | inline static std::uintptr_t* vtbl{ *std::bit_cast(refr_instance) };
40 | // the index of the target virtual function we want to log in vtbl
41 | inline static std::size_t index{ 0x54 };
42 |
43 | // actual logging function, we have added third argument, it's the return address/callsite
44 | static void Intercept_SSP(
45 | TESObjectREFR* a_this,
46 | const RE::NiPoint3& a_pos,
47 | std::uintptr_t a_caller = 0) // [!code ++]
48 | {
49 | INFO("ret 0x{:X}", dku::Hook::GetRawAddress(a_caller));
50 | }
51 |
52 | public:
53 | SetStartingPositionEx()
54 | {
55 | // move the return address on stack[rsp] to third argument, as per x64 calling convention
56 | mov(r8, qword[rsp]);
57 | }
58 |
59 | static void Install()
60 | {
61 | SetStartingPositionEx sse{};
62 | sse.ready();
63 |
64 | // generates prolog & epilog patches that preserve register values,
65 | // so our logging function won't break anything
66 | auto [ppatch, epatch] = dku::Hook::JIT::MakeNonVolatilePatch(
67 | {
68 | dku::Hook::Register::ALL
69 | });
70 |
71 | // actual prolog patch
72 | Patch prolog;
73 | // first we move the third argument
74 | prolog.Append(sse);
75 | // then apply the preserve patch
76 | prolog.Append(ppatch);
77 | // nothing extra to add for epilog patch, because we didn't break stack balance
78 |
79 | // target function is at index 0x54 in vtbl
80 | auto addr = vtbl[index];
81 | auto hook = dku::Hook::AddCaveHook(
82 | addr,
83 | { 0, 5 },
84 | FUNC_INFO(Intercept_SSP),
85 | &prolog,
86 | &epatch,
87 | // important that we restore original function prolog(the 5 bytes we took) after returning from logger
88 | HookFlag::kRestoreAfterEpilog);
89 | hook->Enable();
90 | }
91 | };
92 | ```
93 |
94 | ## Custom Prolog/Epilog
95 |
96 | When composing arguments for custom cave functions, do follow [x64 calling convention](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170).
97 |
--------------------------------------------------------------------------------
/docs/hooks/cave-hook.md:
--------------------------------------------------------------------------------
1 | # CaveHook
2 |
3 | Detour to hook function from target memory.
4 |
5 | ## Syntax
6 |
7 | ```cpp
8 | CaveHookHandle AddCaveHook(
9 | std::uintptr_t address,
10 | offset_pair offset,
11 | FuncInfo funcInfo,
12 | Patch* prolog = nullptr,
13 | Patch* epilog = nullptr,
14 | HookFlag flag = HookFlag::kSkipNOP
15 | );
16 | ```
17 |
18 | ## Parameter
19 |
20 | + `address` : target instruction address.
21 | + `offsets` : pair containing the {begin, end} offsets from target instruction to patch.
22 | + `funcInfo` : FUNC_INFO macro wrapper of hook function.
23 | + `prolog` : **optional**, memory patch **before** detouring to hook function, `nullptr` if none.
24 | + `epilog` : **optional**, memory patch **after** returning from hook function, `nullptr` if none.
25 | + `flag` : **optional**, special flag for cave hook operation.
26 |
27 | ## HookFlag
28 |
29 | ```cpp
30 | enum class HookFlag
31 | {
32 | kNoFlag = 0, // default
33 |
34 | kSkipNOP = 1u << 0, // skip NOPs
35 | kRestoreBeforeProlog = 1u << 1, // apply stolens before prolog
36 | kRestoreAfterProlog = 1u << 2, // apply stolens after prolog
37 | kRestoreBeforeEpilog = 1u << 3, // apply stolens before epilog
38 | kRestoreAfterEpilog = 1u << 4, // apply stolens after epilog
39 | };
40 | ```
41 |
42 | You can set multiple flags by passing `{HookFlag, HookFlag, ...}`.
43 |
44 | ## HookHandle
45 |
46 | A `CaveHookHandle` object will be returned:
47 |
48 | ```cpp
49 | class CaveHookHandle
50 | {
51 | const offset_pair Offset;
52 | const std::size_t CaveSize;
53 | const std::uintptr_t CaveEntry;
54 | std::uintptr_t CavePtr{ 0x0 };
55 | std::vector OldBytes{};
56 | std::vector CaveBuf{};
57 | };
58 | ```
59 |
60 | ## Workflow
61 |
62 | CaveHook will first fill the target block of memory defined by `address` + `{offset_pair}` with `NOP`, then write its detour into trampoline, which then applies user prolog patch(if any), detour to user function, applies user epilog patch(if any), then return to original block. Original bytes of target block can will be applied depending on the `HookFlag` parameter(if set).
63 |
64 | ## Example
65 |
66 | ```cpp
67 | using namespace DKUtil::Alias;
68 |
69 | // hook function
70 | float Hook_MyAwesomeFunc(int a_awesomeInt) {
71 | // do awesome stuff
72 | return static_cast(a_awesomeInt);
73 | }
74 |
75 | std::uintptr_t funcAddr = 0x7FF712345678;
76 | // or offset from module base
77 | std::uintptr_t funcAddr = dku::Hook::Module::get().base() + 0x345678;
78 |
79 | // mark the begin and the end of target code to patch
80 | // starts at funcAddr + 0x120
81 | // ends at funcAddr + 0x130
82 | auto offset = std::make_pair(0x120, 0x130);
83 |
84 | // this is DKUtil::Hook::Patch, you can also use xbyak or raw patch
85 | // move return value to xmm3
86 | DKUtil::Hook::Patch Epilog = {
87 | "\x0F\x10\xD8", // movups xmm3, xmm0
88 | 0x3 // size of patch
89 | };
90 |
91 | auto Hook_MAF = dku::Hook::AddCaveHook(
92 | funcAddr,
93 | offset,
94 | FUNC_INFO(Hook_MyAwesomeFunc),
95 | nullptr,
96 | &Epilog,
97 | dku::HookFlag::kRestoreAfterEpilog);
98 |
99 | Hook_MAF->Enable();
100 | ```
101 |
102 | ::: details Workflow for This Example
103 | Under the hood it'll `NOP` all bytes from `funcAddr + 0x120` to `funcAddr + 0x130`, set a branch call to `Hook_MyAwesomeFunc`, apply custom epilog patch, apply original bytes taken from 0x120 to 0x130(`kRestoreAfterEpilog`), then finally return to `funcAddr + 0x130`.
104 | :::
105 |
106 | ## Custom Prolog/Epilog
107 |
108 | When composing arguments for custom cave functions, do follow [x64 calling convention](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170).
109 |
--------------------------------------------------------------------------------
/docs/hooks/hook-handles.md:
--------------------------------------------------------------------------------
1 | # Hook Handles
2 |
3 | `HookHandle` class is the base object of a successful `DKUtil::Hook` operation, and it's used to access the hook specific data and enable/disable the hook.
4 |
5 | ```cpp
6 | class HookHandle
7 | {
8 | const std::uintptr_t Address;
9 | const std::uintptr_t TramEntry;
10 | std::uintptr_t TramPtr{ 0x0 };
11 | };
12 | ```
13 |
14 | ## Control
15 |
16 | `HookHandle` can be used to enable/disable a hook.
17 |
18 | ```cpp
19 | HookHandle handle = SomeDKUtilHookAPI();
20 |
21 | // hook control
22 | handle->Enable();
23 | handle->Disable();
24 | ```
25 |
26 | ## Derived Cast
27 |
28 | To cast into derived types of specific hook API:
29 |
30 | ```cpp
31 | HookHandle handle = SomeDKUtilHookAPI();
32 | ASMPatchHandle asmHandle = handle->As();
33 | ```
34 |
35 | This is useful when you only declare a base `HookHandle` object before commiting any hook operation, then downcast it.
36 |
37 | For related information, see each API's own section.
38 |
39 | ## Internal Trampoline
40 |
41 | You can write data directly to `HookHandle` internal `TramPtr`, this normally points to next available memory address in trampoline. However, unless you know what you are doing, you shouldn't be doing it.
42 |
43 | ```cpp
44 | HookHandle handle = SomeDKUtilHookAPI();
45 | handle->Write(0x100); // write Imm32 0x100 in trampoline
46 | ```
47 |
--------------------------------------------------------------------------------
/docs/hooks/hooking-with-dkutil.md:
--------------------------------------------------------------------------------
1 | # DKUtil::Hook
2 |
3 | A hook module that DKUtil project originally started with.
4 |
5 | ## Usage
6 |
7 | The entire module is behind the namespace `DKUtil::Hook` or `dku::Hook`. To use, include the header:
8 |
9 | ```cpp
10 | #include "DKUtil/Hook.hpp"
11 | ```
12 |
--------------------------------------------------------------------------------
/docs/hooks/iat-swap.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gottyduke/DKUtil/31d0bdf36d0c3979c346e6f20735f2cbb763d419/docs/hooks/iat-swap.md
--------------------------------------------------------------------------------
/docs/hooks/lto-enabled-hook.md:
--------------------------------------------------------------------------------
1 | # LTO.what()
2 |
3 | When the target program has interprocedural optimizations enabled for their builds, it often translates to link time optimizations, and for MSVC mainstream, it's `/LTCG` and `/GL`, a.k.a. link time code generation and whole program optimizations. This technique feeds the compiler with extra information that allows it make interprocedural optimizations.
4 |
5 | How does it affect our hooking methods and common approaches?
6 |
7 | For MSVC x64 windows targets, we hook by complying with [x64 calling convention](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention). This is under the assumptions that caller handles the shadow stack and volatile register are safe to use with callee.
8 |
9 | The x64 MSVC ABI considers `rax`, `rcx`, `rdx`, `r8`, `r9`, `r10`, `r11` and `xmm0` to `xmm5` as volatile.
10 |
11 | ## LTO.why()
12 |
13 | Consider the following call example:
14 |
15 | ```asm
16 | mov rdx, [rcx] // a2
17 | mov rcx, r8 // use r8 value here as a1
18 | call my_func(rcx, rdx) // call boundary
19 | mov r8, 0x100 // using r8 as a free register now
20 | ```
21 |
22 | After returning from `my_func`, compiler will consider all volatile registers value changed, thus compiler will not reuse `rcx` or `rdx` registers.
23 |
24 | **However**, with LTO enabled targets, it may look like this:
25 |
26 | ```asm
27 | mov rdx, [rcx] // a2
28 | mov rcx, r8 // use r8 value here as a1
29 | call my_func(rcx, rdx) // call boundary
30 | test r8, r8 // keep using r8
31 | ```
32 |
33 | With the PGO information from linker, compiler knows that `my_func` did not change `r8`, or it can be optimized to not change `r8`, so compiler lets the caller use `r8` across the call boundary. This effectively reduces register preserving/stack usage, thus optimizations.
34 |
35 | When implementing the hook functions for our plugins, normally there's no way of knowing if all the hooks will change specific registers, and that information cannot be accounted for when dealing with LTO targets.
36 |
37 | ## Syntax
38 |
39 | Currently, `dku::Hook` offers `write_call` variant, `write_call_ex`. This API is designated for preserving regular/sse registers across a hook call boundary and keeping the original LTO code executing.
40 |
41 | ```cpp
42 | auto write_call_ex(
43 | const dku_memory auto src,
44 | F dst,
45 | enumeration regs = { Register::NONE },
46 | enumeration simd = { SIMD::NONE }
47 | );
48 | ```
49 |
50 | ## Parameter
51 |
52 | + `src` : address of the target callsite
53 | + `dst` : hook function
54 | + `regs` : regular registers to preserve as non volatile
55 | + `simd` : sse registers to preserve as non volatile
56 |
57 | You can set multiple registers by passing `{Register, Register, ...}`.
58 |
59 | ## Example
60 |
61 | ```cpp{17,20,23,26}
62 | using namespace DKUtil::Alias;
63 |
64 | // hook function
65 | bool Hook_123456(void* a_gameInstance)
66 | {
67 | return func(a_gameInstance);
68 | }
69 |
70 | // original function
71 | static inline std::add_pointer_t func;
72 |
73 | // callsite
74 | auto addr = 0x7FF712345678;
75 |
76 | // preserve rdx, r9
77 | func = dku::Hook::write_call_ex<5>(addr, Hook_123456,
78 | { Reg::RDX, Reg::R9 });
79 | // preserve xmm0, xmm2
80 | func = dku::Hook::write_call_ex<5>(addr, Hook_123456,
81 | { Reg::NONE }, { Xmm::XMM0, Xmm::XMM2 });
82 | // preserve rdx, r9, and xmm0, xmm2
83 | func = dku::Hook::write_call_ex<5>(addr, Hook_123456,
84 | { Reg::RDX, Reg::R9 }, { Xmm::XMM0, Xmm::XMM2 });
85 | // preserve all
86 | func = dku::Hook::write_call_ex<5>(addr, Hook_123456,
87 | { Reg::ALL }, { Xmm::ALL });
88 | ```
89 |
90 | ## Non-Volatile Patch
91 |
92 | Sometimes you may not need to commit a `write_call_ex` hook, but could use some boilerplate assemblies for preserving registers and restoring them.
93 |
94 | ```cpp
95 | auto [prolog, epilog] = dku::Hook::JIT::MakeNonVolatilePatch({Reg::ALL});
96 | Patch userPatch;
97 | // wrap user patch in non-volatile patch generated by DKUtil
98 | userPatch = prolog.Append(userPatch).Append(epilog);
99 | ```
100 |
--------------------------------------------------------------------------------
/docs/hooks/memory-edit.md:
--------------------------------------------------------------------------------
1 | # Memory Editing
2 |
3 | `DKUtil::Hook` offers some memory writing methods like other library does.
4 |
5 | ::: code-group
6 |
7 | ```cpp [Arbitrary Data]
8 | void WriteData(std::uintptr_t dst, const void* data, const std::size_t size);
9 | ```
10 |
11 | ```cpp [Immediate Value]
12 | // any trivial, integral or standard layout(pod) types
13 | void WriteImm(std::uintptr_t dst, const dku_h_pod_t auto& data);
14 | ```
15 |
16 | ```cpp [DKUtil Patch]
17 | void WritePatch(std::uintptr_t dst, const Patch* patch);
18 | void WritePatch(std::uintptr_t dst, const Xbyak::CodeGenerator* xbyak);
19 | ```
20 |
21 | :::
22 |
23 | ## Patch Structure
24 |
25 | Anything that has data pointer and data size are accepted. Xbyak is also supported.
26 |
27 | `DKUtil::Patch` is a wrapper for data pointer and data size, and provides extended functionalities.
28 |
29 | ```cpp
30 | struct Patch
31 | {
32 | const void* Data;
33 | const size_t Size;
34 | bool Managed; // ignore unless you know what you are doing
35 | }
36 | ```
37 |
38 | The `Managed` field indicates whether or not this Patch owns the `Data` resource. This field should not be changed, it's auto set on internal operations.
39 |
40 | ## Common Practices
41 |
42 | ::: code-group
43 |
44 | ```cpp [Hex String]
45 | using namespace DKUtil::Alias;
46 |
47 | static constexpr Patch RadiusPatch{
48 | // lahf
49 | "\x9F\x50"
50 | // mov rdx, rdi
51 | "\x48\x89\xFA"
52 | // movss xmm2, xmm6
53 | "\xF3\x0F\x10\xD6"
54 | // mov r9, rbp
55 | "\x49\x89\xE9"
56 | // mov [rsp-0x8], rbx
57 | "\x48\x89\x5C\x24\xF8",
58 | 17
59 | };
60 | ```
61 |
62 | ```cpp [Read Memory]
63 | using namespace DKUtil::Alias;
64 |
65 | Patch RadiusPatch{
66 | AsPointer(mem_address), // macro provided by DKUtil
67 | 10 // read 10 bytes from mem_address
68 | };
69 | ```
70 |
71 | ```cpp [Xbyak]
72 | struct MyPatch :
73 | public Xbyak::CodeGenerator
74 | {
75 | MyPatch()
76 | {
77 | lahf();
78 | mov(rdx, rdi);
79 | movss(xmm2, xmm6);
80 | mov(r9, rbp);
81 | mov(ptr[rsp-0x8], rbx);
82 | }
83 | };
84 | ```
85 |
86 | ```cpp [Raw Patch]
87 | std::array RawPatch{ 0x48, 0x8B, 0x4C, 0x24, 0x20 };
88 | ```
89 |
90 | :::
91 |
92 | ## Combining Patches
93 |
94 | ```cpp
95 | Patch prolog;
96 | Patch epilog;
97 | Patch extra;
98 | prolog.Append(epilog).Append(extra);
99 | ```
100 |
--------------------------------------------------------------------------------
/docs/hooks/references.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gottyduke/DKUtil/31d0bdf36d0c3979c346e6f20735f2cbb763d419/docs/hooks/references.md
--------------------------------------------------------------------------------
/docs/hooks/relocation.md:
--------------------------------------------------------------------------------
1 | # Relocation
2 |
3 | Replace `call/jmp` instruction with a hook.
4 |
5 | ## Syntax
6 |
7 | ```cpp
8 | RelHookHandle Hook::AddRelHook(address, function);
9 | ```
10 |
11 | ## Parameter
12 |
13 | + `N` : length of target instruction, e.g. `call Game.exe+0x1999DF` is `E8 0C 77 FA FF`, which is 5 bytes in length. `FF 15 12 34 56 78` is 6 bytes.
14 | + `bool` : whether to return or not, `call` returns, `jmp` branches thus no return.
15 | + `address` : target instruction address.
16 | + `function` : hook function.
17 |
18 | ## HookHandle
19 |
20 | A `RelHookHandle` object will be returned:
21 |
22 | ```cpp
23 | class RelHookHandle
24 | {
25 | const std::size_t OpSeqSize;
26 | const Imm64 OriginalFunc;
27 | Imm64 Destination;
28 | std::vector OldBytes{};
29 | std::vector Detour{};
30 | };
31 | ```
32 |
33 | `RelHookHandle` can be implicitly convereted to address of original function.
34 |
35 | ## Wrapper
36 |
37 | For the ease of use, you can use `Hook::write_call` and `Hook::write_branch` for convenience. These wrappers will enable itself and return the original function address. However, you lose control of disabling the hook.
38 |
39 | ## Example
40 |
41 | Given target assembly:
42 |
43 | ```asm
44 | 0x140345675: mov rcx, rax
45 | 0x140345678: call Game.exe+0x123456
46 | ```
47 |
48 | We want to replace the `call Game.exe+0x123456` to `call HookFunc`:
49 |
50 | ::: code-group
51 |
52 | ```cpp [Class Style]
53 | class Hook
54 | {
55 | // hook function
56 | static bool Hook_123456(void* a_instance)
57 | {
58 | // do something
59 | return func(a_instance);
60 | }
61 |
62 | // original function
63 | static inline std::add_pointer_t func;
64 |
65 | public:
66 | static void Install()
67 | {
68 | // absolute
69 | auto addr = 0x7FF712345678;
70 | // or offset from module base
71 | auto addr = dku::Hook::Module::get().base() + 0x345678;
72 |
73 | // save original function
74 | func = dku::Hook::write_call<5>(addr, Hook_123456);
75 | }
76 | };
77 | ```
78 |
79 | ```cpp [Free Functions]
80 | // forward
81 | bool Hook_123456(void* a_instance);
82 | // original function
83 | std::add_pointer_t func;
84 |
85 | bool Hook_123456(void* a_instance)
86 | {
87 | // do something
88 | return func(a_instance);
89 | }
90 |
91 | auto addr = dku::Hook::Module::get().base() + 0x345678;
92 | // save original function
93 | func = dku::Hook::write_call<5>(addr, Hook_123456);
94 | ```
95 |
96 | :::
97 |
--------------------------------------------------------------------------------
/docs/hooks/trampoline.md:
--------------------------------------------------------------------------------
1 | # Trampoline
2 |
3 | A trampoline is often used internally, it leverages many limitations in memory related operations, common scenarios are:
4 |
5 | + Detouring a function call, because of rip addressing mode is within -/+2GiB range, DKUtil will utilize a trampoline to extend the call indirection.
6 | + Writing assembly patches, trampoline serves as a block of memory to put assembly patches in.
7 | + Allocating custom data.
8 |
9 | ## Workflow
10 |
11 | `Target Code Region` <-**within -/+2GiB**-> `Trampoline` <-> `Our Code Region`
12 |
13 | ## Allocating Trampoline
14 |
15 | Before committing **any** action that requires a trampoline (e.g. detour hooks, cave hooks, asm patches with exceeding sizes), `DKUtil::Hook` requires a manual `DKUtil::Hook::Trampoline::AllocTrampoline(size)` call to initiate a page allocation for trampoline to use. This should be called **only once**, with sufficient size.
16 |
17 | ```cpp
18 | BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
19 | {
20 | DKUtil::Logger::Init(Plugin::NAME, REL::Module::get().version().string());
21 |
22 | DKUtil::Hook::Trampoline::AllocTrampoline(1 << 10); // [!code focus]
23 |
24 | Configs::Load();
25 | Hooks::Install();
26 | }
27 | ```
28 |
29 | ::: warning Planned Change
30 | This API has been planned for breaking changes that will auto allocate trampoline and extend its size accordingly.
31 | :::
32 |
33 | ## Determine Proper Size
34 |
35 | Allocating large enough memory for trampoline is good to begin with, the actual trampoline usage are always logged for debug builds, from there an appropriate sufficient size can be determined for release builds.
36 |
37 | ## Allocate Memory
38 |
39 | To allocate memory **from** trampoline:
40 |
41 | ```cpp
42 | auto& trampoline = dku::Hook::Trampoline::GetTrampoline();
43 |
44 | struct CustomData;
45 | CustomData* data = trampoline.allocate();
46 | void* sized_data = trampoline.allocate(0x100);
47 | ```
48 |
49 | ## SKSE / SFSE / F4SE
50 |
51 | For SKSE, SFSE, and F4SE plugin projects, `DKUtil::Hook` will use commonlib's trampoline interface instead of its internal trampoline. However, `CommonLib::AllocTrampoline` still needs to be called.
52 |
--------------------------------------------------------------------------------
/docs/hooks/useful-helpers.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gottyduke/DKUtil/31d0bdf36d0c3979c346e6f20735f2cbb763d419/docs/hooks/useful-helpers.md
--------------------------------------------------------------------------------
/docs/hooks/vtable-swap.md:
--------------------------------------------------------------------------------
1 | # Virtual Method Table Swap
2 |
3 | Swaps a virtual method table function with hook function.
4 |
5 | ## Syntax
6 |
7 | ```cpp
8 | VMTHookHandle AddVMTHook(
9 | void* vtbl,
10 | std::uint16_t index,
11 | FuncInfo funcInfo,
12 | Patch* patch = nullptr
13 | );
14 | ```
15 |
16 | ## Parameter
17 |
18 | + `vtbl` : pointer to virtual method table (a.k.a pointer to class object).
19 | + `index` : **index** of the virtual function in the virtual method table.
20 | + `funcInfo` : FUNC_INFO macro wrapper of hook function.
21 | + `patch` : **optional**, prolog patch before detouring to hook function.
22 |
23 | ## HookHandle
24 |
25 | A `VMTHookHandle` object will be returned:
26 |
27 | ```cpp
28 | class VMTHookHandle
29 | {
30 | template
31 | F GetOldFunction();
32 |
33 | std::uintptr_t OldAddress;
34 | };
35 | ```
36 |
37 | ## Example
38 |
39 | Given base class `Dummy` with two functions:
40 |
41 | ```cpp
42 | class Dummy
43 | {
44 | public:
45 | void MsgA() { INFO("Called MsgA"sv); } // 0
46 | void MsgB() { INFO("Called MsgB"sv); } // 1
47 | };
48 |
49 | // target dummy vtbl, it's implicitly at
50 | Dummy* dummy = new Dummy();
51 | ```
52 |
53 | First translate class methods to __cdecl convention:
54 |
55 | ```cpp
56 | using namespace DKUtil::Alias;
57 |
58 | // first parameter is this pointer
59 | using MsgFunc_t = std::add_pointer_t;
60 | MsgFunc_t oldMsgA;
61 | MsgFunc_t oldMsgB;
62 | ```
63 |
64 | To swap the functions with our own:
65 |
66 | ```cpp
67 | // hook function
68 | void MsgC(Dummy* a_this) {
69 | INFO("Called MsgC"sv);
70 | // call original function
71 | oldMsgA(a_this);
72 | oldMsgB(a_this);
73 | }
74 |
75 | auto Hook_MsgA = dku::Hook::AddVMTHook(dummy, 0, FUNC_INFO(MsgC));
76 | auto Hook_MsgB = dku::Hook::AddVMTHook(dummy, 1, FUNC_INFO(MsgC));
77 |
78 | // save original function
79 | oldMsgA = Hook_MsgA->GetOldFunction();
80 | oldMsgB = Hook_MsgB->GetOldFunction();
81 |
82 | Hook_MsgA->Enable();
83 | Hook_MsgB->Enable();
84 | // now MsgA and MsgB will both be detoured to MsgC instead
85 | ```
86 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 | title: DKUtil
4 |
5 | hero:
6 | name: DKUtil
7 | text: Header Library for x64 Native Plugin Development
8 | actions:
9 | - theme: brand
10 | text: Get Started
11 | link: /dkutil/about
12 | - theme: alt
13 | text: View on GitHub
14 | link: https://github.com/gottyduke/dkutil
15 | - theme: alt
16 | text: PluginTemplate
17 | link: https://github.com/gottyduke/plugintemplate
18 |
19 | features:
20 | - icon: 📝
21 | title: Make Plugins Easier
22 | details: Logging, hooking, config files, all features needed for a plugin skeleton is here. Plus more useful API and neat functions.
23 | - icon: 🚀
24 | title: Used by Many
25 | details: Bundled with PluginTemplate, a lot of plugin projects start up with DKUtil.
26 | - icon: ⚙
27 | title: Actively Maintained
28 | details: Actively maintained, DKUtil improves with feedback and contributions.
29 | ---
30 |
31 |
--------------------------------------------------------------------------------
/docs/logger/macros.md:
--------------------------------------------------------------------------------
1 | # Macros
2 |
3 | | Macro | Level |
4 | | ----- | --------------------------------------------- |
5 | | TRACE | `trace`, enabled on debug builds |
6 | | DEBUG | `debug`, enabled on debug builds |
7 | | INFO | `info`, enabled on all builds |
8 | | WARN | `warn`, enabled on all builds |
9 | | ERROR | `err`, message box pop-up, returnable |
10 | | FATAL | `critical`, message box pop-up, halts process |
11 |
12 | ## Logging Level
13 |
14 | To change the current logging level, use `Logger::SetLevel`:
15 |
16 | ```cpp
17 | DKUtil::Logger::SetLevel(spdlog::level::level_enums);
18 | ```
19 |
20 | Convenient switches for disabling/enabling all DEBUG macros:
21 |
22 | ```cpp
23 | ENABLE_DEBUG
24 | // debug logs from here will be printed
25 | DISABLE_DEBUG
26 | // debug logs from here will be omitted
27 | ```
28 |
29 | ## Formatting
30 |
31 | `DKUtil::Logger` uses `spdlog` as backend, thus powered by `fmt`, for detailed syntax, refer to [fmtlib syntax](https://fmt.dev/latest/syntax.html).
32 |
33 | ```cpp
34 | INFO("{} - {} = {}", 10, 5, 10 - 5);
35 | INFO("addr: {:X}", &pointer);
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/logger/setup.md:
--------------------------------------------------------------------------------
1 | # DKUtil::Logger
2 |
3 | Some macro style loggers with `spdlog` backend.
4 |
5 | ## Setup PROJECT_NAME
6 |
7 | For backwards compatibility reasons, DKUtil requires `PROJECT_NAME` definition prior to including any DKUtil files.
8 |
9 | ```cpp
10 | #define PROJECT_NAME "YourPluginName"
11 | ```
12 |
13 | ::: tip
14 | This is already configured if using [PluginTemplate](https://github.com/gottyduke/plugintemplate).
15 | :::
16 |
17 | ## Init Logger
18 |
19 | By default the log file outputs to the current working path.
20 | The relative path `LOG_PATH` can be defined before including.
21 |
22 | ```cpp{4}
23 | #define LOG_PATH "logs\\" // optional
24 | #include "DKUtil/Logger.hpp"
25 |
26 | DKUtil::Logger::Init(PROJECT_NAME, VersionString);
27 | ```
28 |
29 | ## Internal Debugging
30 |
31 | DKUtil extensively logs its internal operations when building with `Debug` configuration. To disable this behavior, define `DKU_L_DISABLE_INTERNAL_DEBUGGING` explicitly prior to including any DKUtil files.
32 |
33 | ```cpp
34 | #define DKU_L_DISABLE_INTERNAL_DEBUGGING
35 | #include "DKUtil/Logger.hpp"
36 | ```
37 |
38 | ::: tip Release Builds
39 | `DKU_L_DISABLE_INTERNAL_DEBUGGING` is enabled on release builds.
40 | :::
41 |
--------------------------------------------------------------------------------
/docs/utils/enumeration.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gottyduke/DKUtil/31d0bdf36d0c3979c346e6f20735f2cbb763d419/docs/utils/enumeration.md
--------------------------------------------------------------------------------
/docs/utils/numbers.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gottyduke/DKUtil/31d0bdf36d0c3979c346e6f20735f2cbb763d419/docs/utils/numbers.md
--------------------------------------------------------------------------------
/docs/utils/string.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gottyduke/DKUtil/31d0bdf36d0c3979c346e6f20735f2cbb763d419/docs/utils/string.md
--------------------------------------------------------------------------------
/docs/utils/templates.md:
--------------------------------------------------------------------------------
1 | DKUtil::Utility
2 |
3 | Some helper functions used within other DKUtil headers.
4 | + function
5 | + `consteval` helper functions retrieving the argument count of a function.
6 | + model
7 | + `Singleton` data model abstract class to save boiler plater code.
8 | + `enumeration` addition to the original `RE::stl::enumeration`.
9 | + static reflection for enum name, type name and value name, support value_type(`n`) and flag_type(`1< static reflection is not implemented using external lib, DKUtil wraps a lightweight compile time **nasty** macro inside.
29 |
30 | ```C++
31 | enum class Color : std::uint32_t
32 | {
33 | red,
34 | yellow,
35 | white,
36 | };
37 |
38 | // print
39 | Color redColor = Color::red;
40 | INFO("this enum is {}", dku::print_enum(redColor));
41 |
42 | // cast
43 | std::string colorStr = "yellow";
44 | auto& colorTbl = dku::static_enum();
45 | Color yellowColor = colorTbl.from_string(colorStr);
46 |
47 | // iterate
48 | for (const Color c : colorTbl.value_range(Color::red, Color::white)) {
49 | INFO("color is {}", colorTbl.to_string(c));
50 | }
51 |
52 | // iterate flag type enum ( 1 << 1, 1 << 2 etc..)
53 | enum class ColorFlag : std::uint32_t
54 | {
55 | red = 1 << 1,
56 | yellow = 1 << 2,
57 | white = 1 << 3,
58 | };
59 | auto& flagTbl = dku::static_enum();
60 |
61 | for (const ColorFlag c : flagTbl.flag_range(ColorFlag::red, ColorFlag::white)) {
62 | INFO("color is {}", flagTbl.to_string(c));
63 | }
64 | ```
65 |
66 | ---
67 | ## struct_cast, tuple_cast, concepts
68 | compile time conversion for same aligned struct/tuple
69 | ```C++
70 | struct AggregateType
71 | {
72 | int i;
73 | std::string s;
74 | char c;
75 | bool b;
76 | };
77 |
78 | auto tv = dku::model::tuple_cast(AggregateType{});
79 | static_assert(std::is_same_v>);
80 | auto sv = dku::model::struct_cast(tv);
81 | static_assert(std::is_same_v);
82 |
83 | // bindables
84 | static_assert(dku::model::number_of_bindables() == 4);
85 | static_assert(dku::model::number_of_bindables() == 4);
86 | static_assert(dku::model::number_of_bindables() == 4);
87 |
88 | // concepts
89 | int av[] = { 1, 2, 3, 4 };
90 | static_assert(dku::model::concepts::dku_aggregate);
91 | static_assert(dku::model::concepts::dku_bindable);
92 | static_assert(dku::model::concepts::dku_ranges);
93 | static_assert(dku::model::concepts::dku_trivial_ranges);
94 | ```
95 |
96 | ---
97 | ## `dku::string`
98 | + iequal
99 | + icontains
100 | + istarts_with
101 | + iends_with
102 | + lexical_cast
103 | + remove_non_alphanumeric
104 | + remove_non_numeric
105 | + replace_nth_occurrence
106 | + split (support multiple delimiters)
107 | + join
108 | + trim
109 | + static_string
110 |
111 | ---
112 |
--------------------------------------------------------------------------------
/format-all.ps1:
--------------------------------------------------------------------------------
1 | #Requires -Version 5
2 |
3 | # args
4 | param (
5 | [string]$CF = $env:clang_format_instance
6 | )
7 |
8 | $CF
9 |
10 | if (!(Test-Path $CF -PathType Leaf)) {
11 | "Failed to locate clang-format.exe"
12 | Exit
13 | }
14 |
15 | $headers = Get-ChildItem "$PSScriptRoot\include" -Recurse -File -ErrorAction SilentlyContinue | ? { $_.DirectoryName -inotmatch "external" }
16 | $src = Get-ChildItem "$PSScriptRoot\test" -Recurse -File -ErrorAction SilentlyContinue | ? { $_.DirectoryName -inotmatch "configs" }
17 |
18 | foreach ($file in $headers) {
19 | $file.BaseName
20 | & $CF -i -style=file $file
21 | }
22 |
23 | foreach ($file in $src) {
24 | $file.BaseName
25 | & $CF -i -style=file $file
26 | }
27 |
28 | "Formatted $($headers.Length) header files"
29 | "Formatted $($src.Length) source files"
--------------------------------------------------------------------------------
/generate-sln.ps1:
--------------------------------------------------------------------------------
1 | Remove-Item $PSScriptRoot/build -Recurse -Force -ErrorAction:SilentlyContinue -Confirm:$False | Out-Null
2 | & cmake -B $PSScriptRoot/build -S $PSScriptRoot --preset=REL -DPLUGIN_MODE:BOOL=TRUE
--------------------------------------------------------------------------------
/include/DKUtil/Config.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | /**
4 | * 1.2.0
5 | * Added Schema parser;
6 | *
7 | * 1.1.6
8 | * SFSE integration;
9 | *
10 | * 1.1.5
11 | * Removed DataManager layer between raw data and proxy;
12 | *
13 | * 1.1.4
14 | * Adaptaion of file structural changes;
15 | *
16 | * 1.1.3
17 | * out-of-bound adjusts;
18 | *
19 | * 1.1.2
20 | * constexpr specifiers;
21 | *
22 | * 1.1.1
23 | * Added alias for config proxies;
24 | * Minor logging practices;
25 | *
26 | * 1.1.0
27 | * NG-update;
28 | * Added clamp range;
29 | * Removed Proxy isLoaded check;
30 | *
31 | * 1.0.2
32 | * F4SE integration;
33 | *
34 | * 1.0.1
35 | * Fixed toml empty array crash;
36 | * Removed explicit identifier on copy constructor;
37 | *
38 | *
39 | * 1.0.0
40 | * Proxy, Bind and Load APIs for ini, json and toml type config files;
41 | *
42 | */
43 |
44 | #define DKU_C_VERSION_MAJOR 1
45 | #define DKU_C_VERSION_MINOR 2
46 | #define DKU_C_VERSION_REVISION 0
47 |
48 | #pragma warning(push)
49 | #pragma warning(disable: 4244)
50 |
51 | #include "Impl/pch.hpp"
52 |
53 | #if !defined(CONFIG_ENTRY)
54 |
55 | # if defined(F4SEAPI)
56 | # define CONFIG_ENTRY "Data\\F4SE\\Plugins\\"
57 | # elif defined(SKSEAPI)
58 | # define CONFIG_ENTRY "Data\\SKSE\\Plugins\\"
59 | # elif defined(SFSEAPI)
60 | # define CONFIG_ENTRY "Data\\SFSE\\Plugins\\"
61 | # else
62 | # define CONFIG_ENTRY ""
63 | # endif
64 |
65 | #endif
66 |
67 | namespace DKUtil
68 | {
69 | constexpr auto DKU_C_VERSION = DKU_C_VERSION_MAJOR * 10000 + DKU_C_VERSION_MINOR * 100 + DKU_C_VERSION_REVISION;
70 | } // namespace DKUtil
71 |
72 | #include "Impl/Config/shared.hpp"
73 |
74 | #include "Impl/Config/data.hpp"
75 |
76 | #include "Impl/Config/ini.hpp"
77 | #include "Impl/Config/json.hpp"
78 | #include "Impl/Config/schema.hpp"
79 | #include "Impl/Config/toml.hpp"
80 |
81 | #include "Impl/Config/proxy.hpp"
82 |
83 | namespace DKUtil::Config
84 | {
85 | /** \brief Parse a string into user defined struct
86 | * \brief e.g. CustomData d = ParseSchemaString(line, delim...)
87 | * \brief This API is an alias of global schema parser for ease of use
88 | * \param a_str : non-empty formatted schema string
89 | * \param a_delimiters : one or multiple string delimiters used to make segments
90 | * \return user defined struct
91 | */
92 | template
93 | requires(model::concepts::dku_aggregate)
94 | inline static SchemaData ParseSchemaString(std::string a_str, const std::convertible_to auto&... a_delimiters) noexcept
95 | {
96 | return detail::Schema::ParseString(a_str, std::forward(a_delimiters)...);
97 | }
98 |
99 | /** \brief Parse a string to user defined series of segments
100 | * \brief e.g. auto [a, b, c, d] = ParseSchemaString(line, delim...)
101 | * \brief This API is an alias of global schema parser for ease of use
102 | * \param a_str : non-empty formatted schema string
103 | * \param a_delimiters : one or multiple string delimiters used to make segments
104 | * \return std::tuple of user defined segments
105 | */
106 | template
107 | requires(sizeof...(SchemaSegment) > 1)
108 | inline static std::tuple ParseSchemaString(std::string a_str, const std::convertible_to auto&... a_delimiters) noexcept
109 | {
110 | return detail::Schema::ParseString>(a_str, std::forward(a_delimiters)...);
111 | }
112 | } // namespace DKUtil::Config
113 |
114 | namespace DKUtil::Alias
115 | {
116 | using Boolean = DKUtil::Config::detail::AData;
117 | using Integer = DKUtil::Config::detail::AData;
118 | using Double = DKUtil::Config::detail::AData;
119 | using String = DKUtil::Config::detail::AData>;
120 |
121 | using IniConfig = DKUtil::Config::Proxy;
122 | using JsonConfig = DKUtil::Config::Proxy;
123 | using TomlConfig = DKUtil::Config::Proxy;
124 | using SchemaConfig = DKUtil::Config::Proxy;
125 | using DynamicConfig = DKUtil::Config::Proxy;
126 | } // namespace DKUtil::Alias
127 |
128 | #pragma warning(pop)
129 |
--------------------------------------------------------------------------------
/include/DKUtil/Extra.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #define DKU_E_VERSION_MAJOR 1
4 | #define DKU_E_VERSION_MINOR 0
5 | #define DKU_E_VERSION_REVISION 0
6 |
7 | #if defined(SKSEAPI)
8 |
9 | # include
10 | # include
11 | # include
12 |
13 | # include "Impl/pch.hpp"
14 |
15 | # include "Impl/Extra/xconsole.hpp"
16 | # include "Impl/Extra/xserialize.hpp"
17 |
18 | # define DKU_X_VERSION_MAJOR 1
19 | # define DKU_X_VERSION_MINOR 0
20 | # define DKU_X_VERSION_REVISION 0
21 |
22 | namespace DKUtil
23 | {
24 | constexpr auto DKU_X_VERSION = DKU_X_VERSION_MAJOR * 10000 + DKU_X_VERSION_MINOR * 100 + DKU_X_VERSION_REVISION;
25 | } // namespace DKUtil
26 |
27 | namespace DKUtil
28 | {
29 | template
30 | using serializable = DKUtil::serialization::Serializable;
31 |
32 | using ResolveType = DKUtil::serialization::ResolveOrder;
33 | } // DKUtil
34 |
35 | #endif
--------------------------------------------------------------------------------
/include/DKUtil/Hook.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | /**
4 | * 2.6.6
5 | * Added KMP version of search_pattern.
6 | *
7 | * 2.6.5
8 | * Added AssumeNonVolatile helper for preserving registers across call boundaries;
9 | *
10 | * 2.6.4
11 | * Separated internal implementations details into individual headers;
12 | *
13 | * 2.6.3
14 | * SFSEAPI integration;
15 | * Fixed wrong Hook indirect call layout
16 | *
17 | * 2.6.2
18 | * Added GetDisp helper function;
19 | *
20 | * 2.6.1
21 | * Assert trampoline in range;
22 | *
23 | * 2.6.0
24 | * Added pattern scan modules;
25 | * Completed trampoline implementation;
26 | *
27 | * 2.5.3
28 | * Fixed stack alignment, force 0x20 allocation;
29 | *
30 | * 2.5.2
31 | * Adaptation of file structural changes;
32 | *
33 | * 2.5.1
34 | * Fixed the issue where CavePtr hasn't been forwarded but calculation is done;
35 | * Fixed the issue kSkipNOP not actually working;
36 | *
37 | * 2.5.0
38 | * Added new assembly structs;
39 | * Removed redundant structs;
40 | * Removed CAVE_MAXIMUM_BYTES;
41 | * Added register preserve functions;
42 | * Changed CaveHookFlag to HookFlag;
43 | *
44 | * 2.4.1
45 | * Added runtime counterparts for NG;
46 | *
47 | * 2.4.0
48 | * Minor formatting and refractoring for NG;
49 | *
50 | * 2.3.3
51 | * F4SE integration;
52 | * CaveHook auto patch the stolen opcodes;
53 | *
54 | * 2.3.2
55 | * Minor formatting changes;
56 | *
57 | * 2.3.1
58 | * CaveHookHandle changed base class;
59 | *
60 | * 2.3.0
61 | * Added assembly patch;
62 | * Improved cave hook;
63 | *
64 | * 2.2.0
65 | * Added import address table hook;
66 | *
67 | * 2.1.0
68 | * Added virtual method table hook;
69 | *
70 | * 2.0.0
71 | * Code partial redone; wtf are those macros;
72 | * Added stack buffer to help with stack unwindability;
73 | * Replaced old detour method ( rax call ) with ( call rip ) so DKU_H does not dirty register;
74 | *
75 | * 1.0.0 ~ 1.9.2
76 | * CMake integration, inter-library integration;
77 | * Fixed a misrelocation where trampoline ptr may not be pointed to correct address within cave;
78 | * Fixed an error where branch may not be correctly initiated;
79 | * Fixed an misorder where flags may be disabled inappropriately;
80 | * Reordered the cave code layout;
81 | * Added SMART_ALLOC and CAVE related conditional macro, if enabled:
82 | * - Attempt to write patches into code cave to reduce trampoline load if cave size satisfies;
83 | * - Attempt to skip trampoline allocation if hook function is null and cave size satisfies;
84 | * - Attempt to write rax-clean instructions into code cave if a_preserve and cave size satisfies;
85 | * Removed success checks during the writing procedure, due to F4SE64 implementation has no returning value;
86 | * Restructured two separate implementations into conditional compilation;
87 | * Resolve address differently based on which implementation is used;
88 | * Added derived prototype of BranchToFunction<...>(...) without address library usage;
89 | * Added support for ( a_hookFunc = 0 ) so that patches will be applied without branching to any function;
90 | * Added default value for patch related parameters, if branching is done without any patches;
91 | * Fixed various rvalue const cast errors within F4SE64 implementation;
92 | * Added prototype of GetCurrentPtr();
93 | * Added prototype of ResetPtr();
94 | * Renamed some template parameters;
95 | * Changed some local variables to constexpr;
96 | * Added F4SE64 implementation;
97 | * Added VERBOSE conditional to log each step for better debugging;
98 | * Moved predefined values into Impl namespace;
99 | * Changed above values from const to constexpr;
100 | * Renamed InjectAt<...>(...) to BranchToFunction<...>(...);
101 | * Changed patch type from ( const char* ) to ( const void* );
102 | * Removed strlen(...) usage;
103 | * Added additional parameter into InjectAt<...>( a_hookFunc, a_prePatch, a_prePatchSize, a_postPatch, a_postPatchSize );
104 | * Removed BranchAt<...>(...);
105 | * Integrated CodeGenerator;
106 | * Implemented InjectAt<...>(...);
107 | * Added prototype of bool : BranchAt< a_hookFunc >( a_prePatch, a_postPatch );
108 | * Added prototype of InjectAt< ID, START, END >( a_hookFunc, a_prePatch, a_postPatch );
109 | */
110 |
111 | #define DKU_H_VERSION_MAJOR 2
112 | #define DKU_H_VERSION_MINOR 6
113 | #define DKU_H_VERSION_REVISION 4
114 |
115 | #pragma warning(push)
116 | #pragma warning(disable: 4244)
117 |
118 | #include "Impl/pch.hpp"
119 |
120 | namespace DKUtil
121 | {
122 | constexpr auto DKU_H_VERSION = DKU_H_VERSION_MAJOR * 10000 + DKU_H_VERSION_MINOR * 100 + DKU_H_VERSION_REVISION;
123 | } // namespace DKUtil
124 |
125 | #include "Impl/Hook/shared.hpp"
126 |
127 | #include "Impl/Hook/api.hpp"
128 |
129 | namespace DKUtil::Alias
130 | {
131 | using Patch = DKUtil::Hook::Patch;
132 | using HookHandle = std::unique_ptr;
133 | using CaveHandle = DKUtil::Hook::CaveHookHandle;
134 | using VMTHandle = DKUtil::Hook::VMTHookHandle;
135 | using IATHandle = DKUtil::Hook::IATHookHandle;
136 |
137 | using Reg = DKUtil::Hook::Assembly::Register;
138 | using Xmm = DKUtil::Hook::Assembly::SIMD;
139 | template
140 | using Pattern = DKUtil::Hook::Assembly::Pattern::PatternMatcher;
141 |
142 | using HookFlag = DKUtil::Hook::HookFlag;
143 | } // namespace DKUtil::Alias
144 |
145 | #pragma warning(pop)
146 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Config/Data.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "shared.hpp"
4 |
5 | namespace DKUtil::Config::detail
6 | {
7 | enum class DataType
8 | {
9 | kBoolean,
10 | kDouble,
11 | kInteger,
12 | kString,
13 |
14 | kError,
15 | };
16 |
17 | template
18 | struct data_trait
19 | {
20 | using type = DataType;
21 | static constexpr bool is_bool = std::is_same_v;
22 | static constexpr bool is_double = std::is_same_v;
23 | static constexpr bool is_integer = std::is_same_v;
24 | static constexpr bool is_string = std::is_same_v>;
25 | static constexpr auto value()
26 | {
27 | if constexpr (is_bool)
28 | return type::kBoolean;
29 | if constexpr (is_double)
30 | return type::kDouble;
31 | if constexpr (is_integer)
32 | return type::kInteger;
33 | if constexpr (is_string)
34 | return type::kString;
35 | return type::kError;
36 | };
37 | };
38 | template
39 | static constexpr auto data_trait_v = data_trait::value();
40 |
41 | class IData
42 | {
43 | public:
44 | constexpr IData(DataType a_type) noexcept :
45 | _type(a_type)
46 | {}
47 | virtual constexpr ~IData() {}
48 |
49 | [[nodiscard]] constexpr auto get_type() const noexcept { return _type; }
50 |
51 | template
52 | [[nodiscard]] auto* As();
53 |
54 | protected:
55 | DataType _type;
56 | };
57 |
58 | // automatic data with collection enabled
59 | template <
60 | typename data_t,
61 | DataType TYPE = data_trait_v>
62 | class AData : public IData
63 | {
64 | using collection = std::vector;
65 | using IData::IData;
66 |
67 | public:
68 | constexpr AData() noexcept = delete;
69 | constexpr AData(const std::string& a_key, const std::string& a_section = {}) :
70 | IData(TYPE), _key(std::move(a_key)), _section(a_section.empty() ? "Global" : std::move(a_section))
71 | {}
72 |
73 | constexpr AData(const AData&) noexcept = delete;
74 | constexpr AData(AData&&) noexcept = delete;
75 | constexpr ~AData() = default;
76 |
77 | // return the front if out of bounds
78 | [[nodiscard]] auto& operator[](const std::size_t a_index) noexcept
79 | requires(!std::is_same_v)
80 | {
81 | if (_isCollection) {
82 | return a_index < _collection->size() ? _collection->at(a_index) : *_collection->end();
83 | } else {
84 | return _data;
85 | }
86 | }
87 | [[nodiscard]] constexpr operator bool() noexcept
88 | requires(std::is_same_v)
89 | {
90 | return _data;
91 | }
92 | [[nodiscard]] constexpr auto& operator*() noexcept { return _data; }
93 | [[nodiscard]] constexpr std::string_view get_key() const noexcept { return _key; }
94 | [[nodiscard]] constexpr std::string_view get_section() const noexcept { return _section; }
95 | [[nodiscard]] constexpr auto is_collection() const noexcept { return _isCollection; }
96 |
97 | [[nodiscard]] constexpr const auto get_data() const noexcept { return _data; }
98 | [[nodiscard]] constexpr const auto get_collection() const noexcept
99 | {
100 | if (_isCollection) {
101 | return *_collection;
102 | } else {
103 | // force singular data into size-1 collection
104 | return collection{ _data };
105 | }
106 | }
107 | [[nodiscard]] constexpr auto get_size() const noexcept { return _isCollection ? _collection->size() : 0; }
108 | [[nodiscard]] constexpr auto get_type() const noexcept { return typeid(data_t).name(); }
109 | constexpr void debug_dump() const noexcept
110 | {
111 | #ifndef NDEBUG
112 | if (_isCollection) {
113 | std::ranges::for_each(*_collection, [&](data_t val) {
114 | __DEBUG("Setting collection value [{}] to [{}]", val, _key);
115 | });
116 | } else {
117 | __DEBUG("Setting value [{}] to [{}]", _data, _key);
118 | }
119 | #endif
120 | }
121 |
122 | constexpr void set_data(data_t a_value) noexcept
123 | {
124 | _isCollection = false;
125 | _data = a_value;
126 |
127 | clamp();
128 | debug_dump();
129 | }
130 |
131 | constexpr void set_data(const std::initializer_list& a_list)
132 | {
133 | _collection.reset();
134 |
135 | _isCollection = (a_list.size() > 1);
136 | if (_isCollection) {
137 | _collection = std::make_unique(a_list);
138 | }
139 |
140 | _data = *a_list.begin();
141 |
142 | clamp();
143 | debug_dump();
144 | }
145 |
146 | constexpr void set_data(const collection& a_collection)
147 | {
148 | _collection.reset();
149 |
150 | _isCollection = (a_collection.size() > 1);
151 | if (_isCollection) {
152 | _collection = std::make_unique(std::move(a_collection));
153 | _data = _collection->front();
154 | }
155 |
156 | clamp();
157 | debug_dump();
158 | }
159 |
160 | constexpr void set_range(std::pair a_range)
161 | {
162 | if constexpr (model::concepts::dku_numeric) {
163 | _range = a_range;
164 | }
165 | }
166 |
167 | constexpr void clamp()
168 | {
169 | if (!model::concepts::dku_numeric || _range.first > _range.second) {
170 | return;
171 | }
172 |
173 | auto single_clamp = [this](data_t data) {
174 | return data < _range.first ?
175 | _range.first :
176 | (data > _range.second ? _range.second : data);
177 | };
178 |
179 | if (_isCollection) {
180 | *_collection = std::move(*_collection | std::views::transform(single_clamp) | std::ranges::to());
181 | _data = _collection->front();
182 | } else {
183 | _data = single_clamp(_data);
184 | }
185 | }
186 |
187 | private:
188 | const std::string _key;
189 | const std::string _section;
190 | bool _isCollection = false;
191 | data_t _data;
192 | std::pair _range;
193 | std::unique_ptr _collection{ nullptr };
194 | };
195 |
196 | template
197 | auto* IData::As()
198 | {
199 | return dynamic_cast*>(this);
200 | }
201 |
202 | extern template class AData;
203 | extern template class AData;
204 | extern template class AData;
205 | extern template class AData>;
206 | } // namespace DKUtil::Config::detail
207 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Config/Ini.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "data.hpp"
4 |
5 | #include "SimpleIni.h"
6 |
7 | namespace DKUtil::Config::detail
8 | {
9 | class Ini final : public IParser
10 | {
11 | public:
12 | using IParser::IParser;
13 |
14 | void Parse(const char* a_data) noexcept override
15 | {
16 | _ini.SetUnicode();
17 | auto result = a_data ? _ini.LoadData(a_data) : _ini.LoadFile(_filepath.c_str());
18 | if (result < 0) {
19 | FATAL("DKU_C: Parser#{}: Loading failed! -> {}\n{}", _id, _filepath.c_str(), err_getmsg());
20 | }
21 |
22 | CSimpleIniA::TNamesDepend sections;
23 | _ini.GetAllSections(sections);
24 |
25 | for (auto& section : sections) {
26 | CSimpleIniA::TNamesDepend keys;
27 | _ini.GetAllKeys(section.pItem, keys);
28 |
29 | for (auto& key : keys) {
30 | const char* value = _ini.GetValue(section.pItem, key.pItem);
31 | if (!value) {
32 | continue;
33 | }
34 |
35 | if (_manager.contains(std::make_pair(key.pItem, section.pItem))) {
36 | std::string raw{ value };
37 |
38 | auto& data = _manager.at(std::make_pair(key.pItem, section.pItem));
39 | switch (data->get_type()) {
40 | case DataType::kBoolean:
41 | {
42 | if (raw == "0" || dku::string::iequals(raw, "false")) {
43 | data->As()->set_data(false);
44 | } else if (raw == "1" || dku::string::iequals(raw, "true")) {
45 | data->As()->set_data(true);
46 | } else {
47 | err_mismatch("Invalid bool input", key.pItem, "Boolean", value);
48 | }
49 |
50 | break;
51 | }
52 | case DataType::kDouble:
53 | {
54 | auto sv = dku::string::split(raw, ",", " ");
55 | try {
56 | if (sv.size() <= 1) {
57 | data->As()->set_data(std::stod(value));
58 | } else {
59 | auto tv = sv | std::views::transform([](std::string str) { return std::stod(str); });
60 | data->As()->set_data({ tv.begin(), tv.end() });
61 | }
62 | } catch (const std::exception& e) {
63 | err_mismatch(e.what(), key.pItem, "Double", value);
64 | }
65 |
66 | break;
67 | }
68 | case DataType::kInteger:
69 | {
70 | auto sv = dku::string::split(raw, ",", " ");
71 | try {
72 | if (sv.size() <= 1) {
73 | data->As()->set_data(std::stoll(value));
74 | } else {
75 | auto tv = sv | std::views::transform([](std::string& str) { return std::stoll(str); });
76 | data->As()->set_data({ tv.begin(), tv.end() });
77 | }
78 | } catch (const std::exception& e) {
79 | err_mismatch(e.what(), key.pItem, "Double", value);
80 | }
81 |
82 | break;
83 | }
84 | case DataType::kString:
85 | {
86 | std::string old{ raw };
87 | dku::string::replace_nth_occurrence(raw, 0, "\\,", "_dku_comma_");
88 | dku::string::replace_nth_occurrence(raw, 0, "\\\"", "_dku_quote_");
89 |
90 | auto sv = dku::string::split(raw, ",");
91 | try {
92 | if (sv.size() <= 1) {
93 | old = dku::string::trim(old);
94 | dku::string::replace_nth_occurrence(old, 0, "\"");
95 | data->As>()->set_data(old);
96 | } else {
97 | std::ranges::for_each(sv, [](std::string& str) {
98 | dku::string::replace_nth_occurrence(str, 0, "\"");
99 | dku::string::replace_nth_occurrence(str, 0, "_dku_comma_", "\\,");
100 | dku::string::replace_nth_occurrence(str, 0, "_dku_quote_", "\\\"");
101 | str = dku::string::trim(str);
102 | });
103 | data->As>()->set_data(sv);
104 | }
105 | } catch (const std::exception& e) {
106 | err_mismatch(e.what(), key.pItem, "String", value);
107 | }
108 |
109 | break;
110 | }
111 | case DataType::kError:
112 | default:
113 | continue;
114 | }
115 | }
116 | }
117 | }
118 |
119 | auto sr = _ini.Save(_content);
120 | if (sr < 0) {
121 | ERROR("DKU_C: Parser#{}: Saving data failed!\nFile: {}\n{}", _id, _filepath, err_getmsg());
122 | }
123 |
124 | __DEBUG("DKU_C: Parser#{}: Parsing finished", _id);
125 | }
126 |
127 | void Write(const std::string_view a_filePath) noexcept override
128 | {
129 | auto result = a_filePath.empty() ? _ini.SaveFile(_filepath.c_str()) : _ini.SaveFile(a_filePath.data());
130 | if (result < 0) {
131 | ERROR("DKU_C: Parser#{}: Writing file failed!\nFile: {}\n{}", _id, _filepath, err_getmsg());
132 | }
133 |
134 | __DEBUG("DKU_C: Parser#{}: Writing finished", _id);
135 | }
136 |
137 | void Generate() noexcept override
138 | {
139 | CSimpleIniA::TNamesDepend sections;
140 | _ini.GetAllSections(sections);
141 |
142 | for (auto& section : sections) {
143 | _ini.Delete(section.pItem, nullptr);
144 | }
145 |
146 | for (auto& [key, data] : _manager) {
147 | auto sanitized = key.second.empty() ? "Global"sv : key.second;
148 | std::string raw{};
149 | switch (data->get_type()) {
150 | case DataType::kBoolean:
151 | {
152 | raw = data->As()->get_data() ? "true" : "false";
153 | break;
154 | }
155 | case DataType::kDouble:
156 | {
157 | raw = std::to_string(data->As()->get_data());
158 | break;
159 | }
160 | case DataType::kInteger:
161 | {
162 | raw = std::to_string(data->As()->get_data());
163 | break;
164 | }
165 | case DataType::kString:
166 | {
167 | auto* str = data->As>();
168 | if (str->is_collection()) {
169 | raw = dku::string::join(str->get_collection(), ", "sv);
170 | } else {
171 | raw = str->get_data();
172 | }
173 |
174 | break;
175 | }
176 | case DataType::kError:
177 | default:
178 | continue;
179 | }
180 |
181 | auto rc = _ini.SetValue(sanitized.data(), key.first.data(), raw.data());
182 | if (rc == SI_FAIL) {
183 | ERROR(
184 | "DKU_C: Parser#{}: failed generating default value\n"
185 | "File: {}\nKey: {}, Section: {}\nType: {}\nValue: {}",
186 | _id, _filepath.data(), key.first, sanitized, dku::print_enum(data->get_type()), raw.data());
187 | }
188 | }
189 |
190 | __DEBUG("DKU_C: Parser#{}: Generating finished", _id);
191 | }
192 |
193 | private:
194 | const char* err_getmsg() noexcept
195 | {
196 | std::ranges::fill(errmsg, 0);
197 | strerror_s(errmsg, errno);
198 | return errmsg;
199 | }
200 |
201 | void err_mismatch(std::string_view a_key, std::string_view a_type, std::string_view a_value, std::string_view a_what) noexcept
202 | {
203 | ERROR("DKU_C: Parser#{}: {}\nValue type mismatch!\nFile: {}\nKey: {}, Expected: {}, Value: {}", _id, a_what, _filepath.c_str(), a_key, a_type, a_value);
204 | }
205 |
206 | CSimpleIniA _ini;
207 | char errmsg[72];
208 | };
209 | } // namespace DKUtil::Config
210 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Config/Json.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "data.hpp"
4 |
5 | #include "nlohmann/json.hpp"
6 |
7 | namespace DKUtil::Config::detail
8 | {
9 | class Json final : public IParser
10 | {
11 | public:
12 | using IParser::IParser;
13 | using json = nlohmann::json;
14 |
15 | void Parse(const char* a_data) noexcept override
16 | {
17 | if (a_data) {
18 | _json = json::parse(a_data);
19 | } else {
20 | std::basic_ifstream file{ _filepath };
21 | if (!file.is_open()) {
22 | FATAL("DKU_C: Parser#{}: Loading failed! -> {}", _id, _filepath.c_str());
23 | }
24 |
25 | file >> _json;
26 | file.close();
27 | }
28 |
29 | for (auto& [key, data] : _manager) {
30 | auto raw = _json.find(key.first.data());
31 | if (raw == _json.end()) {
32 | ERROR("DKU_C: Parser#{}: Retrieving config failed!\nFile: {}\nKey: {}", _id, _filepath.c_str(), key.first);
33 | }
34 |
35 | switch (data->get_type()) {
36 | case DataType::kBoolean:
37 | {
38 | data->As()->set_data(raw->get());
39 | break;
40 | }
41 | case DataType::kDouble:
42 | {
43 | if (raw->type() == json::value_t::array) {
44 | data->As()->set_data(raw->get>());
45 | } else {
46 | data->As()->set_data(raw->get());
47 | }
48 | break;
49 | }
50 | case DataType::kInteger:
51 | {
52 | if (raw->type() == json::value_t::array) {
53 | data->As()->set_data(raw->get>());
54 | } else {
55 | data->As()->set_data(raw->get());
56 | }
57 | break;
58 | }
59 | case DataType::kString:
60 | {
61 | if (raw->type() == json::value_t::array) {
62 | data->As>()->set_data(raw->get>>());
63 | } else {
64 | data->As>()->set_data(raw->get>());
65 | }
66 | break;
67 | }
68 | case DataType::kError:
69 | default:
70 | continue;
71 | }
72 | }
73 |
74 | _content = std::move(_json.dump());
75 | __DEBUG("DKU_C: Parser#{}: Parsing finished", _id);
76 | }
77 |
78 | void Write(const std::string_view a_filePath) noexcept override
79 | {
80 | auto* filePath = a_filePath.empty() ? _filepath.data() : a_filePath.data();
81 | std::basic_ofstream file{ filePath };
82 | if (!file.is_open() || !file) {
83 | ERROR("DKU_C: Parser#{}: Writing file failed! -> {}\nofstream cannot be opened", _id, filePath);
84 | }
85 |
86 | file << _json.dump(4);
87 | file.close();
88 |
89 | __DEBUG("DKU_C: Parser#{}: Writing finished", _id);
90 | }
91 |
92 | void Generate() noexcept override
93 | {
94 | _json.clear();
95 | for (auto& [key, data] : _manager) {
96 | switch (data->get_type()) {
97 | case DataType::kBoolean:
98 | {
99 | _json[key.first.data()] = data->As()->get_data();
100 | break;
101 | }
102 | case DataType::kDouble:
103 | {
104 | if (auto* raw = data->As(); raw->is_collection()) {
105 | _json[key.first.data()] = raw->get_collection();
106 | } else {
107 | _json[key.first.data()] = raw->get_data();
108 | }
109 | break;
110 | }
111 | case DataType::kInteger:
112 | {
113 | if (auto* raw = data->As(); raw->is_collection()) {
114 | _json[key.first.data()] = raw->get_collection();
115 | } else {
116 | _json[key.first.data()] = raw->get_data();
117 | }
118 | break;
119 | }
120 | case DataType::kString:
121 | {
122 | if (auto* raw = data->As>(); raw->is_collection()) {
123 | _json[key.first.data()] = raw->get_collection();
124 | } else {
125 | _json[key.first.data()] = raw->get_data();
126 | }
127 | break;
128 | }
129 | case DataType::kError:
130 | default:
131 | continue;
132 | }
133 | }
134 | }
135 |
136 | private:
137 | json _json;
138 | };
139 | } // namespace DKUtil::Config
140 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Config/Proxy.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "shared.hpp"
4 |
5 | #define __eval_helper(SRC) DKUtil::Config::EvaluateConfig([]() { return SRC; })
6 | // compile-time evaluation
7 | #define COMPILE_PROXY(SRC) DKUtil::Config::Proxy<__eval_helper(SRC)>(SRC)
8 | // runtime dynamic
9 | #define RUNTIME_PROXY(SRC) DKUtil::Config::Proxy(SRC)
10 | // schema configs
11 | #define SCHEMA_PROXY(SRC) DKUtil::Config::Proxy(SRC)
12 |
13 | namespace DKUtil::Config
14 | {
15 | enum class FileType
16 | {
17 | kDynamic = 0,
18 | kIni,
19 | kJson,
20 | kToml,
21 | kSchema,
22 |
23 | kError
24 | };
25 |
26 | // compile-time file type evaluation from literal/literal view
27 | template
28 | consteval FileType EvaluateConfig(const input_string_t a_file)
29 | {
30 | constexpr std::basic_string_view file = a_file();
31 | static_assert(!file.empty(), "Empty filename passed");
32 | static_assert(file.size() > 4, "Filename too short");
33 |
34 | // where is case insensitive consteval compare?!
35 | constexpr auto extension = file.substr(file.size() - 4);
36 | if constexpr (extension[0] == '.' &&
37 | (extension[1] == 'i' || extension[1] == 'I') &&
38 | (extension[2] == 'n' || extension[2] == 'N') &&
39 | (extension[3] == 'i' || extension[3] == 'I')) {
40 | return FileType::kIni;
41 | }
42 | if constexpr ((extension[0] == 'j' || extension[0] == 'J') &&
43 | (extension[1] == 's' || extension[1] == 'S') &&
44 | (extension[2] == 'o' || extension[2] == 'O') &&
45 | (extension[3] == 'n' || extension[3] == 'N')) {
46 | return FileType::kJson;
47 | }
48 | if constexpr ((extension[0] == 't' || extension[0] == 'T') &&
49 | (extension[1] == 'o' || extension[1] == 'O') &&
50 | (extension[2] == 'm' || extension[2] == 'M') &&
51 | (extension[3] == 'l' || extension[3] == 'L')) {
52 | return FileType::kToml;
53 | }
54 |
55 | return FileType::kError;
56 | }
57 |
58 | template
59 | requires(ConfigFileType != FileType::kError)
60 | class Proxy
61 | {
62 | // clang-format off
63 | using parser_t =
64 | std::conditional_t>>>;
68 | // clang-format on
69 | public:
70 | // compile defined
71 | constexpr explicit Proxy(const std::string_view a_file) noexcept
72 | requires(ConfigFileType != FileType::kDynamic)
73 | :
74 | _id(detail::_Count++),
75 | _filename(a_file),
76 | _type(ConfigFileType), _parser(std::make_unique(a_file, _id, _manager))
77 | {
78 | __DEBUG("DKU_C: Proxy#{}: Compile -> {}", _id, _filename);
79 | }
80 |
81 | // runtime defined
82 | constexpr explicit Proxy(const std::string_view a_file) noexcept
83 | requires(ConfigFileType == FileType::kDynamic)
84 | :
85 | _id(detail::_Count++),
86 | _filename(a_file)
87 | {
88 | if (dku::string::iends_with(a_file, "ini")) {
89 | _parser = std::make_unique(a_file, _id, _manager);
90 | } else if (dku::string::iends_with(a_file, "json")) {
91 | _parser = std::make_unique(a_file, _id, _manager);
92 | } else if (dku::string::iends_with(a_file, "toml")) {
93 | _parser = std::make_unique(a_file, _id, _manager);
94 | } else {
95 | ERROR("DKU_C: Proxy#{}: No suitable parser found for file -> {}", _id, a_file);
96 | }
97 |
98 | __DEBUG("DKU_C: Proxy#{}: Runtime -> {}", _id, _filename);
99 | }
100 |
101 | Proxy() = default;
102 | Proxy(const Proxy&) = delete;
103 | Proxy(Proxy&&) = default;
104 | ~Proxy() = default;
105 |
106 | Proxy& operator=(const Proxy&) = delete;
107 | Proxy& operator=(Proxy&&) = default;
108 |
109 | void Load(const char* a_data = nullptr) noexcept
110 | {
111 | __DEBUG("DKU_C: Proxy#{}: Loading -> {}", _id, _filename);
112 |
113 | if (GenerateIfMissing()) {
114 | _parser->Parse(a_data);
115 | }
116 | }
117 |
118 | void Write(const std::string_view a_file = {}) noexcept
119 | {
120 | __DEBUG("DKU_C: Proxy#{}: Writing -> {}", _id, _filename);
121 |
122 | _parser->Write(a_file);
123 | }
124 |
125 | template <
126 | const double min = 1.,
127 | const double max = 0.,
128 | typename data_t>
129 | constexpr void Bind(detail::AData& a_data, const std::convertible_to auto&... a_value) noexcept
130 | {
131 | static_assert(ConfigFileType != FileType::kSchema, "Schema parser cannot use regular data bindings!");
132 |
133 | _manager.try_emplace(std::make_pair(a_data.get_key(), a_data.get_section()), std::addressof(a_data));
134 | a_data.set_range({ min, max });
135 | a_data.set_data({ static_cast(a_value)... });
136 | }
137 |
138 | constexpr void Generate() noexcept
139 | {
140 | __DEBUG("DKU_C: Proxy#{}: Generating -> {}", _id, _filename);
141 |
142 | _parser->Generate();
143 | }
144 |
145 | constexpr bool GenerateIfMissing() noexcept
146 | {
147 | auto found = std::filesystem::exists(_parser->filepath());
148 |
149 | if (!found) {
150 | Generate();
151 | Write();
152 | }
153 |
154 | return found;
155 | }
156 |
157 | [[nodiscard]] constexpr auto get_id() const noexcept { return _id; }
158 | [[nodiscard]] constexpr auto get_filename() const noexcept { return _filename; }
159 | [[nodiscard]] constexpr auto get_type() const noexcept { return _type; }
160 | [[nodiscard]] constexpr auto& get_parser() const noexcept { return *_parser; }
161 | [[nodiscard]] constexpr auto* data() noexcept { return _parser->data(); }
162 |
163 | private:
164 | const std::uint32_t _id;
165 | const std::string _filename;
166 | FileType _type;
167 | std::unique_ptr _parser;
168 | detail::manager _manager; // key,
169 | };
170 | } // namespace DKUtil::Config
171 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Config/Shared.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "DKUtil/Impl/pch.hpp"
4 | #include "DKUtil/Logger.hpp"
5 | #include "DKUtil/Utility.hpp"
6 |
7 | namespace DKUtil::Config
8 | {
9 | inline auto GetPath(const std::string_view a_file) noexcept
10 | {
11 | std::filesystem::path dir{ CONFIG_ENTRY };
12 | if (dir.is_relative() || dir.empty()) {
13 | dir = std::filesystem::current_path() / dir;
14 | }
15 |
16 | std::filesystem::path file{ a_file.data() };
17 | if (!a_file.empty() && file.is_relative()) {
18 | file = dir / file;
19 | }
20 |
21 | return file.string();
22 | }
23 |
24 | template
25 | inline std::vector GetAllFiles(std::string_view a_path = {}, std::string_view a_ext = {}, std::string_view a_prefix = {}, std::string_view a_suffix = {}) noexcept
26 | {
27 | using dir_iterator = std::conditional_t;
28 |
29 | std::vector files;
30 | auto file_iterator = [&](const std::filesystem::directory_entry& a_file) {
31 | if (a_file.exists() &&
32 | !a_file.path().empty()) {
33 | if (!a_ext.empty() && a_file.path().extension() != a_ext) {
34 | return;
35 | }
36 |
37 | const auto path = a_file.path().string();
38 |
39 | if (!a_prefix.empty() && path.find(a_prefix) != std::string::npos) {
40 | files.push_back(path);
41 | } else if (!a_suffix.empty() && path.rfind(a_suffix) != std::string::npos) {
42 | files.push_back(path);
43 | } else if (a_prefix.empty() && a_suffix.empty()) {
44 | files.push_back(path);
45 | }
46 | }
47 | };
48 |
49 | std::string dir(MAX_PATH + 1, ' ');
50 | auto res = GetModuleFileNameA(nullptr, dir.data(), MAX_PATH + 1);
51 | if (res == 0) {
52 | ERROR("DKU_C: Unable to acquire valid path using default null path argument!\nExpected: Current directory\nResolved: NULL");
53 | }
54 |
55 | auto eol = dir.find_last_of("\\/");
56 | dir = dir.substr(0, eol);
57 |
58 | auto path = a_path.empty() ? std::filesystem::path{ dir } : std::filesystem::path{ a_path };
59 | if (!is_directory(path.parent_path())) {
60 | path = dir / path;
61 | }
62 |
63 | std::ranges::for_each(dir_iterator(path), file_iterator);
64 | std::ranges::sort(files);
65 |
66 | return files;
67 | }
68 |
69 | namespace detail
70 | {
71 | struct section_key_hash
72 | {
73 | size_t operator()(const std::pair& key) const
74 | {
75 | // Hash combining algorithm taken from boost::hash_combine
76 | auto hash = std::hash()(key.first);
77 | hash ^= std::hash()(key.second) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
78 | return hash;
79 | }
80 | };
81 |
82 | class IData;
83 | // , data*
84 | using manager = std::unordered_map, detail::IData*, section_key_hash>;
85 |
86 | inline static std::uint32_t _Count{ 0 };
87 |
88 | class IParser
89 | {
90 | public:
91 | explicit IParser(std::string_view a_file, const std::uint32_t a_id, manager& a_manager) :
92 | _filename(Logger::detail::short_file(a_file.data())), _filepath(GetPath(a_file)), _id(a_id), _manager(a_manager)
93 | {}
94 |
95 | constexpr IParser() = delete;
96 | constexpr IParser(const IParser&) noexcept = default;
97 | constexpr IParser(IParser&&) noexcept = default;
98 | constexpr virtual ~IParser() = default;
99 |
100 | // accessor
101 | [[nodiscard]] constexpr auto* data() noexcept { return _content.data(); }
102 | [[nodiscard]] constexpr auto& content() const noexcept { return _content; }
103 | [[nodiscard]] constexpr std::string_view filename() const noexcept { return _filename; }
104 | [[nodiscard]] constexpr std::string_view filepath() const noexcept { return _filepath; }
105 |
106 | virtual void Parse(const char* = nullptr) noexcept = 0;
107 | virtual void Write(const std::string_view) noexcept = 0;
108 | virtual void Generate() noexcept = 0;
109 |
110 | protected:
111 | const std::uint32_t _id;
112 | const std::string _filepath;
113 | const std::string _filename;
114 | std::string _content;
115 | const manager& _manager;
116 | };
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Config/Toml.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "data.hpp"
4 |
5 | #define TOML_EXCEPTIONS 0
6 | #include "external/toml.hpp"
7 |
8 | namespace DKUtil::Config::detail
9 | {
10 | class Toml final : public IParser
11 | {
12 | public:
13 | using IParser::IParser;
14 |
15 | void Parse(const char* a_data) noexcept override
16 | {
17 | auto result = a_data ? toml::parse(a_data) : toml::parse_file(_filepath);
18 | if (!result) {
19 | FATAL("DKU_C: Parser#{}: Parsing failed!\nFile: {}\nDesc: {}", _id, *result.error().source().path.get(), result.error().description());
20 | }
21 |
22 | _toml = std::move(result).table();
23 | for (auto& [section, table] : _toml) {
24 | if (!table.is_table()) {
25 | __INFO("DKU_C: WARNING\nParser#{}: Sectionless configuration present and skipped.\nPossible inappropriate formatting at [{}]", _id, section.str());
26 | continue;
27 | } else {
28 | for (auto& [key, data] : _manager) {
29 | if (section != key.second) {
30 | continue;
31 | }
32 |
33 | auto raw = table.as_table()->find(key.first.data());
34 | if (table.as_table()->begin() != table.as_table()->end() &&
35 | raw == table.as_table()->end()) {
36 | continue;
37 | }
38 |
39 | switch (data->get_type()) {
40 | case DataType::kBoolean:
41 | {
42 | if (raw->second.as_boolean()) {
43 | data->As()->set_data(raw->second.as_boolean()->get());
44 | }
45 | break;
46 | }
47 | case DataType::kDouble:
48 | {
49 | double input;
50 | if (raw->second.is_array() && raw->second.as_array()) {
51 | if (raw->second.as_array()->size() == 1 &&
52 | raw->second.as_array()->front().value()) {
53 | input = raw->second.as_array()->front().value().value();
54 | } else if (raw->second.as_array()->size()) {
55 | std::vector array;
56 | for (auto& node : *raw->second.as_array()) {
57 | // default to 0 for numeric types
58 | array.push_back(node.value_or(0.0));
59 | }
60 |
61 | data->As()->set_data(array);
62 | break;
63 | }
64 | } else {
65 | input = raw->second.value_or(0.0);
66 | }
67 |
68 | data->As()->set_data(input);
69 | break;
70 | }
71 | case DataType::kInteger:
72 | {
73 | std::int64_t input;
74 | if (raw->second.is_array() && raw->second.as_array()) {
75 | if (raw->second.as_array()->size() == 1) {
76 | auto& front = raw->second.as_array()->front();
77 | // downcast
78 | input = front.value() ?
79 | front.value().value() :
80 | front.value_or(0);
81 | } else if (raw->second.as_array()->size() > 1) {
82 | std::vector array;
83 | for (auto& node : *raw->second.as_array()) {
84 | // default to 0 for numeric types
85 | // downcast
86 | input = node.value() ?
87 | node.value().value() :
88 | node.value_or(0);
89 | array.push_back(input);
90 | }
91 |
92 | data->As()->set_data(array);
93 | break;
94 | }
95 | } else {
96 | // downcast
97 | input = raw->second.value() ?
98 | raw->second.value().value() :
99 | raw->second.value_or(0);
100 | }
101 |
102 | data->As()->set_data(input);
103 | break;
104 | }
105 | case DataType::kString:
106 | {
107 | std::basic_string input;
108 | if (raw->second.is_array() && raw->second.as_array()) {
109 | if (raw->second.as_array()->size() == 1 &&
110 | raw->second.as_array()->front().as_string()) {
111 | input = raw->second.as_array()->front().as_string()->get();
112 | } else if (raw->second.as_array()->size()) {
113 | std::vector> array;
114 | for (auto& node : *raw->second.as_array()) {
115 | if (node.as_string()) {
116 | array.push_back(node.as_string()->get());
117 | }
118 | }
119 |
120 | data->As>()->set_data(array);
121 | break;
122 | }
123 | } else {
124 | if (raw->second.as_string()) {
125 | input = raw->second.as_string()->get();
126 | }
127 | }
128 |
129 | data->As>()->set_data(input);
130 | break;
131 | }
132 | case DataType::kError:
133 | default:
134 | continue;
135 | }
136 | }
137 | }
138 | }
139 |
140 | std::stringstream os{};
141 | os << _toml;
142 | _content = std::move(os.str());
143 |
144 | __DEBUG("DKU_C: Parser#{}: Parsing finished", _id);
145 | }
146 |
147 | void Write(const std::string_view a_filePath) noexcept override
148 | {
149 | auto filePath = a_filePath.empty() ? _filepath.c_str() : a_filePath.data();
150 | std::basic_ofstream file{ filePath };
151 | if (!file.is_open() || !file) {
152 | ERROR("DKU_C: Parser#{}: Writing file failed! -> {}\nofstream cannot be opened", _id, filePath);
153 | }
154 |
155 | file << _toml;
156 | file.close();
157 |
158 | __DEBUG("DKU_C: Parser#{}: Writing finished", _id);
159 | }
160 |
161 | void Generate() noexcept override
162 | {
163 | auto tableGnt = [](RNG&& a_rng) {
164 | toml::array collection;
165 | std::ranges::for_each(a_rng, [&collection](auto value) {
166 | collection.push_back(value);
167 | });
168 | return collection;
169 | };
170 |
171 | _toml.clear();
172 | for (auto& [key, data] : _manager) {
173 | std::string sanitized = key.second.empty() ? "Global" : key.second.data();
174 | auto [section, success] = _toml.insert(sanitized, toml::table{});
175 | auto* table = section->second.as_table();
176 |
177 | switch (data->get_type()) {
178 | case DataType::kBoolean:
179 | {
180 | table->insert(key.first, data->As()->get_data());
181 | break;
182 | }
183 | case DataType::kDouble:
184 | {
185 | if (auto* raw = data->As(); raw->is_collection()) {
186 | table->insert(key.first, tableGnt(raw->get_collection()));
187 | } else {
188 | table->insert(key.first, raw->get_data());
189 | }
190 | break;
191 | }
192 | case DataType::kInteger:
193 | {
194 | if (auto* raw = data->As(); raw->is_collection()) {
195 | table->insert(key.first, tableGnt(raw->get_collection()));
196 | } else {
197 | table->insert(key.first, raw->get_data());
198 | }
199 | break;
200 | }
201 | case DataType::kString:
202 | {
203 | if (auto* raw = data->As>(); raw->is_collection()) {
204 | table->insert(key.first, tableGnt(raw->get_collection()));
205 | } else {
206 | table->insert(key.first, raw->get_data());
207 | }
208 | break;
209 | }
210 | case DataType::kError:
211 | default:
212 | continue;
213 | }
214 | }
215 | }
216 |
217 | private:
218 | toml::table _toml;
219 | };
220 | } // namespace detail
221 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Extra/Serialization/exception.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "shared.hpp"
4 |
5 | namespace DKUtil::serialization
6 | {
7 | namespace exception
8 | {
9 | enum class code : index_type
10 | {
11 | failed_to_open_record,
12 | failed_to_write_data,
13 | failed_to_read_data,
14 | failed_to_resolve_formId,
15 | failed_to_revert,
16 |
17 | unexpected_type_mismatch,
18 | bindable_type_unpackable,
19 | internal_buffer_overflow,
20 | invalid_skse_interface,
21 | };
22 |
23 | #define DKU_XS_EXCEPTION_FMT \
24 | "Serialization error\n[{}]\n\nrecord: {}\nversion: {}\ntype: {}\n\n{}\n\n", \
25 | dku::print_enum(a_code), a_header.name, a_header.version, typeid(T).name(), a_fmt
26 |
27 | template
28 | void report(
29 | code a_code,
30 | std::string_view a_fmt = {},
31 | ISerializable::Header a_header = { "DKU_X: ", 0x1234, DKU_XS_VERSION })
32 | {
33 | if constexpr (PROMPT) {
34 | ERROR(DKU_XS_EXCEPTION_FMT);
35 | } else {
36 | __WARN(DKU_XS_EXCEPTION_FMT);
37 | }
38 | }
39 | } // namespace exception
40 | } // namespace DKUtil::serialization
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Extra/Serialization/interface.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "exception.hpp"
4 | #include "shared.hpp"
5 |
6 | namespace DKUtil::serialization
7 | {
8 | namespace api
9 | {
10 | namespace detail
11 | {
12 | inline auto* check_skse_intfc(SKSE::SerializationInterface* a_intfc = nullptr)
13 | {
14 | const auto* intfc = a_intfc ? a_intfc : SKSE::GetSerializationInterface();
15 |
16 | if (!a_intfc && !intfc) {
17 | exception::report(exception::code::invalid_skse_interface);
18 | }
19 |
20 | return intfc;
21 | }
22 |
23 | void save_all(SKSE::SerializationInterface* a_intfc)
24 | {
25 | #ifndef DKU_X_MOCK
26 | check_skse_intfc(a_intfc);
27 |
28 | for (auto* serializable : ISerializable::ManagedSerializables) {
29 | if (!a_intfc->OpenRecord(serializable->header.hash, serializable->header.version)) {
30 | exception::report(exception::code::failed_to_open_record,
31 | fmt::format("type: {}", serializable->header.typeInfo));
32 | continue;
33 | } else {
34 | serializable->try_save();
35 | }
36 | }
37 | #endif
38 | }
39 |
40 | void load_all(SKSE::SerializationInterface* a_intfc)
41 | {
42 | #ifndef DKU_X_MOCK
43 | check_skse_intfc(a_intfc);
44 |
45 | std::uint32_t hash, version, length;
46 | while (a_intfc->GetNextRecordInfo(hash, version, length)) {
47 | for (auto* serializable : ISerializable::ManagedSerializables) {
48 | serializable->try_load(hash, version);
49 | }
50 | }
51 | #endif
52 | }
53 |
54 | void revert_all(SKSE::SerializationInterface* a_intfc)
55 | {
56 | for (auto* serializable : ISerializable::ManagedSerializables) {
57 | serializable->try_revert();
58 | }
59 | }
60 | } // namespace detail
61 |
62 | #ifndef DKU_X_MOCK
63 |
64 | # define DKU_X_WRITE(D, L, T) api::write(D, L, a_res.header)
65 | # define DKU_X_WRITE_SIZE(D) DKU_X_WRITE(std::addressof(D), sizeof(D), decltype(data))
66 | # define DKU_X_READ(D, L, T) api::read(D, L, a_res.header)
67 | # define DKU_X_READ_SIZE(D) DKU_X_READ(std::addressof(D), sizeof(D), decltype(data))
68 | # define DKU_X_REPORT() api::report()
69 | # define DKU_X_FORMID(F) api::resolveFormId(F)
70 |
71 | template
72 | inline void write(const void* a_buf, size_type a_length, const ISerializable::Header& a_header) noexcept
73 | {
74 | const auto* intfc = detail::check_skse_intfc();
75 | if (!intfc->WriteRecordData(a_buf, a_length)) {
76 | exception::report(exception::code::failed_to_write_data,
77 | fmt::format("size: {}B", a_length), a_header);
78 | }
79 | }
80 |
81 | template
82 | inline void read(void* a_buf, size_type a_length, const ISerializable::Header& a_header) noexcept
83 | {
84 | const auto* intfc = detail::check_skse_intfc();
85 | if (!intfc->ReadRecordData(a_buf, a_length)) {
86 | exception::report(exception::code::failed_to_read_data,
87 | fmt::format("size: {}B", a_length), a_header);
88 | }
89 | }
90 |
91 | inline RE::FormID resolveFormId(RE::FormID a_form) noexcept
92 | {
93 | RE::FormID newForm;
94 | const auto* intfc = detail::check_skse_intfc();
95 |
96 | if (!intfc->ResolveFormID(a_form, newForm) && !newForm) {
97 | exception::report(exception::code::failed_to_resolve_formId,
98 | fmt::format("old formId: {:X} | resolved: {:X}", a_form, newForm));
99 | }
100 |
101 | return newForm;
102 | }
103 |
104 | inline void report() noexcept
105 | {
106 | const auto* intfc = detail::check_skse_intfc();
107 | __DEBUG("\nSKSE::SerializationInterface version: {}\nDKU_X_SERIALIZE version: {}", intfc->Version(), DKU_XS_VERSION);
108 | }
109 | #endif
110 |
111 | inline void RegisterSerializable(std::string a_ref = {}) noexcept
112 | {
113 | const auto* intfc = detail::check_skse_intfc();
114 |
115 | a_ref += "_DKU_XS";
116 | auto&& [key, hash] = colliding::make_hash_key(a_ref.data());
117 |
118 | intfc->SetUniqueID(hash);
119 | intfc->SetSaveCallback(detail::save_all);
120 | intfc->SetLoadCallback(detail::load_all);
121 | intfc->SetRevertCallback(detail::revert_all);
122 |
123 | __DEBUG("DKU_XS: Registered serializables\nplugin handle {}\nplugin hash {}", key, hash);
124 | DKU_X_REPORT();
125 | }
126 | } // DKUtil::serialization::api
127 | } // DKUtil::serialization
128 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Extra/Serialization/mock.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "exception.hpp"
4 | #include "shared.hpp"
5 |
6 | #if defined(DKU_X_MOCK)
7 | # include
8 |
9 | # define DKU_X_WRITE(D, L, T) mock::write(D, L)
10 | # define DKU_X_WRITE_SIZE(D) DKU_X_WRITE(std::addressof(D), sizeof(D), decltype(D))
11 | # define DKU_X_READ(D, L, T) mock::read(D, L)
12 | # define DKU_X_READ_SIZE(D) DKU_X_READ(std::addressof(D), sizeof(D), decltype(D))
13 | # define DKU_X_REPORT() mock::report()
14 | # define DKU_X_FORMID(F) F
15 |
16 | namespace DKUtil::serialization
17 | {
18 | namespace mock
19 | {
20 | inline static std::array Buffer;
21 | inline static size_type ReadPos = 0;
22 | inline static size_type WritePos = 0;
23 |
24 | inline void read(void* a_buf, size_type a_length) noexcept
25 | {
26 | std::memcpy(a_buf, Buffer.data() + ReadPos, a_length);
27 | ReadPos += a_length;
28 | __INFO(fmt::format(
29 | fmt::bg(fmt::terminal_color::green) | fmt::fg(fmt::terminal_color::black),
30 | "[mock] read {}B", a_length));
31 | }
32 |
33 | inline void write(const void* a_buf, size_type a_length) noexcept
34 | {
35 | std::memcpy(Buffer.data() + WritePos, a_buf, a_length);
36 | WritePos += a_length;
37 | __INFO(fmt::format(
38 | fmt::bg(fmt::terminal_color::cyan) | fmt::fg(fmt::terminal_color::black),
39 | "[mock] write {}B", a_length));
40 | }
41 |
42 | inline void clear() noexcept
43 | {
44 | Buffer.fill(std::byte{ 0 });
45 | ReadPos = 0;
46 | WritePos = 0;
47 | }
48 |
49 | inline void report() noexcept
50 | {
51 | __INFO("[mock] current read {}B", ReadPos);
52 | __INFO("[mock] current write {}B", WritePos);
53 | }
54 | } // namespace mock
55 | } // namespace DKUtil::serialization
56 | #endif
57 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Extra/Serialization/shared.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "DKUtil/Logger.hpp"
4 | #include "DKUtil/Utility.hpp"
5 |
6 | #define DKU_X_SERIALIZE_MAJOR 1
7 | #define DKU_X_SERIALIZE_MINOR 0
8 | #define DKU_X_SERIALIZE_REVISION 0
9 |
10 | namespace DKUtil
11 | {
12 | constexpr auto DKU_XS_VERSION = DKU_X_SERIALIZE_MAJOR * 10000 + DKU_X_SERIALIZE_MINOR * 100 + DKU_X_SERIALIZE_REVISION;
13 | } // namespace DKUtil
14 |
15 | namespace DKUtil::serialization
16 | {
17 | using size_type = std::uint32_t;
18 | using index_type = std::uint32_t;
19 | using key_type = std::string;
20 | using hash_type = std::uint32_t;
21 | using version_type = std::uint32_t;
22 |
23 | namespace colliding
24 | {
25 | inline static constexpr hash_type KnownHash[] = {
26 | 'COMP', // CompletionistNG
27 | 'ISCR', // IndividualSoutCooldownRemake
28 | 'SAMS', // achievement system for skyrim
29 | 'SPIS', // SplitItemStacks
30 | 0xFD34899E, // NPCsUsePotions & AlchemyExpansion
31 | 0x68ED6325, // DiseaseOverhaul
32 | };
33 |
34 | inline static index_type HashIndex = 0;
35 |
36 | inline constexpr auto make_hash_key(const char* a_key)
37 | {
38 | key_type key = dku::string::join({ PROJECT_NAME, a_key }, "_");
39 |
40 | while (std::ranges::contains(KnownHash, dku::numbers::FNV_1A_32(key) + HashIndex)) {
41 | key += "_";
42 | HashIndex++;
43 | }
44 |
45 | return std::make_pair(key, dku::numbers::FNV_1A_32(key) + HashIndex);
46 | }
47 | } // namespace colliding
48 |
49 | enum class ResolveOrder : std::uint32_t
50 | {
51 | kSave = 0,
52 | kLoad,
53 | kRevert,
54 | kInternal,
55 | };
56 |
57 | struct ISerializable
58 | {
59 | struct Header
60 | {
61 | key_type name;
62 | hash_type hash{ 0 };
63 | version_type version{ 0 };
64 | std::string typeInfo;
65 | };
66 |
67 | virtual void enable() noexcept
68 | {
69 | disable();
70 | ManagedSerializables.emplace_back(this);
71 | }
72 |
73 | virtual void disable() noexcept
74 | {
75 | std::erase_if(ManagedSerializables, [this](auto* a_ptr) { return a_ptr == this; });
76 | }
77 |
78 | virtual ~ISerializable() noexcept
79 | {
80 | disable();
81 | }
82 |
83 | virtual void try_save() noexcept = 0;
84 | virtual void try_load(hash_type, version_type) noexcept = 0;
85 | virtual void try_revert() noexcept = 0;
86 |
87 | inline static std::vector ManagedSerializables = {};
88 |
89 | Header header;
90 | };
91 | } // namespace DKUtil::Serialization
92 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Extra/Serialization/variant.hpp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gottyduke/DKUtil/31d0bdf36d0c3979c346e6f20735f2cbb763d419/include/DKUtil/Impl/Extra/Serialization/variant.hpp
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Extra/xconsole.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "DKUtil/Logger.hpp"
4 | #include "DKUtil/Utility.hpp"
5 |
6 | #define CONSOLE(...) \
7 | { \
8 | if (auto* console = RE::ConsoleLog::GetSingleton()) { \
9 | auto fmt = fmt::format(__VA_ARGS__); \
10 | __INFO("[console] {}", fmt); \
11 | console->Print(fmt::format("{}->{}", PROJECT_NAME, fmt).c_str()); \
12 | } \
13 | }
14 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Extra/xserialize.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | /**
4 | *
5 | * 1.0.0
6 | * Basic serialization and deserialization;
7 | * Support (should) all types that are commonly used;
8 | * Flatten user defined types for thorough serialization;
9 | * Concept constrained auto templates;
10 | *
11 | */
12 |
13 | #include "DKUtil/Logger.hpp"
14 | #include "DKUtil/Utility.hpp"
15 |
16 | //#define DKU_X_MOCK
17 |
18 | #ifndef DKU_X_STRICT_SERIALIZATION
19 | # define DKU_X_STRICT_SERIALIZATION false
20 | #endif
21 |
22 | #include "Serialization/exception.hpp"
23 | #include "Serialization/interface.hpp"
24 | #include "Serialization/mock.hpp"
25 | #include "Serialization/resolver.hpp"
26 | #include "Serialization/shared.hpp"
27 | #include "Serialization/variant.hpp"
28 |
29 | namespace DKUtil::serialization
30 | {
31 | template
32 | struct Serializable : ISerializable
33 | {
34 | using type = std::remove_cvref_t;
35 | using resolver_func_t = std::add_pointer_t;
36 |
37 | constexpr Serializable() noexcept
38 | {
39 | auto&& [name, hash] = colliding::make_hash_key(HEADER.c);
40 |
41 | header.name = name;
42 | header.hash = hash;
43 | header.version = VERSION;
44 | header.typeInfo = typeid(type).name();
45 |
46 | ISerializable::enable();
47 | }
48 |
49 | constexpr Serializable(const type& a_data) noexcept :
50 | Serializable()
51 | {
52 | _data = a_data;
53 | }
54 |
55 | constexpr Serializable(type&& a_data) noexcept :
56 | Serializable()
57 | {
58 | _data = std::move(a_data);
59 | }
60 |
61 | constexpr Serializable& operator=(const type& a_data) noexcept
62 | {
63 | _data = a_data;
64 | return *this;
65 | }
66 |
67 | constexpr Serializable& operator=(type&& a_data) noexcept
68 | {
69 | _data = std::move(a_data);
70 | return *this;
71 | }
72 |
73 | ~Serializable() noexcept = default;
74 |
75 | constexpr auto* operator->() noexcept { return std::addressof(_data); }
76 | constexpr auto& operator*() noexcept { return _data; }
77 | constexpr auto get() noexcept { return _data; }
78 |
79 | constexpr void add_resolver(resolver_func_t a_func) noexcept
80 | {
81 | _resolvers.emplace_back(a_func);
82 | }
83 |
84 | virtual void try_save() noexcept override
85 | {
86 | __DEBUG("saving {}...", header.name);
87 |
88 | resolver::resolve_save(header, _data);
89 |
90 | for (auto& resolver : _resolvers) {
91 | resolver(_data, ResolveOrder::kSave);
92 | }
93 |
94 | __DEBUG("...done saving {}", header.name);
95 | }
96 |
97 | virtual void try_load(hash_type a_hash, version_type a_version) noexcept override
98 | {
99 | if (a_hash != header.hash) {
100 | return;
101 | }
102 |
103 | if (a_version != header.version) {
104 | // TODO: handle version variant
105 | return;
106 | }
107 |
108 | __DEBUG("loading {}...", header.name);
109 |
110 | _data = resolver::resolve_load(header, _data);
111 |
112 | for (auto& resolver : _resolvers) {
113 | resolver(_data, ResolveOrder::kLoad);
114 | }
115 |
116 | __DEBUG("...done loading {}", header.name);
117 | }
118 |
119 | virtual void try_revert() noexcept override
120 | {
121 | __DEBUG("reverting {}...", header.name);
122 |
123 | _data = type{};
124 |
125 | for (auto& resolver : _resolvers) {
126 | resolver(_data, ResolveOrder::kRevert);
127 | }
128 |
129 | __DEBUG("...done reverting {}", header.name);
130 | }
131 |
132 | private:
133 | type _data;
134 | std::vector _resolvers;
135 | };
136 | } // namespace DKUtil::serialization
137 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Hook/API.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "assembly.hpp"
4 | #include "internal.hpp"
5 | #include "shared.hpp"
6 | #include "trampoline.hpp"
7 |
8 | namespace DKUtil::Hook
9 | {
10 | using namespace model::concepts;
11 |
12 | /** \brief Apply assembly patch in the body of execution
13 | * \param a_address : Memory address of the BEGINNING of target function
14 | * \param a_offset : Offset pairs for of cave entry from the head of function
15 | * \param a_patch : Assembly patch
16 | * \param a_forward : Skip the rest of NOPs until next valid opcode
17 | * \return ASMPatchHandle
18 | */
19 | inline auto AddASMPatch(
20 | const std::uintptr_t a_address,
21 | const offset_pair a_offset,
22 | const Xbyak::CodeGenerator* a_xbyak,
23 | const bool a_forward = true) noexcept
24 | {
25 | return AddASMPatch(a_address, a_offset, std::make_pair(a_xbyak->getCode(), a_xbyak->getSize()), a_forward);
26 | }
27 |
28 | /** \brief Apply assembly patch in the body of execution
29 | * \param a_address : Memory address of the BEGINNING of target function
30 | * \param a_offset : Offset pairs for of cave entry from the head of function
31 | * \param a_patch : Assembly patch
32 | * \param a_forward : Skip the rest of NOPs until next valid opcode
33 | * \return ASMPatchHandle
34 | */
35 | inline auto AddASMPatch(
36 | const std::uintptr_t a_address,
37 | const offset_pair a_offset,
38 | const Patch* a_patch,
39 | const bool a_forward = true) noexcept
40 | {
41 | return AddASMPatch(a_address, a_offset, std::make_pair(a_patch->Data, a_patch->Size), a_forward);
42 | }
43 |
44 | /** \brief Branch to hook function in the body of execution from target function.
45 | * \param a_offset : Offset pairs for of cave entry from the head of function
46 | * \param a_address : Memory address of the BEGINNING of target function
47 | * \param a_funcInfo : FUNC_INFO or RT_INFO wrapper of hook function
48 | * \param a_prolog : Prolog patch before detouring to hook function
49 | * \param a_epilog : Epilog patch after returning from hook function
50 | * \param a_flag : Specifies operation on cave hook
51 | * \return CaveHookHandle
52 | */
53 | inline auto AddCaveHook(
54 | const std::uintptr_t a_address,
55 | const offset_pair a_offset,
56 | const FuncInfo a_funcInfo,
57 | const Xbyak::CodeGenerator* a_prolog,
58 | const Xbyak::CodeGenerator* a_epilog,
59 | model::enumeration a_flag = HookFlag::kSkipNOP) noexcept
60 | {
61 | return AddCaveHook(
62 | a_address, a_offset, a_funcInfo,
63 | a_prolog ? std::make_pair(a_prolog->getCode(), a_prolog->getSize()) : std::make_pair(nullptr, 0),
64 | a_epilog ? std::make_pair(a_epilog->getCode(), a_epilog->getSize()) : std::make_pair(nullptr, 0),
65 | a_flag);
66 | }
67 |
68 | /** \brief Branch to hook function in the body of execution from target function.
69 | * \param a_offset : Offset pairs for of cave entry from the head of function
70 | * \param a_address : Memory address of the BEGINNING of target function
71 | * \param a_funcInfo : FUNC_INFO or RT_INFO wrapper of hook function
72 | * \param a_prolog : Prolog patch before detouring to hook function
73 | * \param a_epilog : Epilog patch after returning from hook function
74 | * \param a_flag : Specifies operation on cave hook
75 | * \return CaveHookHandle
76 | */
77 | inline auto AddCaveHook(
78 | const std::uintptr_t a_address,
79 | const offset_pair a_offset,
80 | const FuncInfo a_funcInfo,
81 | const Patch* a_prolog,
82 | const Patch* a_epilog,
83 | model::enumeration a_flag = HookFlag::kSkipNOP) noexcept
84 | {
85 | return AddCaveHook(
86 | a_address, a_offset, a_funcInfo,
87 | a_prolog ? std::make_pair(a_prolog->Data, a_prolog->Size) : std::make_pair(nullptr, 0),
88 | a_epilog ? std::make_pair(a_epilog->Data, a_epilog->Size) : std::make_pair(nullptr, 0),
89 | a_flag);
90 | }
91 |
92 | inline auto AddVMTHook(
93 | void* a_vtbl,
94 | const std::uint16_t a_index,
95 | const FuncInfo a_funcInfo,
96 | const Xbyak::CodeGenerator* a_xbyak) noexcept
97 | {
98 | return AddVMTHook(a_vtbl, a_index, a_funcInfo, std::make_pair(a_xbyak->getCode(), a_xbyak->getSize()));
99 | }
100 |
101 | inline auto AddVMTHook(
102 | void* a_vtbl,
103 | const std::uint16_t a_index,
104 | const FuncInfo a_funcInfo,
105 | const Patch* a_patch) noexcept
106 | {
107 | return AddVMTHook(a_vtbl, a_index, a_funcInfo, std::make_pair(a_patch->Data, a_patch->Size));
108 | }
109 |
110 | inline auto AddIATHook(
111 | std::string_view a_moduleName,
112 | std::string_view a_libraryName,
113 | std::string_view a_importName,
114 | const FuncInfo a_funcInfo,
115 | const Xbyak::CodeGenerator* a_xbyak) noexcept
116 | {
117 | return AddIATHook(a_moduleName, a_libraryName, a_importName, a_funcInfo, std::make_pair(a_xbyak->getCode(), a_xbyak->getSize()));
118 | }
119 |
120 | inline auto AddIATHook(
121 | std::string_view a_moduleName,
122 | std::string_view a_libraryName,
123 | std::string_view a_importName,
124 | const FuncInfo a_funcInfo,
125 | const Patch* a_patch) noexcept
126 | {
127 | return AddIATHook(a_moduleName, a_libraryName, a_importName, a_funcInfo, std::make_pair(a_patch->Data, a_patch->Size));
128 | }
129 |
130 | /** \brief Relocate a jmpsite with target hook function
131 | * \brief This API exists for compatiblity reason with CLib-style invocations, hook enabled by default
132 | * \param : Length of source instruction
133 | * \param a_src : Address of jmp instruction
134 | * \param a_dst : Destination function
135 | * \return Transitive RelHookHandle that can be converted to F
136 | */
137 | template
138 | requires(dku_memory)
139 | inline auto write_branch(
140 | const std::uintptr_t a_src,
141 | F a_dst) noexcept
142 | {
143 | auto handle = AddRelHook(a_src, unrestricted_cast(a_dst));
144 | handle->Enable();
145 | return std::move(*handle.get());
146 | }
147 |
148 | /** \brief Relocate a callsite with target hook function
149 | * \brief This API exists for compatiblity reason with CLib-style invocations, hook enabled by default
150 | * \param : Length of source instruction
151 | * \param a_src : Address of call instruction
152 | * \param a_dst : Destination function
153 | * \return Transitive RelHookHandle that can be converted to F
154 | */
155 | template
156 | requires(dku_memory)
157 | inline auto write_call(
158 | const std::uintptr_t a_src,
159 | F a_dst) noexcept
160 | {
161 | auto handle = AddRelHook(a_src, unrestricted_cast(a_dst));
162 | handle->Enable();
163 | return std::move(*handle.get());
164 | }
165 |
166 | /** \brief Relocate a callsite with target hook function
167 | * \brief This API preserves regular and sse registers across non-volatile call boundaries
168 | * \param : Length of source instruction
169 | * \param a_src : Address of call instruction
170 | * \param a_dst : Destination function
171 | * \param a_regs : Regular registers to preserve as non volatile
172 | * \param a_simd : SSE registers to preserve as non volatile
173 | * \return F, the CaveHookHandle is discarded
174 | */
175 | template
176 | inline auto write_call_ex(
177 | const std::uintptr_t a_src,
178 | F a_dst,
179 | enumeration a_regs = { Register::NONE },
180 | enumeration a_simd = { SIMD::NONE }) noexcept
181 | {
182 | dku_assert(a_regs.none(Register::NONE) || a_simd.none(SIMD::NONE),
183 | "DKU_H: Cannot write_call_ex with empty flags");
184 |
185 | // get original disp call
186 | auto func = GetDisp(a_src);
187 |
188 | // transform into CaveHook
189 | auto [prolog1, epilog1] = JIT::MakeNonVolatilePatch(a_regs);
190 | auto [prolog2, epilog2] = JIT::MakeNonVolatilePatch(a_simd);
191 |
192 | // combine patches
193 | if (prolog2.Data && epilog2.Data) {
194 | prolog1.Append(prolog2);
195 | epilog2.Append(epilog1);
196 | }
197 |
198 | auto handle = AddCaveHook(
199 | a_src,
200 | { 0, N },
201 | RT_INFO(a_dst, fmt::format("write_call_ex<{}>", N)),
202 | &prolog1,
203 | &epilog2);
204 | handle->Enable();
205 |
206 | return std::bit_cast(func);
207 | }
208 | } // namespace DKUtil::Hook
209 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Hook/Internal.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "assembly.hpp"
4 | #include "jit.hpp"
5 | #include "trampoline.hpp"
6 |
7 | namespace DKUtil::Hook
8 | {
9 | using namespace Assembly;
10 |
11 | class HookHandle
12 | {
13 | public:
14 | virtual ~HookHandle() = default;
15 |
16 | virtual void Enable() noexcept = 0;
17 | virtual void Disable() noexcept = 0;
18 |
19 | template derived_t>
20 | constexpr derived_t* As() noexcept
21 | {
22 | return dynamic_cast(this);
23 | }
24 |
25 | // write directly to internal trampoline ptr
26 | template
27 | requires(!std::is_pointer_v)
28 | void Write(T a_in) noexcept
29 | {
30 | WriteData(TramPtr, std::addressof(a_in), sizeof(a_in), true);
31 | TramPtr += sizeof(a_in);
32 | }
33 |
34 | void Write(const void* a_src, std::size_t a_size) noexcept
35 | {
36 | WriteData(TramPtr, a_src, a_size, true);
37 | TramPtr += a_size;
38 | }
39 |
40 | const std::uintptr_t Address;
41 | const std::uintptr_t TramEntry;
42 | std::uintptr_t TramPtr{ 0x0 };
43 |
44 | protected:
45 | HookHandle(const std::uintptr_t a_address, const std::uintptr_t a_tramEntry) :
46 | Address(a_address), TramEntry(a_tramEntry), TramPtr(a_tramEntry)
47 | {}
48 | };
49 | } // namespace DKUtil::Hook
50 |
51 | #define DKU_H_INTERNAL_IMPORTS DKU_H_VERSION_MAJOR
52 |
53 | #include "Internal/ASMPatch.hpp"
54 | #include "Internal/CaveHook.hpp"
55 | #include "Internal/IATHook.hpp"
56 | #include "Internal/RelHook.hpp"
57 | #include "Internal/VMTHook.hpp"
58 |
59 | #undef DKU_H_INTERNAL_IMPORTS
60 |
--------------------------------------------------------------------------------
/include/DKUtil/Impl/Hook/Internal/ASMPatch.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #if !defined(DKU_H_INTERNAL_IMPORTS)
4 | # error Incorrect DKUtil::Hook internal import order.
5 | #endif
6 |
7 | namespace DKUtil::Hook
8 | {
9 | class ASMPatchHandle : public HookHandle
10 | {
11 | public:
12 | // execution address,
13 | ASMPatchHandle(
14 | const std::uintptr_t a_address,
15 | const offset_pair a_offset) noexcept :
16 | HookHandle(a_address, a_address + a_offset.first),
17 | Offset(a_offset), PatchSize(a_offset.second - a_offset.first)
18 | {
19 | OldBytes.resize(PatchSize);
20 | PatchBuf.resize(PatchSize, NOP);
21 | std::memcpy(OldBytes.data(), AsPointer(TramEntry), PatchSize);
22 |
23 | __DEBUG("DKU_H: Patch capacity: {} bytes\nPatch entry @ {:X}", PatchSize, TramEntry);
24 | }
25 |
26 | // TramEntry is the CaveEntry for asm patch
27 | void Enable() noexcept override
28 | {
29 | WriteData(TramEntry, PatchBuf.data(), PatchSize, false);
30 | __DEBUG("DKU_H: Enabled ASM patch @ {:X}", TramEntry);
31 | }
32 |
33 | void Disable() noexcept override
34 | {
35 | WriteData(TramEntry, OldBytes.data(), PatchSize, false);
36 | __DEBUG("DKU_H: Disabled ASM patch @ {:X}", TramEntry);
37 | }
38 |
39 | const offset_pair Offset;
40 | const std::size_t PatchSize;
41 | std::vector OldBytes{};
42 | std::vector PatchBuf{};
43 | };
44 |
45 | /** \brief Apply assembly patch in the body of execution
46 | * \param a_address : Memory address of the BEGINNING of target function
47 | * \param a_offset : Offset pairs for of cave entry from the head of function
48 | * \param a_patch : Assembly patch
49 | * \param a_forward : Skip the rest of NOPs until next valid opcode
50 | * \return ASMPatchHandle
51 | */
52 | [[nodiscard]] inline auto AddASMPatch(
53 | const std::uintptr_t a_address,
54 | const offset_pair a_offset,
55 | const unpacked_data a_patch = std::make_pair(nullptr, 0),
56 | const bool a_forward = true) noexcept
57 | {
58 | dku_assert(a_address && a_patch.first && a_patch.second,
59 | "DKU_H: Invalid ASM patch");
60 |
61 | auto handle = std::make_unique(a_address, a_offset);
62 |
63 | if (a_patch.second > (a_offset.second - a_offset.first)) {
64 | __DEBUG("DKU_H: ASM patch size exceeds the patch capacity, enabled trampoline");
65 | dku_assert((a_offset.second - a_offset.first) >= sizeof(JmpRel),
66 | "DKU_H: ASM patch size exceeds the patch capacity & cannot fulfill the minimal trampoline requirement");
67 |
68 | JmpRel asmDetour; // cave -> tram
69 | JmpRel asmReturn; // tram -> cave
70 |
71 | handle->TramPtr = TRAM_ALLOC(0);
72 | __DEBUG("DKU_H: ASM patch trampoline entry -> {:X}", handle->TramPtr);
73 |
74 | std::ptrdiff_t disp = handle->TramPtr - handle->TramEntry - sizeof(asmDetour);
75 | assert_trampoline_range(disp);
76 |
77 | asmDetour.Disp = static_cast(disp);
78 | AsMemCpy(handle->PatchBuf.data(), asmDetour);
79 |
80 | handle->Write(a_patch.first, a_patch.second);
81 |
82 | if (a_forward) {
83 | asmReturn.Disp = static_cast(handle->TramEntry + handle->PatchSize - handle->TramPtr - sizeof(asmReturn));
84 | } else {
85 | asmReturn.Disp = static_cast