├── .gitattributes ├── .gitignore ├── CMakeLists.txt ├── CMakePresets.json ├── LICENSE ├── README.md ├── build.bat ├── cmake ├── Plugin.h.in ├── packaging.cmake └── version.rc.in ├── src ├── CMakeLists.txt ├── PCH.h ├── bin │ ├── Hooks.cpp │ ├── Hooks.h │ ├── InputListener.cpp │ ├── InputListener.h │ ├── Renderer.cpp │ ├── Renderer.h │ ├── Utils.cpp │ ├── Utils.h │ ├── _empty.cpp │ ├── _empty.h │ ├── dMenu.cpp │ ├── dMenu.h │ ├── main.cpp │ └── menus │ │ ├── AIM.cpp │ │ ├── AIM.h │ │ ├── ModSettings.cpp │ │ ├── ModSettings.h │ │ ├── Settings.cpp │ │ ├── Settings.h │ │ ├── Trainer.cpp │ │ ├── Trainer.h │ │ ├── Translator.cpp │ │ └── Translator.h └── include │ ├── Utils.h │ └── lib │ ├── nanosvg.h │ └── nanosvgrast.h └── vcpkg.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !Update.ps1 2 | CMakeCache.txt 3 | CMakeFiles 4 | CMakeScripts 5 | Testing 6 | Makefile 7 | cmake_install.cmake 8 | install_manifest.txt 9 | compile_commands.json 10 | CTestTestfile.cmake 11 | build 12 | .clang-format 13 | .editorconfig -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.22) 2 | 3 | project( 4 | dmenu 5 | VERSION 1.0.0 6 | LANGUAGES CXX 7 | ) 8 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 9 | 10 | if("${PROJECT_SOURCE_DIR}" STREQUAL "${PROJECT_BINARY_DIR}") 11 | message(FATAL_ERROR "in-source builds are not allowed") 12 | endif() 13 | 14 | macro(set_from_environment VARIABLE) 15 | if(NOT DEFINED ${VARIABLE} AND DEFINED ENV{${VARIABLE}}) 16 | set(${VARIABLE} $ENV{${VARIABLE}}) 17 | endif() 18 | endmacro() 19 | 20 | set_from_environment(CompiledPluginsPath) 21 | if(NOT DEFINED CompiledPluginsPath) 22 | message(FATAL_ERROR "CompiledPluginsPath is not set") 23 | endif() 24 | 25 | option(COPY_OUTPUT "Copy the output of build operations to the game directory" ON) 26 | option(ENABLE_SKYRIM_SE "Enable support for Skyrim SE in the dynamic runtime feature." ON) 27 | option(ENABLE_SKYRIM_AE "Enable support for Skyrim AE in the dynamic runtime feature." ON) 28 | option(ENABLE_SKYRIM_VR "Enable support for Skyrim VR in the dynamic runtime feature." OFF) 29 | set(BUILD_TESTS OFF) 30 | 31 | list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") 32 | 33 | add_subdirectory(src) 34 | include(cmake/packaging.cmake) 35 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurePresets": [ 3 | { 4 | "binaryDir": "${sourceDir}/build", 5 | "cacheVariables": { 6 | "CMAKE_BUILD_TYPE": { 7 | "type": "STRING", 8 | "value": "Debug" 9 | } 10 | }, 11 | "errors": { 12 | "deprecated": true 13 | }, 14 | "hidden": true, 15 | "name": "cmake-dev", 16 | "warnings": { 17 | "deprecated": true, 18 | "dev": true 19 | } 20 | }, 21 | { 22 | "cacheVariables": { 23 | "CMAKE_TOOLCHAIN_FILE": { 24 | "type": "STRING", 25 | "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" 26 | } 27 | }, 28 | "hidden": true, 29 | "name": "vcpkg" 30 | }, 31 | { 32 | "cacheVariables": { 33 | "CMAKE_MSVC_RUNTIME_LIBRARY": { 34 | "type": "STRING", 35 | "value": "MultiThreaded$<$:Debug>DLL" 36 | }, 37 | "VCPKG_TARGET_TRIPLET": { 38 | "type": "STRING", 39 | "value": "x64-windows-static-md" 40 | } 41 | }, 42 | "hidden": true, 43 | "name": "windows" 44 | }, 45 | { 46 | "architecture": { 47 | "strategy": "set", 48 | "value": "x64" 49 | }, 50 | "cacheVariables": { 51 | "CMAKE_CXX_FLAGS": "/EHsc /MP /W0" 52 | }, 53 | "generator": "Visual Studio 17 2022", 54 | "inherits": [ 55 | "cmake-dev", 56 | "vcpkg", 57 | "windows" 58 | ], 59 | "name": "vs2022-windows", 60 | "toolset": "v143" 61 | } 62 | ], 63 | "version": 3 64 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 d7ry 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 | spaghetti code don't reference 2 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | cmake --preset vs2022-windows 2 | -------------------------------------------------------------------------------- /cmake/Plugin.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace Plugin 6 | { 7 | using namespace std::literals; 8 | 9 | inline constexpr REL::Version VERSION 10 | { 11 | // clang-format off 12 | @PROJECT_VERSION_MAJOR@u, 13 | @PROJECT_VERSION_MINOR@u, 14 | @PROJECT_VERSION_PATCH@u, 15 | // clang-format on 16 | }; 17 | 18 | inline constexpr auto NAME = "@PROJECT_NAME@"sv; 19 | } 20 | -------------------------------------------------------------------------------- /cmake/packaging.cmake: -------------------------------------------------------------------------------- 1 | set(CPACK_PACKAGE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/packaging") 2 | set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}") 3 | set(CPACK_VERBATIM_VARIABLES ON) 4 | 5 | set(CPACK_GENERATOR "ZIP") 6 | set(CPACK_ARCHIVE_COMPONENT_INSTALL ON) 7 | 8 | get_cmake_property(CPACK_COMPONENTS_ALL COMPONENTS) 9 | list(REMOVE_ITEM CPACK_COMPONENTS_ALL "Unspecified") 10 | 11 | include(CPack) 12 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/..") 2 | 3 | set(SOURCE_DIR "${ROOT_DIR}/src") 4 | file(GLOB_RECURSE SOURCE_FILES "${SOURCE_DIR}/*.cpp" "${SOURCE_DIR}/*.h" "${SOURCE_DIR}/*.hpp") 5 | 6 | # 对 SourceFile 变量里面的所有文件分类(保留资源管理器的目录结构) 7 | foreach(fileItem ${SOURCE_FILES}) 8 | # Get the directory of the source file 9 | get_filename_component(PARENT_DIR "${fileItem}" DIRECTORY) 10 | # Remove common directory prefix to make the group 11 | string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}" "" GROUP "${PARENT_DIR}") 12 | # Make sure we are using windows slashes 13 | string(REPLACE "/" "\\" GROUP "${GROUP}") 14 | # Group into "Source Files" and "Header Files" 15 | set(GROUP "${GROUP}") 16 | source_group("${GROUP}" FILES "${fileItem}") 17 | endforeach() 18 | 19 | source_group(TREE "${ROOT_DIR}" FILES ${SOURCE_FILES}) 20 | 21 | set(VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/src/Plugin.h") 22 | configure_file( 23 | "${ROOT_DIR}/cmake/Plugin.h.in" 24 | "${VERSION_HEADER}" 25 | @ONLY 26 | ) 27 | 28 | source_group("src" FILES "${VERSION_HEADER}") 29 | 30 | configure_file( 31 | "${ROOT_DIR}/cmake/version.rc.in" 32 | "${CMAKE_CURRENT_BINARY_DIR}/version.rc" 33 | @ONLY 34 | ) 35 | 36 | add_library( 37 | "${PROJECT_NAME}" 38 | SHARED 39 | ${SOURCE_FILES} 40 | "${VERSION_HEADER}" 41 | "${CMAKE_CURRENT_BINARY_DIR}/version.rc" 42 | "${ROOT_DIR}/.clang-format" 43 | "${ROOT_DIR}/.editorconfig" 44 | ) 45 | 46 | target_compile_features( 47 | "${PROJECT_NAME}" 48 | PRIVATE 49 | cxx_std_20 50 | ) 51 | 52 | if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") 53 | target_compile_options( 54 | "${PROJECT_NAME}" 55 | PRIVATE 56 | "/sdl" # Enable Additional Security Checks 57 | "/utf-8" # Set Source and Executable character sets to UTF-8 58 | "/Zi" # Debug Information Format 59 | 60 | "/await" 61 | 62 | "/permissive-" # Standards conformance 63 | "/Zc:preprocessor" # Enable preprocessor conformance mode 64 | 65 | "/wd4200" # nonstandard extension used : zero-sized array in struct/union 66 | 67 | "$<$:>" 68 | "$<$:/Zc:inline;/JMC-;/Ob3>" 69 | ) 70 | 71 | target_link_options( 72 | "${PROJECT_NAME}" 73 | PRIVATE 74 | "$<$:/INCREMENTAL;/OPT:NOREF;/OPT:NOICF>" 75 | "$<$:/INCREMENTAL:NO;/OPT:REF;/OPT:ICF;/DEBUG:FULL>" 76 | ) 77 | endif() 78 | 79 | target_include_directories( 80 | "${PROJECT_NAME}" 81 | PRIVATE 82 | "${CMAKE_CURRENT_BINARY_DIR}/src" 83 | "${SOURCE_DIR}" 84 | ) 85 | 86 | set(SKSE_SUPPORT_XBYAK ON) 87 | 88 | add_subdirectory("$ENV{CommonLibSSEPath_NG}" CommonLibSSE EXCLUDE_FROM_ALL) 89 | 90 | find_package(xbyak REQUIRED CONFIG) 91 | 92 | find_package(imgui REQUIRED) 93 | 94 | find_package(nlohmann_json REQUIRED) 95 | 96 | target_link_libraries( 97 | "${PROJECT_NAME}" 98 | PRIVATE 99 | CommonLibSSE::CommonLibSSE 100 | xbyak::xbyak 101 | imgui::imgui 102 | nlohmann_json::nlohmann_json 103 | ) 104 | 105 | target_precompile_headers( 106 | "${PROJECT_NAME}" 107 | PRIVATE 108 | "${SOURCE_DIR}/PCH.h" 109 | ) 110 | 111 | install( 112 | FILES 113 | "$" 114 | DESTINATION "SKSE/Plugins" 115 | COMPONENT "main" 116 | ) 117 | 118 | install( 119 | FILES 120 | "$" 121 | DESTINATION "/" 122 | COMPONENT "pdbs" 123 | ) 124 | 125 | if("${COPY_OUTPUT}") 126 | add_custom_command( 127 | TARGET "${PROJECT_NAME}" 128 | POST_BUILD 129 | COMMAND "${CMAKE_COMMAND}" -E copy_if_different "$" "${CompiledPluginsPath}/SKSE/Plugins/" 130 | COMMAND "${CMAKE_COMMAND}" -E copy_if_different "$" "${CompiledPluginsPath}/SKSE/Plugins/" 131 | VERBATIM 132 | ) 133 | endif() 134 | -------------------------------------------------------------------------------- /src/PCH.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #pragma warning(push) 5 | #include 6 | #include 7 | #include 8 | 9 | #pragma warning(disable: 4702) 10 | #include 11 | 12 | #ifdef NDEBUG 13 | # include 14 | #else 15 | # include 16 | #endif 17 | #pragma warning(pop) 18 | 19 | using namespace std::literals; 20 | 21 | namespace logger = SKSE::log; 22 | 23 | namespace util 24 | { 25 | using SKSE::stl::report_and_fail; 26 | } 27 | 28 | namespace std 29 | { 30 | template 31 | struct hash> 32 | { 33 | uint32_t operator()(const RE::BSPointerHandle& a_handle) const 34 | { 35 | uint32_t nativeHandle = const_cast*>(&a_handle)->native_handle(); // ugh 36 | return nativeHandle; 37 | } 38 | }; 39 | } 40 | 41 | #define DLLEXPORT __declspec(dllexport) 42 | 43 | #define RELOCATION_OFFSET(SE, AE) REL::VariantOffset(SE, AE, 0).offset() 44 | 45 | #include "Plugin.h" 46 | -------------------------------------------------------------------------------- /src/bin/Hooks.cpp: -------------------------------------------------------------------------------- 1 | #include "Hooks.h" 2 | #include "menus/Trainer.h" 3 | #include "Renderer.h" 4 | #include "InputListener.h" 5 | void Hooks::Install() 6 | { 7 | SKSE::AllocTrampoline(1 << 5); 8 | onWeatherChange::install(); 9 | OnInputEventDispatch::Install(); 10 | } 11 | 12 | void Hooks::onWeatherChange::updateWeather(RE::Sky* a_sky) 13 | { 14 | if (Trainer::isWeatherLocked()) { 15 | return; 16 | } 17 | _updateWeather(a_sky); 18 | } 19 | 20 | void Hooks::OnInputEventDispatch::DispatchInputEvent(RE::BSTEventSource* a_dispatcher, RE::InputEvent** a_evns) 21 | { 22 | static RE::InputEvent* dummy[] = { nullptr }; 23 | if (!a_evns) { 24 | _DispatchInputEvent(a_dispatcher, a_evns); 25 | return; 26 | } 27 | InputListener::GetSingleton()->ProcessEvent(a_evns); 28 | if (Renderer::IsEnabled()) { 29 | _DispatchInputEvent(a_dispatcher, dummy); 30 | return; 31 | } else { 32 | _DispatchInputEvent(a_dispatcher, a_evns); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/bin/Hooks.h: -------------------------------------------------------------------------------- 1 | #include "PCH.h" 2 | namespace Hooks 3 | { 4 | class onWeatherChange 5 | { 6 | public: 7 | static void install() 8 | { 9 | auto& trampoline = SKSE::GetTrampoline(); 10 | REL::Relocation caller_setWeather{ RELOCATION_ID(25682, 26229) }; //Up p Sky__Update_1403B1670+29C call Sky__UpdateWeather_1403B1C80 Up p sub_1403C8F20+3E6 call sub_1403C9870 11 | _updateWeather = trampoline.write_call<5>(caller_setWeather.address() + RELOCATION_OFFSET(0x29c, 0x3E6), updateWeather); 12 | logger::info("hook:onWeatherChange"); 13 | } 14 | 15 | private: 16 | static void updateWeather(RE::Sky* a_sky); 17 | 18 | static inline REL::Relocation _updateWeather; 19 | }; 20 | 21 | class OnInputEventDispatch 22 | { 23 | public: 24 | static void Install() 25 | { 26 | auto& trampoline = SKSE::GetTrampoline(); 27 | REL::Relocation caller{ RELOCATION_ID(67315, 68617) }; 28 | _DispatchInputEvent = trampoline.write_call<5>(caller.address() + RELOCATION_OFFSET(0x7B, 0x7B), DispatchInputEvent); 29 | } 30 | 31 | private: 32 | static void DispatchInputEvent(RE::BSTEventSource* a_dispatcher, RE::InputEvent** a_evns); 33 | static inline REL::Relocation _DispatchInputEvent; 34 | }; 35 | void Install(); 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/bin/InputListener.cpp: -------------------------------------------------------------------------------- 1 | #include "InputListener.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "Renderer.h" 10 | 11 | #include "menus/ModSettings.h" 12 | #include "menus/Settings.h" 13 | 14 | // enum : uint32_t 15 | // { 16 | // kInvalid = static_cast(-1), 17 | // kKeyboardOffset = 0, 18 | // kMouseOffset = 256, 19 | // kGamepadOffset = 266, 20 | // kMaxOffset = 282 21 | // }; 22 | 23 | // enum 24 | // { 25 | // kGamepadButtonOffset_DPAD_UP = kGamepadOffset, // 266 26 | // kGamepadButtonOffset_DPAD_DOWN, 27 | // kGamepadButtonOffset_DPAD_LEFT, 28 | // kGamepadButtonOffset_DPAD_RIGHT, 29 | // kGamepadButtonOffset_START, 30 | // kGamepadButtonOffset_BACK, 31 | // kGamepadButtonOffset_LEFT_THUMB, 32 | // kGamepadButtonOffset_RIGHT_THUMB, 33 | // kGamepadButtonOffset_LEFT_SHOULDER, 34 | // kGamepadButtonOffset_RIGHT_SHOULDER, 35 | // kGamepadButtonOffset_A, 36 | // kGamepadButtonOffset_B, 37 | // kGamepadButtonOffset_X, 38 | // kGamepadButtonOffset_Y, 39 | // kGamepadButtonOffset_LT, 40 | // kGamepadButtonOffset_RT // 281 41 | // }; 42 | 43 | #define IM_VK_KEYPAD_ENTER (VK_RETURN + 256) 44 | static ImGuiKey ImGui_ImplWin32_VirtualKeyToImGuiKey(WPARAM wParam) 45 | { 46 | switch (wParam) { 47 | case VK_TAB: 48 | return ImGuiKey_Tab; 49 | case VK_LEFT: 50 | return ImGuiKey_LeftArrow; 51 | case VK_RIGHT: 52 | return ImGuiKey_RightArrow; 53 | case VK_UP: 54 | return ImGuiKey_UpArrow; 55 | case VK_DOWN: 56 | return ImGuiKey_DownArrow; 57 | case VK_PRIOR: 58 | return ImGuiKey_PageUp; 59 | case VK_NEXT: 60 | return ImGuiKey_PageDown; 61 | case VK_HOME: 62 | return ImGuiKey_Home; 63 | case VK_END: 64 | return ImGuiKey_End; 65 | case VK_INSERT: 66 | return ImGuiKey_Insert; 67 | case VK_DELETE: 68 | return ImGuiKey_Delete; 69 | case VK_BACK: 70 | return ImGuiKey_Backspace; 71 | case VK_SPACE: 72 | return ImGuiKey_Space; 73 | case VK_RETURN: 74 | return ImGuiKey_Enter; 75 | case VK_ESCAPE: 76 | return ImGuiKey_Escape; 77 | case VK_OEM_7: 78 | return ImGuiKey_Apostrophe; 79 | case VK_OEM_COMMA: 80 | return ImGuiKey_Comma; 81 | case VK_OEM_MINUS: 82 | return ImGuiKey_Minus; 83 | case VK_OEM_PERIOD: 84 | return ImGuiKey_Period; 85 | case VK_OEM_2: 86 | return ImGuiKey_Slash; 87 | case VK_OEM_1: 88 | return ImGuiKey_Semicolon; 89 | case VK_OEM_PLUS: 90 | return ImGuiKey_Equal; 91 | case VK_OEM_4: 92 | return ImGuiKey_LeftBracket; 93 | case VK_OEM_5: 94 | return ImGuiKey_Backslash; 95 | case VK_OEM_6: 96 | return ImGuiKey_RightBracket; 97 | case VK_OEM_3: 98 | return ImGuiKey_GraveAccent; 99 | case VK_CAPITAL: 100 | return ImGuiKey_CapsLock; 101 | case VK_SCROLL: 102 | return ImGuiKey_ScrollLock; 103 | case VK_NUMLOCK: 104 | return ImGuiKey_NumLock; 105 | case VK_SNAPSHOT: 106 | return ImGuiKey_PrintScreen; 107 | case VK_PAUSE: 108 | return ImGuiKey_Pause; 109 | case VK_NUMPAD0: 110 | return ImGuiKey_Keypad0; 111 | case VK_NUMPAD1: 112 | return ImGuiKey_Keypad1; 113 | case VK_NUMPAD2: 114 | return ImGuiKey_Keypad2; 115 | case VK_NUMPAD3: 116 | return ImGuiKey_Keypad3; 117 | case VK_NUMPAD4: 118 | return ImGuiKey_Keypad4; 119 | case VK_NUMPAD5: 120 | return ImGuiKey_Keypad5; 121 | case VK_NUMPAD6: 122 | return ImGuiKey_Keypad6; 123 | case VK_NUMPAD7: 124 | return ImGuiKey_Keypad7; 125 | case VK_NUMPAD8: 126 | return ImGuiKey_Keypad8; 127 | case VK_NUMPAD9: 128 | return ImGuiKey_Keypad9; 129 | case VK_DECIMAL: 130 | return ImGuiKey_KeypadDecimal; 131 | case VK_DIVIDE: 132 | return ImGuiKey_KeypadDivide; 133 | case VK_MULTIPLY: 134 | return ImGuiKey_KeypadMultiply; 135 | case VK_SUBTRACT: 136 | return ImGuiKey_KeypadSubtract; 137 | case VK_ADD: 138 | return ImGuiKey_KeypadAdd; 139 | case IM_VK_KEYPAD_ENTER: 140 | return ImGuiKey_KeypadEnter; 141 | case VK_LSHIFT: 142 | return ImGuiKey_LeftShift; 143 | case VK_LCONTROL: 144 | return ImGuiKey_LeftCtrl; 145 | case VK_LMENU: 146 | return ImGuiKey_LeftAlt; 147 | case VK_LWIN: 148 | return ImGuiKey_LeftSuper; 149 | case VK_RSHIFT: 150 | return ImGuiKey_RightShift; 151 | case VK_RCONTROL: 152 | return ImGuiKey_RightCtrl; 153 | case VK_RMENU: 154 | return ImGuiKey_RightAlt; 155 | case VK_RWIN: 156 | return ImGuiKey_RightSuper; 157 | case VK_APPS: 158 | return ImGuiKey_Menu; 159 | case '0': 160 | return ImGuiKey_0; 161 | case '1': 162 | return ImGuiKey_1; 163 | case '2': 164 | return ImGuiKey_2; 165 | case '3': 166 | return ImGuiKey_3; 167 | case '4': 168 | return ImGuiKey_4; 169 | case '5': 170 | return ImGuiKey_5; 171 | case '6': 172 | return ImGuiKey_6; 173 | case '7': 174 | return ImGuiKey_7; 175 | case '8': 176 | return ImGuiKey_8; 177 | case '9': 178 | return ImGuiKey_9; 179 | case 'A': 180 | return ImGuiKey_A; 181 | case 'B': 182 | return ImGuiKey_B; 183 | case 'C': 184 | return ImGuiKey_C; 185 | case 'D': 186 | return ImGuiKey_D; 187 | case 'E': 188 | return ImGuiKey_E; 189 | case 'F': 190 | return ImGuiKey_F; 191 | case 'G': 192 | return ImGuiKey_G; 193 | case 'H': 194 | return ImGuiKey_H; 195 | case 'I': 196 | return ImGuiKey_I; 197 | case 'J': 198 | return ImGuiKey_J; 199 | case 'K': 200 | return ImGuiKey_K; 201 | case 'L': 202 | return ImGuiKey_L; 203 | case 'M': 204 | return ImGuiKey_M; 205 | case 'N': 206 | return ImGuiKey_N; 207 | case 'O': 208 | return ImGuiKey_O; 209 | case 'P': 210 | return ImGuiKey_P; 211 | case 'Q': 212 | return ImGuiKey_Q; 213 | case 'R': 214 | return ImGuiKey_R; 215 | case 'S': 216 | return ImGuiKey_S; 217 | case 'T': 218 | return ImGuiKey_T; 219 | case 'U': 220 | return ImGuiKey_U; 221 | case 'V': 222 | return ImGuiKey_V; 223 | case 'W': 224 | return ImGuiKey_W; 225 | case 'X': 226 | return ImGuiKey_X; 227 | case 'Y': 228 | return ImGuiKey_Y; 229 | case 'Z': 230 | return ImGuiKey_Z; 231 | case VK_F1: 232 | return ImGuiKey_F1; 233 | case VK_F2: 234 | return ImGuiKey_F2; 235 | case VK_F3: 236 | return ImGuiKey_F3; 237 | case VK_F4: 238 | return ImGuiKey_F4; 239 | case VK_F5: 240 | return ImGuiKey_F5; 241 | case VK_F6: 242 | return ImGuiKey_F6; 243 | case VK_F7: 244 | return ImGuiKey_F7; 245 | case VK_F8: 246 | return ImGuiKey_F8; 247 | case VK_F9: 248 | return ImGuiKey_F9; 249 | case VK_F10: 250 | return ImGuiKey_F10; 251 | case VK_F11: 252 | return ImGuiKey_F11; 253 | case VK_F12: 254 | return ImGuiKey_F12; 255 | default: 256 | return ImGuiKey_None; 257 | } 258 | } 259 | 260 | class CharEvent : public RE::InputEvent 261 | { 262 | public: 263 | uint32_t keyCode; // 18 (ascii code) 264 | }; 265 | 266 | 267 | static enum : std::uint32_t 268 | { 269 | kInvalid = static_cast(-1), 270 | kKeyboardOffset = 0, 271 | kMouseOffset = 256, 272 | kGamepadOffset = 266 273 | }; 274 | 275 | static inline std::uint32_t GetGamepadIndex(RE::BSWin32GamepadDevice::Key a_key) 276 | { 277 | using Key = RE::BSWin32GamepadDevice::Key; 278 | 279 | std::uint32_t index; 280 | switch (a_key) { 281 | case Key::kUp: 282 | index = 0; 283 | break; 284 | case Key::kDown: 285 | index = 1; 286 | break; 287 | case Key::kLeft: 288 | index = 2; 289 | break; 290 | case Key::kRight: 291 | index = 3; 292 | break; 293 | case Key::kStart: 294 | index = 4; 295 | break; 296 | case Key::kBack: 297 | index = 5; 298 | break; 299 | case Key::kLeftThumb: 300 | index = 6; 301 | break; 302 | case Key::kRightThumb: 303 | index = 7; 304 | break; 305 | case Key::kLeftShoulder: 306 | index = 8; 307 | break; 308 | case Key::kRightShoulder: 309 | index = 9; 310 | break; 311 | case Key::kA: 312 | index = 10; 313 | break; 314 | case Key::kB: 315 | index = 11; 316 | break; 317 | case Key::kX: 318 | index = 12; 319 | break; 320 | case Key::kY: 321 | index = 13; 322 | break; 323 | case Key::kLeftTrigger: 324 | index = 14; 325 | break; 326 | case Key::kRightTrigger: 327 | index = 15; 328 | break; 329 | default: 330 | index = kInvalid; 331 | break; 332 | } 333 | 334 | return index != kInvalid ? index + kGamepadOffset : kInvalid; 335 | } 336 | void InputListener::ProcessEvent(RE::InputEvent** a_event) 337 | { 338 | if (!a_event) 339 | return; 340 | 341 | auto& io = ImGui::GetIO(); 342 | 343 | for (auto event = *a_event; event; event = event->next) { 344 | if (event->eventType == RE::INPUT_EVENT_TYPE::kChar) { 345 | io.AddInputCharacter(static_cast(event)->keyCode); 346 | } else if (event->eventType == RE::INPUT_EVENT_TYPE::kButton) { 347 | const auto button = static_cast(event); 348 | if (!button || (button->IsPressed() && !button->IsDown())) 349 | continue; 350 | 351 | auto scan_code = button->GetIDCode(); 352 | 353 | using DeviceType = RE::INPUT_DEVICE; 354 | auto input = scan_code; 355 | switch (button->device.get()) { 356 | case DeviceType::kMouse: 357 | input += kMouseOffset; 358 | break; 359 | case DeviceType::kKeyboard: 360 | input += kKeyboardOffset; 361 | break; 362 | case DeviceType::kGamepad: 363 | input = GetGamepadIndex((RE::BSWin32GamepadDevice::Key)input); 364 | break; 365 | default: 366 | continue; 367 | } 368 | ModSettings::submitInput(input); 369 | 370 | 371 | uint32_t key = MapVirtualKeyEx(scan_code, MAPVK_VSC_TO_VK_EX, GetKeyboardLayout(0)); 372 | switch (scan_code) { 373 | case DIK_LEFTARROW: 374 | key = VK_LEFT; 375 | break; 376 | case DIK_RIGHTARROW: 377 | key = VK_RIGHT; 378 | break; 379 | case DIK_UPARROW: 380 | key = VK_UP; 381 | break; 382 | case DIK_DOWNARROW: 383 | key = VK_DOWN; 384 | break; 385 | case DIK_DELETE: 386 | key = VK_DELETE; 387 | break; 388 | case DIK_END: 389 | key = VK_END; 390 | break; 391 | case DIK_HOME: 392 | key = VK_HOME; 393 | break; // pos1 394 | case DIK_PRIOR: 395 | key = VK_PRIOR; 396 | break; // page up 397 | case DIK_NEXT: 398 | key = VK_NEXT; 399 | break; // page down 400 | case DIK_INSERT: 401 | key = VK_INSERT; 402 | break; 403 | case DIK_NUMPAD0: 404 | key = VK_NUMPAD0; 405 | break; 406 | case DIK_NUMPAD1: 407 | key = VK_NUMPAD1; 408 | break; 409 | case DIK_NUMPAD2: 410 | key = VK_NUMPAD2; 411 | break; 412 | case DIK_NUMPAD3: 413 | key = VK_NUMPAD3; 414 | break; 415 | case DIK_NUMPAD4: 416 | key = VK_NUMPAD4; 417 | break; 418 | case DIK_NUMPAD5: 419 | key = VK_NUMPAD5; 420 | break; 421 | case DIK_NUMPAD6: 422 | key = VK_NUMPAD6; 423 | break; 424 | case DIK_NUMPAD7: 425 | key = VK_NUMPAD7; 426 | break; 427 | case DIK_NUMPAD8: 428 | key = VK_NUMPAD8; 429 | break; 430 | case DIK_NUMPAD9: 431 | key = VK_NUMPAD9; 432 | break; 433 | case DIK_DECIMAL: 434 | key = VK_DECIMAL; 435 | break; 436 | case DIK_NUMPADENTER: 437 | key = IM_VK_KEYPAD_ENTER; 438 | break; 439 | case DIK_RMENU: 440 | key = VK_RMENU; 441 | break; // right alt 442 | case DIK_RCONTROL: 443 | key = VK_RCONTROL; 444 | break; // right control 445 | case DIK_LWIN: 446 | key = VK_LWIN; 447 | break; // left win 448 | case DIK_RWIN: 449 | key = VK_RWIN; 450 | break; // right win 451 | case DIK_APPS: 452 | key = VK_APPS; 453 | break; 454 | default: 455 | break; 456 | } 457 | 458 | switch (button->device.get()) { 459 | case RE::INPUT_DEVICE::kMouse: 460 | if (scan_code > 7) // middle scroll 461 | io.AddMouseWheelEvent(0, button->Value() * (scan_code == 8 ? 1 : -1)); 462 | else { 463 | if (scan_code > 5) 464 | scan_code = 5; 465 | io.AddMouseButtonEvent(scan_code, button->IsPressed()); 466 | } 467 | break; 468 | case RE::INPUT_DEVICE::kKeyboard: 469 | io.AddKeyEvent(ImGui_ImplWin32_VirtualKeyToImGuiKey(key), button->IsPressed()); 470 | if (button->GetIDCode() == Settings::key_toggle_dmenu) { // home 471 | if (button->IsDown()) { 472 | Renderer::flip(); 473 | } 474 | } 475 | break; 476 | case RE::INPUT_DEVICE::kGamepad: 477 | // not implemented yet 478 | // key = GetGamepadIndex((RE::BSWin32GamepadDevice::Key)key); 479 | break; 480 | default: 481 | continue; 482 | } 483 | } 484 | } 485 | return; 486 | } 487 | -------------------------------------------------------------------------------- /src/bin/InputListener.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "PCH.h" 3 | 4 | class InputListener 5 | { 6 | public: 7 | static InputListener* GetSingleton() 8 | { 9 | static InputListener listener; 10 | return std::addressof(listener); 11 | } 12 | 13 | void ProcessEvent(RE::InputEvent** a_event); 14 | }; 15 | -------------------------------------------------------------------------------- /src/bin/Renderer.cpp: -------------------------------------------------------------------------------- 1 | #include "Renderer.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include "imgui_internal.h" 10 | 11 | #include "dMenu.h" 12 | 13 | #include "Utils.h" 14 | #include "menus/Settings.h" 15 | 16 | #include "menus/Translator.h" 17 | // stole this from MaxSu's detection meter 18 | 19 | namespace stl 20 | { 21 | using namespace SKSE::stl; 22 | 23 | template 24 | void write_thunk_call() 25 | { 26 | auto& trampoline = SKSE::GetTrampoline(); 27 | const REL::Relocation hook{ T::id, T::offset }; 28 | T::func = trampoline.write_call<5>(hook.address(), T::thunk); 29 | } 30 | } 31 | 32 | 33 | LRESULT Renderer::WndProcHook::thunk(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 34 | { 35 | auto& io = ImGui::GetIO(); 36 | if (uMsg == WM_KILLFOCUS) { 37 | io.ClearInputCharacters(); 38 | io.ClearInputKeys(); 39 | } 40 | 41 | return func(hWnd, uMsg, wParam, lParam); 42 | } 43 | 44 | 45 | void SetupImGuiStyle() 46 | { 47 | auto& style = ImGui::GetStyle(); 48 | auto& colors = style.Colors; 49 | 50 | // Theme from https://github.com/ArranzCNL/ImprovedCameraSE-NG 51 | 52 | //style.WindowTitleAlign = ImVec2(0.5, 0.5); 53 | //style.FramePadding = ImVec2(4, 4); 54 | 55 | // Rounded slider grabber 56 | style.GrabRounding = 12.0f; 57 | 58 | // Window 59 | colors[ImGuiCol_WindowBg] = ImVec4{ 0.118f, 0.118f, 0.118f, 0.784f }; 60 | colors[ImGuiCol_ResizeGrip] = ImVec4{ 0.2f, 0.2f, 0.2f, 0.5f }; 61 | colors[ImGuiCol_ResizeGripHovered] = ImVec4{ 0.3f, 0.3f, 0.3f, 0.75f }; 62 | colors[ImGuiCol_ResizeGripActive] = ImVec4{ 0.15f, 0.15f, 0.15f, 1.0f }; 63 | 64 | // Header 65 | colors[ImGuiCol_Header] = ImVec4{ 0.2f, 0.2f, 0.2f, 1.0f }; 66 | colors[ImGuiCol_HeaderHovered] = ImVec4{ 0.3f, 0.3f, 0.3f, 1.0f }; 67 | colors[ImGuiCol_HeaderActive] = ImVec4{ 0.15f, 0.15f, 0.15f, 1.0f }; 68 | 69 | // Title 70 | colors[ImGuiCol_TitleBg] = ImVec4{ 0.15f, 0.15f, 0.15f, 1.0f }; 71 | colors[ImGuiCol_TitleBgActive] = ImVec4{ 0.15f, 0.15f, 0.15f, 1.0f }; 72 | colors[ImGuiCol_TitleBgCollapsed] = ImVec4{ 0.15f, 0.15f, 0.15f, 1.0f }; 73 | 74 | // Frame Background 75 | colors[ImGuiCol_FrameBg] = ImVec4{ 0.2f, 0.2f, 0.2f, 1.0f }; 76 | colors[ImGuiCol_FrameBgHovered] = ImVec4{ 0.3f, 0.3f, 0.3f, 1.0f }; 77 | colors[ImGuiCol_FrameBgActive] = ImVec4{ 0.15f, 0.15f, 0.15f, 1.0f }; 78 | 79 | // Button 80 | colors[ImGuiCol_Button] = ImVec4{ 0.2f, 0.2f, 0.2f, 1.0f }; 81 | colors[ImGuiCol_ButtonHovered] = ImVec4{ 0.3f, 0.3f, 0.3f, 1.0f }; 82 | colors[ImGuiCol_ButtonActive] = ImVec4{ 0.15f, 0.15f, 0.15f, 1.0f }; 83 | 84 | // Tab 85 | colors[ImGuiCol_Tab] = ImVec4{ 0.15f, 0.15f, 0.15f, 1.0f }; 86 | colors[ImGuiCol_TabHovered] = ImVec4{ 0.38f, 0.38f, 0.38f, 1.0f }; 87 | colors[ImGuiCol_TabActive] = ImVec4{ 0.28f, 0.28f, 0.28f, 1.0f }; 88 | colors[ImGuiCol_TabUnfocused] = ImVec4{ 0.15f, 0.15f, 0.15f, 1.0f }; 89 | colors[ImGuiCol_TabUnfocusedActive] = ImVec4{ 0.2f, 0.2f, 0.2f, 1.0f }; 90 | 91 | } 92 | 93 | void Renderer::D3DInitHook::thunk() 94 | { 95 | func(); 96 | 97 | INFO("D3DInit Hooked!"); 98 | auto render_manager = RE::BSRenderManager::GetSingleton(); 99 | if (!render_manager) { 100 | ERROR("Cannot find render manager. Initialization failed!"); 101 | return; 102 | } 103 | 104 | auto render_data = render_manager->GetRuntimeData(); 105 | 106 | INFO("Getting swapchain..."); 107 | auto swapchain = render_data.swapChain; 108 | if (!swapchain) { 109 | ERROR("Cannot find swapchain. Initialization failed!"); 110 | return; 111 | } 112 | 113 | INFO("Getting swapchain desc..."); 114 | DXGI_SWAP_CHAIN_DESC sd{}; 115 | if (swapchain->GetDesc(std::addressof(sd)) < 0) { 116 | ERROR("IDXGISwapChain::GetDesc failed."); 117 | return; 118 | } 119 | 120 | device = render_data.forwarder; 121 | context = render_data.context; 122 | 123 | INFO("Initializing ImGui..."); 124 | ImGui::CreateContext(); 125 | if (!ImGui_ImplWin32_Init(sd.OutputWindow)) { 126 | ERROR("ImGui initialization failed (Win32)"); 127 | return; 128 | } 129 | if (!ImGui_ImplDX11_Init(device, context)) { 130 | ERROR("ImGui initialization failed (DX11)"); 131 | return; 132 | } 133 | 134 | INFO("ImGui initialized!"); 135 | 136 | initialized.store(true); 137 | 138 | WndProcHook::func = reinterpret_cast( 139 | SetWindowLongPtrA( 140 | sd.OutputWindow, 141 | GWLP_WNDPROC, 142 | reinterpret_cast(WndProcHook::thunk))); 143 | if (!WndProcHook::func) 144 | ERROR("SetWindowLongPtrA failed!"); 145 | 146 | // initialize font selection here 147 | INFO("Building font atlas..."); 148 | std::filesystem::path fontPath; 149 | bool foundCustomFont = false; 150 | const ImWchar* glyphRanges = 0; 151 | #define FONTSETTING_PATH "Data\\SKSE\\Plugins\\dMenu\\fonts\\FontConfig.ini" 152 | CSimpleIniA ini; 153 | ini.LoadFile(FONTSETTING_PATH); 154 | if (!ini.IsEmpty()) { 155 | const char* language = ini.GetValue("config", "font", 0); 156 | if (language) { 157 | std::string fontDir = R"(Data\SKSE\Plugins\dMenu\fonts\)" + std::string(language); 158 | // check if folder exists 159 | if (std::filesystem::exists(fontDir) && std::filesystem::is_directory(fontDir)) { 160 | for (const auto& entry : std::filesystem::directory_iterator(fontDir)) { 161 | auto entryPath = entry.path(); 162 | if (entryPath.extension() == ".ttf" || entryPath.extension() == ".ttc") { 163 | fontPath = entryPath; 164 | foundCustomFont = true; 165 | break; 166 | } 167 | } 168 | } 169 | if (foundCustomFont) { 170 | std::string languageStr = language; 171 | INFO("Loading font: {}", fontPath.string().c_str()); 172 | if (languageStr == "Chinese") { 173 | INFO("Glyph range set to Chinese"); 174 | glyphRanges = ImGui::GetIO().Fonts->GetGlyphRangesChineseFull(); 175 | } else if (languageStr == "Korean") { 176 | INFO("Glyph range set to Korean"); 177 | glyphRanges = ImGui::GetIO().Fonts->GetGlyphRangesKorean(); 178 | } else if (languageStr == "Japanese") { 179 | INFO("Glyph range set to Japanese"); 180 | glyphRanges = ImGui::GetIO().Fonts->GetGlyphRangesJapanese(); 181 | } else if (languageStr == "Thai") { 182 | INFO("Glyph range set to Thai"); 183 | glyphRanges = ImGui::GetIO().Fonts->GetGlyphRangesThai(); 184 | } else if (languageStr == "Vietnamese") { 185 | INFO("Glyph range set to Vietnamese"); 186 | glyphRanges = ImGui::GetIO().Fonts->GetGlyphRangesVietnamese(); 187 | } else if (languageStr == "Cyrillic") { 188 | glyphRanges = ImGui::GetIO().Fonts->GetGlyphRangesCyrillic(); 189 | INFO("Glyph range set to Cyrillic"); 190 | } 191 | } else { 192 | INFO("No font found for language: {}", language); 193 | } 194 | } 195 | } 196 | #define ENABLE_FREETYPE 0 197 | #if ENABLE_FREETYPE 198 | ImFontAtlas* atlas = ImGui::GetIO().Fonts; 199 | atlas->FontBuilderIO = ImGuiFreeType::GetBuilderForFreeType(); 200 | atlas->FontBuilderFlags = ImGuiFreeTypeBuilderFlags_LightHinting; 201 | #else 202 | #endif 203 | if (foundCustomFont) { 204 | ImGui::GetIO().Fonts->AddFontFromFileTTF(fontPath.string().c_str(), 32.0f, NULL, glyphRanges); 205 | } 206 | SetupImGuiStyle(); 207 | 208 | } 209 | 210 | void Renderer::DXGIPresentHook::thunk(std::uint32_t a_p1) 211 | { 212 | func(a_p1); 213 | 214 | if (!D3DInitHook::initialized.load()) 215 | return; 216 | 217 | // prologue 218 | ImGui_ImplDX11_NewFrame(); 219 | ImGui_ImplWin32_NewFrame(); 220 | ImGui::NewFrame(); 221 | 222 | // do stuff 223 | Renderer::draw(); 224 | 225 | // epilogue 226 | ImGui::EndFrame(); 227 | ImGui::Render(); 228 | ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); 229 | } 230 | 231 | struct ImageSet 232 | { 233 | std::int32_t my_image_width = 0; 234 | std::int32_t my_image_height = 0; 235 | ID3D11ShaderResourceView* my_texture = nullptr; 236 | }; 237 | 238 | 239 | void Renderer::MessageCallback(SKSE::MessagingInterface::Message* msg) //CallBack & LoadTextureFromFile should called after resource loaded. 240 | { 241 | if (msg->type == SKSE::MessagingInterface::kDataLoaded && D3DInitHook::initialized) { 242 | // Read Texture only after game engine finished load all it renderer resource. 243 | auto& io = ImGui::GetIO(); 244 | io.MouseDrawCursor = true; 245 | io.WantSetMousePos = true; 246 | } 247 | } 248 | 249 | bool Renderer::Install() 250 | { 251 | auto g_message = SKSE::GetMessagingInterface(); 252 | if (!g_message) { 253 | ERROR("Messaging Interface Not Found!"); 254 | return false; 255 | } 256 | 257 | g_message->RegisterListener(MessageCallback); 258 | 259 | SKSE::AllocTrampoline(14 * 2); 260 | 261 | stl::write_thunk_call(); 262 | stl::write_thunk_call(); 263 | 264 | 265 | return true; 266 | } 267 | 268 | void Renderer::flip() 269 | { 270 | enable = !enable; 271 | ImGui::GetIO().MouseDrawCursor = enable; 272 | } 273 | 274 | 275 | float Renderer::GetResolutionScaleWidth() 276 | { 277 | return ImGui::GetIO().DisplaySize.x / 1920.f; 278 | } 279 | 280 | float Renderer::GetResolutionScaleHeight() 281 | { 282 | return ImGui::GetIO().DisplaySize.y / 1080.f; 283 | } 284 | 285 | 286 | void Renderer::draw() 287 | { 288 | //static constexpr ImGuiWindowFlags windowFlag = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; 289 | 290 | 291 | // resize window 292 | //ImGui::SetNextWindowPos(ImVec2(0, 0)); 293 | //ImGui::SetNextWindowSize(ImVec2(screenSizeX, screenSizeY)); 294 | 295 | 296 | // Add UI elements here 297 | //ImGui::Text("sizeX: %f, sizeYL %f", screenSizeX, screenSizeY); 298 | 299 | 300 | 301 | if (enable) { 302 | if (!DMenu::initialized) { 303 | ImVec2 screenSize = ImGui::GetMainViewport()->Size; 304 | float screenSizeX = screenSize.x; 305 | float screenSizeY = screenSize.y; 306 | DMenu::init(screenSizeX, screenSizeY); 307 | } 308 | 309 | DMenu::draw(); 310 | } 311 | 312 | } 313 | -------------------------------------------------------------------------------- /src/bin/Renderer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | class Renderer 5 | { 6 | struct WndProcHook 7 | { 8 | static LRESULT thunk(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); 9 | static inline WNDPROC func; 10 | }; 11 | 12 | struct D3DInitHook 13 | { 14 | static void thunk(); 15 | static inline REL::Relocation func; 16 | 17 | static constexpr auto id = REL::RelocationID(75595, 77226); 18 | static constexpr auto offset = REL::VariantOffset(0x9, 0x275, 0x00); // VR unknown 19 | 20 | static inline std::atomic initialized = false; 21 | }; 22 | 23 | struct DXGIPresentHook 24 | { 25 | static void thunk(std::uint32_t a_p1); 26 | static inline REL::Relocation func; 27 | 28 | static constexpr auto id = REL::RelocationID(75461, 77246); 29 | static constexpr auto offset = REL::Offset(0x9); 30 | }; 31 | 32 | 33 | private: 34 | Renderer() = delete; 35 | 36 | static void draw(); //Rendering Meters. 37 | static void MessageCallback(SKSE::MessagingInterface::Message* msg); 38 | 39 | static inline bool ShowMeters = false; 40 | static inline ID3D11Device* device = nullptr; 41 | static inline ID3D11DeviceContext* context = nullptr; 42 | 43 | static inline bool enable = false; 44 | 45 | 46 | public: 47 | static bool Install(); 48 | 49 | static void flip(); 50 | 51 | static bool IsEnabled() { return enable; } 52 | 53 | static float GetResolutionScaleWidth(); // { return ImGui::GetIO().DisplaySize.x / 1920.f; } 54 | static float GetResolutionScaleHeight(); //{ return ImGui::GetIO().DisplaySize.y / 1080.f; } 55 | 56 | }; 57 | -------------------------------------------------------------------------------- /src/bin/Utils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "imgui_internal.h" 4 | #include "imgui_stdlib.h" 5 | 6 | #include "imgui.h" 7 | #include "Utils.h" 8 | namespace Utils 9 | { 10 | namespace imgui 11 | { 12 | 13 | 14 | void HoverNote(const char* text, const char* note) 15 | { 16 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); 17 | ImGui::Text(note); 18 | ImGui::PopStyleColor(); 19 | if (ImGui::IsItemHovered()) { 20 | ImGui::BeginTooltip(); 21 | ImGui::Text(text); 22 | ImGui::EndTooltip(); 23 | } 24 | } 25 | 26 | } 27 | 28 | std::string getFormEditorID(const RE::TESForm* a_form) 29 | { 30 | switch (a_form->GetFormType()) { 31 | case RE::FormType::Keyword: 32 | case RE::FormType::LocationRefType: 33 | case RE::FormType::Action: 34 | case RE::FormType::MenuIcon: 35 | case RE::FormType::Global: 36 | case RE::FormType::HeadPart: 37 | case RE::FormType::Race: 38 | case RE::FormType::Sound: 39 | case RE::FormType::Script: 40 | case RE::FormType::Navigation: 41 | case RE::FormType::Cell: 42 | case RE::FormType::WorldSpace: 43 | case RE::FormType::Land: 44 | case RE::FormType::NavMesh: 45 | case RE::FormType::Dialogue: 46 | case RE::FormType::Quest: 47 | case RE::FormType::Idle: 48 | case RE::FormType::AnimatedObject: 49 | case RE::FormType::ImageAdapter: 50 | case RE::FormType::VoiceType: 51 | case RE::FormType::Ragdoll: 52 | case RE::FormType::DefaultObject: 53 | case RE::FormType::MusicType: 54 | case RE::FormType::StoryManagerBranchNode: 55 | case RE::FormType::StoryManagerQuestNode: 56 | case RE::FormType::StoryManagerEventNode: 57 | case RE::FormType::SoundRecord: 58 | return a_form->GetFormEditorID(); 59 | default: 60 | { 61 | static auto tweaks = GetModuleHandle("po3_Tweaks"); 62 | static auto func = reinterpret_cast<_GetFormEditorID>(GetProcAddress(tweaks, "GetFormEditorID")); 63 | if (func) { 64 | return func(a_form->formID); 65 | } 66 | return {}; 67 | } 68 | } 69 | } 70 | 71 | 72 | } 73 | 74 | settingsLoader::settingsLoader(const char* settingsFile) 75 | { 76 | _ini.LoadFile(settingsFile); 77 | if (_ini.IsEmpty()) { 78 | logger::info("Warning: {} is empty.", settingsFile); 79 | } 80 | _settingsFile = settingsFile; 81 | } 82 | 83 | settingsLoader::~settingsLoader() 84 | { 85 | log(); 86 | } 87 | 88 | 89 | /*Set the active section. Load() will load keys from this section.*/ 90 | 91 | void settingsLoader::setActiveSection(const char* section) 92 | { 93 | _section = section; 94 | } 95 | 96 | /*Load a boolean value if present.*/ 97 | 98 | void settingsLoader::load(bool& settingRef, const char* key) 99 | { 100 | if (_ini.GetValue(_section, key)) { 101 | bool val = _ini.GetBoolValue(_section, key); 102 | settingRef = val; 103 | _loadedSettings++; 104 | } 105 | } 106 | 107 | /*Load a float value if present.*/ 108 | 109 | void settingsLoader::load(float& settingRef, const char* key) 110 | { 111 | if (_ini.GetValue(_section, key)) { 112 | float val = static_cast(_ini.GetDoubleValue(_section, key)); 113 | settingRef = val; 114 | _loadedSettings++; 115 | } 116 | } 117 | 118 | /*Load an unsigned int value if present.*/ 119 | 120 | void settingsLoader::load(uint32_t& settingRef, const char* key) 121 | { 122 | if (_ini.GetValue(_section, key)) { 123 | uint32_t val = static_cast(_ini.GetDoubleValue(_section, key)); 124 | settingRef = val; 125 | _loadedSettings++; 126 | } 127 | } 128 | 129 | void settingsLoader::save(bool& settingRef, const char* key) 130 | { 131 | if (_ini.GetValue(_section, key)) { 132 | if (settingRef) { 133 | _ini.SetValue(_section, key, "true"); 134 | } else { 135 | _ini.SetValue(_section, key, "false"); 136 | } 137 | _savedSettings++; 138 | } 139 | } 140 | 141 | void settingsLoader::save(float& settingRef, const char* key) 142 | { 143 | if (_ini.GetValue(_section, key)) { 144 | _ini.SetValue(_section, key, std::to_string(settingRef).data()); 145 | _savedSettings++; 146 | } 147 | } 148 | 149 | void settingsLoader::save(uint32_t& settingRef, const char* key) 150 | { 151 | if (_ini.GetValue(_section, key)) { 152 | _ini.SetValue(_section, key, std::to_string(settingRef).data()); 153 | _savedSettings++; 154 | } 155 | } 156 | 157 | /*Load an integer value if present.*/ 158 | 159 | void settingsLoader::load(int& settingRef, const char* key) 160 | { 161 | if (_ini.GetValue(_section, key)) { 162 | int val = static_cast(_ini.GetDoubleValue(_section, key)); 163 | settingRef = val; 164 | _loadedSettings++; 165 | } 166 | } 167 | 168 | void settingsLoader::flush() 169 | { 170 | _ini.SaveFile(_settingsFile); 171 | } 172 | 173 | namespace ImGui 174 | { 175 | bool SliderFloatWithSteps(const char* label, float* v, float v_min, float v_max, float v_step) 176 | { 177 | char text_buf[64] = {}; 178 | ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), "%g", *v); 179 | 180 | // Map from [v_min,v_max] to [0,N] 181 | const int countValues = int((v_max - v_min) / v_step); 182 | int v_i = int((*v - v_min) / v_step); 183 | const bool value_changed = SliderInt(label, &v_i, 0, countValues, text_buf); 184 | 185 | // Remap from [0,N] to [v_min,v_max] 186 | *v = v_min + float(v_i) * v_step; 187 | return value_changed; 188 | } 189 | 190 | void HoverNote(const char* text, const char* note) 191 | { 192 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); 193 | ImGui::Text(note); 194 | ImGui::PopStyleColor(); 195 | if (ImGui::IsItemHovered()) { 196 | ImGui::BeginTooltip(); 197 | ImGui::Text(text); 198 | ImGui::EndTooltip(); 199 | } 200 | } 201 | 202 | 203 | bool ToggleButton(const char* str_id, bool* v) 204 | { 205 | bool ret = false; 206 | ImVec2 p = ImGui::GetCursorScreenPos(); 207 | ImDrawList* draw_list = ImGui::GetWindowDrawList(); 208 | 209 | float height = ImGui::GetFrameHeight(); 210 | float width = height * 1.55f; 211 | float radius = height * 0.50f; 212 | 213 | ImGui::InvisibleButton(str_id, ImVec2(width, height)); 214 | if (ImGui::IsItemClicked()) { 215 | *v = !*v; 216 | ret = true; 217 | } 218 | 219 | float t = *v ? 1.0f : 0.0f; 220 | 221 | ImGuiContext& g = *GImGui; 222 | float ANIM_SPEED = 0.08f; 223 | if (g.LastActiveId == g.CurrentWindow->GetID(str_id)) // && g.LastActiveIdTimer < ANIM_SPEED) 224 | { 225 | float t_anim = ImSaturate(g.LastActiveIdTimer / ANIM_SPEED); 226 | t = *v ? (t_anim) : (1.0f - t_anim); 227 | } 228 | 229 | ImU32 col_bg; 230 | if (ImGui::IsItemHovered()) 231 | col_bg = ImGui::GetColorU32(ImLerp(ImVec4(0.78f, 0.78f, 0.78f, 1.0f), ImVec4(0.64f, 0.83f, 0.34f, 1.0f), t)); 232 | else 233 | col_bg = ImGui::GetColorU32(ImLerp(ImVec4(0.85f, 0.85f, 0.85f, 1.0f), ImVec4(0.56f, 0.83f, 0.26f, 1.0f), t)); 234 | 235 | draw_list->AddRectFilled(p, ImVec2(p.x + width, p.y + height), col_bg, height * 0.5f); 236 | draw_list->AddCircleFilled(ImVec2(p.x + radius + t * (width - radius * 2.0f), p.y + radius), radius - 1.5f, IM_COL32(255, 255, 255, 255)); 237 | 238 | return ret; 239 | } 240 | 241 | bool InputTextRequired(const char* label, std::string* str, ImGuiInputTextFlags flags) 242 | { 243 | bool empty = str->empty(); 244 | if (empty) { 245 | ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(1.0f, 0.0f, 0.0f, 0.2f)); 246 | ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(1.0f, 0.0f, 0.0f, 0.2f)); 247 | ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(1.0f, 0.0f, 0.0f, 0.2f)); 248 | } 249 | bool ret = ImGui::InputText(label, str, flags); 250 | if (empty) { 251 | ImGui::PopStyleColor(3); 252 | } 253 | return ret; 254 | } 255 | 256 | // Callback function to handle resizing the std::string buffer 257 | static int InputTextCallback(ImGuiInputTextCallbackData* data) 258 | { 259 | std::string* str = static_cast(data->UserData); 260 | if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { 261 | str->resize(data->BufTextLen); 262 | data->Buf = &(*str)[0]; 263 | } 264 | return 0; 265 | } 266 | 267 | 268 | bool InputTextWithPaste(const char* label, std::string& text, const ImVec2& size, bool multiline, ImGuiInputTextFlags flags) 269 | { 270 | ImGui::PushID(&text); 271 | // Add the ImGuiInputTextFlags_CallbackResize flag to allow resizing the std::string buffer 272 | flags |= ImGuiInputTextFlags_CallbackResize; 273 | 274 | // Call the InputTextWithCallback function with the std::string buffer and callback function 275 | bool result; 276 | if (multiline) { 277 | result = ImGui::InputTextMultiline(label, &text[0], text.capacity()+1, size, flags, InputTextCallback, &text); 278 | 279 | } else { 280 | result = ImGui::InputText(label, &text[0], text.capacity() + 1, flags, InputTextCallback, &text); 281 | } 282 | 283 | // Check if the InputText is hovered and the right mouse button is clicked 284 | if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(1)) { 285 | // Set the context menu to be shown 286 | ImGui::OpenPopup("InputTextContextMenu"); 287 | } 288 | 289 | // Display the context menu 290 | if (ImGui::BeginPopup("InputTextContextMenu")) { 291 | if (ImGui::MenuItem("Copy")) { 292 | // Copy the selected text to the clipboard 293 | const char* selected_text = text.c_str(); 294 | if (selected_text) { 295 | ImGui::SetClipboardText(selected_text); 296 | } 297 | } 298 | if (ImGui::MenuItem("Paste")) { 299 | // Read the clipboard content 300 | const char* clipboard = ImGui::GetClipboardText(); 301 | 302 | if (clipboard) { 303 | // Insert the clipboard content into the text buffer 304 | text.append(clipboard); 305 | } 306 | } 307 | ImGui::EndPopup(); 308 | } 309 | ImGui::PopID(); 310 | return result; 311 | } 312 | 313 | bool InputTextWithPasteRequired(const char* label, std::string& text, const ImVec2& size, bool multiline, ImGuiInputTextFlags flags) 314 | { 315 | ImGui::PushID(&text); 316 | // Add the ImGuiInputTextFlags_CallbackResize flag to allow resizing the std::string buffer 317 | flags |= ImGuiInputTextFlags_CallbackResize; 318 | 319 | // Call the InputTextWithCallback function with the std::string buffer and callback function 320 | bool empty = text.empty(); 321 | if (empty) { 322 | ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(1.0f, 0.0f, 0.0f, 0.2f)); 323 | ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(1.0f, 0.0f, 0.0f, 0.2f)); 324 | ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(1.0f, 0.0f, 0.0f, 0.2f)); 325 | } 326 | bool result; 327 | if (multiline) { 328 | result = ImGui::InputTextMultiline(label, &text[0], text.capacity() + 1, size, flags, InputTextCallback, &text); 329 | 330 | } else { 331 | 332 | result = ImGui::InputText(label, &text[0], text.capacity() + 1, flags, InputTextCallback, &text); 333 | } 334 | if (empty) { 335 | ImGui::PopStyleColor(3); 336 | } 337 | 338 | // Check if the InputText is hovered and the right mouse button is clicked 339 | if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(1)) { 340 | // Set the context menu to be shown 341 | ImGui::OpenPopup("InputTextContextMenu"); 342 | } 343 | 344 | // Display the context menu 345 | if (ImGui::BeginPopup("InputTextContextMenu")) { 346 | if (ImGui::MenuItem("Copy")) { 347 | // Copy the selected text to the clipboard 348 | const char* selected_text = text.c_str(); 349 | if (selected_text) { 350 | ImGui::SetClipboardText(selected_text); 351 | } 352 | } 353 | if (ImGui::MenuItem("Paste")) { 354 | // Read the clipboard content 355 | const char* clipboard = ImGui::GetClipboardText(); 356 | 357 | if (clipboard) { 358 | // Insert the clipboard content into the text buffer 359 | text.append(clipboard); 360 | } 361 | } 362 | ImGui::EndPopup(); 363 | } 364 | ImGui::PopID(); 365 | return result; 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /src/bin/Utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | inline size_t strl(const char* a_str) 4 | { 5 | if (a_str == nullptr) 6 | return 0; 7 | size_t len = 0; 8 | while (a_str[len] != '\0' && len < 0x7FFFFFFF) { 9 | len++; 10 | } 11 | return len; 12 | } 13 | 14 | namespace Utils 15 | { 16 | namespace imgui 17 | { 18 | 19 | void HoverNote(const char* text, const char* note = "*"); 20 | } 21 | 22 | 23 | template 24 | void loadUsefulPlugins(std::unordered_set& mods) 25 | { 26 | auto data = RE::TESDataHandler::GetSingleton(); 27 | for (auto form : data->GetFormArray()) { 28 | if (form) { 29 | if (!mods.contains(form->GetFile())) { 30 | mods.insert(form->GetFile()); 31 | } 32 | } 33 | } 34 | }; 35 | 36 | using _GetFormEditorID = const char* (*)(std::uint32_t); 37 | 38 | std::string getFormEditorID(const RE::TESForm* a_form); 39 | } 40 | 41 | /*Helper class to load from a simple ini file.*/ 42 | class settingsLoader 43 | { 44 | private: 45 | CSimpleIniA _ini; 46 | const char* _section; 47 | int _loadedSettings; 48 | int _savedSettings; 49 | const char* _settingsFile; 50 | 51 | public: 52 | settingsLoader(const char* settingsFile); 53 | ~settingsLoader(); 54 | /*Set the active section. Load() will load keys from this section.*/ 55 | void setActiveSection(const char* section); 56 | /*Load a boolean value if present.*/ 57 | void load(bool& settingRef, const char* key); 58 | /*Load a float value if present.*/ 59 | void load(float& settingRef, const char* key); 60 | /*Load an unsigned int value if present.*/ 61 | void load(uint32_t& settingRef, const char* key); 62 | 63 | void save(bool& settingRef, const char* key); 64 | void save(float& settingRef, const char* key); 65 | void save(uint32_t& settingRef, const char* key); 66 | 67 | void flush(); 68 | 69 | /*Load an integer value if present.*/ 70 | void load(int& settingRef, const char* key); 71 | 72 | void log() 73 | { 74 | logger::info("Loaded {} settings, saved {} settings from {}.", _loadedSettings, _savedSettings, _settingsFile); 75 | } 76 | }; 77 | 78 | 79 | namespace ImGui 80 | { 81 | bool SliderFloatWithSteps(const char* label, float* v, float v_min, float v_max, float v_step); 82 | void HoverNote(const char* text, const char* note = "(?)"); 83 | 84 | bool ToggleButton(const char* str_id, bool* v); 85 | 86 | bool InputTextRequired(const char* label, std::string* str, ImGuiInputTextFlags flags = 0); 87 | bool InputTextWithPaste(const char* label, std::string& text, const ImVec2& size = ImVec2(0, 0), bool multiline = false, ImGuiInputTextFlags flags = 0); 88 | bool InputTextWithPasteRequired(const char* label, std::string& text, const ImVec2& size = ImVec2(0, 0), bool multiline = false, ImGuiInputTextFlags flags = 0); 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/bin/_empty.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D7ry/dMenu/1b01733cde3ad3d98cc40cb3addaa870026ac1a4/src/bin/_empty.cpp -------------------------------------------------------------------------------- /src/bin/_empty.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D7ry/dMenu/1b01733cde3ad3d98cc40cb3addaa870026ac1a4/src/bin/_empty.h -------------------------------------------------------------------------------- /src/bin/dMenu.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "imgui_internal.h" 4 | #include "imgui.h" 5 | 6 | #include "dMenu.h" 7 | #include "menus/Trainer.h" 8 | #include "menus/AIM.h" 9 | #include "menus/Settings.h" 10 | #include "menus/ModSettings.h" 11 | 12 | #include "Renderer.h" 13 | void DMenu::draw() 14 | { 15 | ImVec2 screenSize = ImGui::GetMainViewport()->Size; 16 | float screenSizeX = screenSize.x; 17 | float screenSizeY = screenSize.y; 18 | ImGui::SetNextWindowSize(ImVec2(Settings::relative_window_size_h * screenSizeX, Settings::relative_window_size_v * screenSizeY)); 19 | 20 | ImGuiWindowFlags windowFlags = ImGuiWindowFlags_None; 21 | if (Settings::lockWindowPos) { 22 | windowFlags |= ImGuiWindowFlags_NoMove; 23 | } 24 | if (Settings::lockWindowSize) { 25 | windowFlags |= ImGuiWindowFlags_NoResize; 26 | } 27 | ImGui::Begin("dMenu", NULL, windowFlags); 28 | 29 | if (ImGui::BeginTabBar("TabBar", ImGuiTabBarFlags_FittingPolicyResizeDown)) { 30 | if (ImGui::BeginTabItem("Trainer")) { 31 | currentTab = Trainer; 32 | Trainer::show(); 33 | ImGui::EndTabItem(); 34 | } 35 | 36 | if (ImGui::BeginTabItem("AIM")) { 37 | currentTab = Tab::AIM; 38 | AIM::show(); 39 | ImGui::EndTabItem(); 40 | } 41 | 42 | if (ImGui::BeginTabItem("Mod Config")) { 43 | currentTab = Tab::ModSettings; 44 | ModSettings::show(); 45 | ImGui::EndTabItem(); 46 | } 47 | 48 | if (ImGui::BeginTabItem("Settings")) { 49 | currentTab = Tab::Settings; 50 | Settings::show(); 51 | ImGui::EndTabItem(); 52 | } 53 | 54 | 55 | 56 | ImGui::EndTabBar(); 57 | } 58 | 59 | ImGui::End(); 60 | } 61 | 62 | ImVec2 DMenu::relativeSize(float a_width, float a_height) 63 | { 64 | ImVec2 parentSize = ImGui::GetMainViewport()->Size; 65 | return ImVec2(a_width * parentSize.x, a_height * parentSize.y); 66 | } 67 | 68 | 69 | void DMenu::init(float a_screenWidth, float a_screenHeight) 70 | { 71 | INFO("Initializing DMenu"); 72 | AIM::init(); 73 | Trainer::init(); 74 | 75 | ImVec2 mainWindowSize = { float(a_screenWidth * Settings::relative_window_size_h), float(a_screenHeight * Settings::relative_window_size_v) }; 76 | ImGui::SetNextWindowSize(mainWindowSize, ImGuiCond_FirstUseEver); 77 | ImVec2 mainWindowPos = { Settings::windowPos_x, Settings::windowPos_y }; 78 | ImGui::SetNextWindowSize(mainWindowPos, ImGuiCond_FirstUseEver); 79 | ImGui::GetIO().FontGlobalScale = Settings::fontScale; 80 | 81 | INFO("DMenu initialized"); 82 | 83 | 84 | initialized = true; 85 | } 86 | 87 | -------------------------------------------------------------------------------- /src/bin/dMenu.h: -------------------------------------------------------------------------------- 1 | class DMenu 2 | { 3 | public: 4 | /// 5 | /// Handles drawing of actual dMenu elements onto the canvas. This function assumes the menu always shows up. 6 | /// 7 | static void draw(); 8 | 9 | static ImVec2 relativeSize(float width, float height); 10 | // called once the game data is loaded and d3d hook installed. 11 | static void init(float a_screenWidth, float a_screenHeight); 12 | 13 | static inline bool initialized = false; 14 | 15 | 16 | private: 17 | 18 | // Define an enum to represent the different tabs 19 | enum Tab 20 | { 21 | Trainer, 22 | AIM, 23 | Settings, 24 | ModSettings, 25 | Help 26 | }; 27 | 28 | // Declare a variable to store the currently selected tab 29 | static inline Tab currentTab = Trainer; 30 | 31 | 32 | }; 33 | 34 | -------------------------------------------------------------------------------- /src/bin/main.cpp: -------------------------------------------------------------------------------- 1 | #include "Renderer.h" 2 | #include "InputListener.h" 3 | #include "Hooks.h" 4 | #include "menus/ModSettings.h" 5 | #include "menus/Settings.h" 6 | void MessageHandler(SKSE::MessagingInterface::Message* a_msg) 7 | { 8 | switch (a_msg->type) { 9 | case SKSE::MessagingInterface::kDataLoaded: 10 | Hooks::Install(); 11 | ModSettings::save_all_game_setting(); // in case some .esp overwrite the game setting // fixme 12 | ModSettings::SendAllSettingsUpdateEvent(); // notify all mods to update their settings 13 | break; 14 | case SKSE::MessagingInterface::kPostLoad: 15 | break; 16 | case SKSE::MessagingInterface::kPostLoadGame: 17 | break; 18 | } 19 | } 20 | 21 | void onSKSEInit() 22 | { 23 | Renderer::Install(); 24 | Settings::init(); 25 | ModSettings::init(); // init modsetting before everyone else 26 | } 27 | 28 | namespace 29 | { 30 | void InitializeLog() 31 | { 32 | #ifndef NDEBUG 33 | auto sink = std::make_shared(); 34 | #else 35 | auto path = logger::log_directory(); 36 | if (!path) { 37 | util::report_and_fail("Failed to find standard logging directory"sv); 38 | } 39 | 40 | *path /= fmt::format("{}.log"sv, Plugin::NAME); 41 | auto sink = std::make_shared(path->string(), true); 42 | #endif 43 | 44 | #ifndef NDEBUG 45 | const auto level = spdlog::level::trace; 46 | #else 47 | const auto level = spdlog::level::info; 48 | #endif 49 | 50 | auto log = std::make_shared("global log"s, std::move(sink)); 51 | log->set_level(level); 52 | log->flush_on(level); 53 | 54 | spdlog::set_default_logger(std::move(log)); 55 | spdlog::set_pattern("%s(%#): [%^%l%$] %v"s); 56 | } 57 | } 58 | 59 | std::string wstring2string(const std::wstring& wstr, UINT CodePage) 60 | 61 | { 62 | 63 | std::string ret; 64 | 65 | int len = WideCharToMultiByte(CodePage, 0, wstr.c_str(), (int)wstr.size(), NULL, 0, NULL, NULL); 66 | 67 | ret.resize((size_t)len, 0); 68 | 69 | WideCharToMultiByte(CodePage, 0, wstr.c_str(), (int)wstr.size(), &ret[0], len, NULL, NULL); 70 | 71 | return ret; 72 | 73 | } 74 | 75 | 76 | extern "C" DLLEXPORT bool SKSEAPI SKSEPlugin_Query(const SKSE::QueryInterface* a_skse, SKSE::PluginInfo* a_info) 77 | { 78 | a_info->infoVersion = SKSE::PluginInfo::kVersion; 79 | a_info->name = Plugin::NAME.data(); 80 | a_info->version = Plugin::VERSION[0]; 81 | 82 | if (a_skse->IsEditor()) { 83 | logger::critical("Loaded in editor, marking as incompatible"sv); 84 | return false; 85 | } 86 | 87 | const auto ver = a_skse->RuntimeVersion(); 88 | if (ver < SKSE::RUNTIME_SSE_1_5_39) { 89 | logger::critical(FMT_STRING("Unsupported runtime version {}"), ver.string()); 90 | return false; 91 | } 92 | 93 | return true; 94 | } 95 | 96 | extern "C" DLLEXPORT constinit auto SKSEPlugin_Version = []() { 97 | SKSE::PluginVersionData v; 98 | 99 | v.PluginVersion(Plugin::VERSION); 100 | v.PluginName(Plugin::NAME); 101 | 102 | v.UsesAddressLibrary(true); 103 | v.CompatibleVersions({ SKSE::RUNTIME_SSE_LATEST }); 104 | v.HasNoStructUse(true); 105 | 106 | return v; 107 | }(); 108 | 109 | 110 | extern "C" DLLEXPORT bool SKSEAPI SKSEPlugin_Load(const SKSE::LoadInterface* a_skse) 111 | { 112 | REL::Module::reset(); // Clib-NG bug workaround 113 | 114 | InitializeLog(); 115 | 116 | 117 | logger::info("{} v{}"sv, Plugin::NAME, Plugin::VERSION.string()); 118 | 119 | SKSE::Init(a_skse); 120 | 121 | auto messaging = SKSE::GetMessagingInterface(); 122 | if (!messaging->RegisterListener("SKSE", MessageHandler)) { 123 | return false; 124 | } 125 | 126 | onSKSEInit(); 127 | 128 | return true; 129 | } 130 | -------------------------------------------------------------------------------- /src/bin/menus/AIM.cpp: -------------------------------------------------------------------------------- 1 | #include "imgui.h" 2 | #include "imgui_internal.h" 3 | #include 4 | #include 5 | #include "AIM.h" 6 | 7 | #include "bin/dMenu.h" 8 | #include 9 | 10 | #include "bin/Utils.h" 11 | // i swear i will RE this 12 | inline void sendConsoleCommand(std::string a_command) 13 | { 14 | const auto scriptFactory = RE::IFormFactory::GetConcreteFormFactoryByType(); 15 | const auto script = scriptFactory ? scriptFactory->Create() : nullptr; 16 | if (script) { 17 | const auto selectedRef = RE::Console::GetSelectedRef(); 18 | script->SetCommand(a_command); 19 | script->CompileAndRun(selectedRef.get()); 20 | delete script; 21 | } 22 | } 23 | 24 | 25 | static bool _init = false; 26 | 27 | 28 | static std::vector> _types = { 29 | { "Weapon", false }, 30 | { "Armor", false }, 31 | { "Ammo", false }, 32 | { "Book", false }, 33 | { "Ingredient", false }, 34 | { "Key", false }, 35 | { "Misc", false }, 36 | { "NPC", false } 37 | }; 38 | 39 | static std::vector> _mods; 40 | static int _selectedModIndex = -1; 41 | 42 | static std::vector> _items; // items to show on AIM 43 | static RE::TESForm* _selectedItem; 44 | 45 | static bool _cached = false; 46 | 47 | static ImGuiTextFilter _modFilter; 48 | static ImGuiTextFilter _itemFilter; 49 | 50 | 51 | 52 | void AIM::init() 53 | { 54 | if (_init) { 55 | return; 56 | } 57 | RE::TESDataHandler* data = RE::TESDataHandler::GetSingleton(); 58 | std::unordered_set mods; 59 | 60 | // only show mods with valid items 61 | Utils::loadUsefulPlugins(mods); 62 | Utils::loadUsefulPlugins(mods); 63 | Utils::loadUsefulPlugins(mods); 64 | Utils::loadUsefulPlugins(mods); 65 | Utils::loadUsefulPlugins(mods); 66 | Utils::loadUsefulPlugins(mods); 67 | Utils::loadUsefulPlugins(mods); 68 | Utils::loadUsefulPlugins(mods); 69 | 70 | for (auto mod : mods) { 71 | _mods.push_back({ mod, false }); 72 | } 73 | 74 | _selectedModIndex = 0; 75 | 76 | _init = true; 77 | INFO("AIM initialized"); 78 | } 79 | 80 | 81 | template 82 | inline void cacheItems(RE::TESDataHandler* a_data) 83 | { 84 | RE::TESFile* selectedMod = _mods[_selectedModIndex].first; 85 | for (auto form : a_data->GetFormArray()) { 86 | if (selectedMod->IsFormInMod(form->GetFormID()) 87 | && form->GetFullNameLength() != 0) { 88 | std::string name = form->GetFullName(); 89 | if (form->IsArmor()) { 90 | if (form->As()->IsHeavyArmor()) { 91 | name += " (Heavy)"; 92 | } else { 93 | name += " (Light)"; 94 | } 95 | } 96 | _items.push_back({ name, form }); 97 | } 98 | } 99 | } 100 | 101 | /* Present all filtered items to the user under the "Items" section*/ 102 | void cache() 103 | { 104 | _items.clear(); 105 | auto data = RE::TESDataHandler::GetSingleton(); 106 | if (!data || _selectedModIndex == -1) { 107 | return; 108 | } 109 | if (_types[0].second) 110 | cacheItems(data); 111 | if (_types[1].second) 112 | cacheItems(data); 113 | if (_types[2].second) 114 | cacheItems(data); 115 | if (_types[3].second) 116 | cacheItems(data); 117 | if (_types[4].second) 118 | cacheItems(data); 119 | if (_types[5].second) 120 | cacheItems(data); 121 | if (_types[6].second) 122 | cacheItems(data); 123 | if (_types[7].second) 124 | cacheItems(data); 125 | // filter out unplayable weapons in 2nd pass 126 | for (auto it = _items.begin(); it != _items.end();) { 127 | auto form = it->second; 128 | auto formtype = form->GetFormType(); 129 | switch (formtype) { 130 | case RE::FormType::Weapon: 131 | if (form->As()->weaponData.flags.any(RE::TESObjectWEAP::Data::Flag::kNonPlayable)) { 132 | it = _items.erase(it); 133 | continue; 134 | } 135 | break; 136 | } 137 | it++; 138 | } 139 | 140 | } 141 | 142 | void AIM::show() 143 | { 144 | // Use consistent padding and alignment 145 | ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10, 10)); 146 | ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(5, 5)); 147 | 148 | // Render the mod filter box 149 | _modFilter.Draw("Mod Name"); 150 | 151 | // Render the mod dropdown menu 152 | if (ImGui::BeginCombo("Mods", _mods[_selectedModIndex].first->GetFilename().data())) { 153 | for (int i = 0; i < _mods.size(); i++) { 154 | if (_modFilter.PassFilter(_mods[i].first->GetFilename().data())) { 155 | bool isSelected = (_mods[_selectedModIndex].first == _mods[i].first); 156 | if (ImGui::Selectable(_mods[i].first->GetFilename().data(), isSelected)) { 157 | _selectedModIndex = i; 158 | _cached = false; 159 | } 160 | if (isSelected) { 161 | ImGui::SetItemDefaultFocus(); 162 | } 163 | } 164 | } 165 | ImGui::EndCombo(); 166 | } 167 | 168 | // Item type filtering 169 | ImGui::Spacing(); 170 | ImGui::Separator(); 171 | ImGui::Spacing(); 172 | 173 | // Group related controls together 174 | ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10, 0)); 175 | for (int i = 0; i < _types.size(); i++) { 176 | if (ImGui::Checkbox(_types[i].first.c_str(), &_types[i].second)) { 177 | _cached = false; 178 | } 179 | if (i < _types.size() - 1) { 180 | ImGui::SameLine(); 181 | } 182 | } 183 | ImGui::PopStyleVar(); 184 | 185 | ImGui::Spacing(); 186 | // Render the list of items 187 | _itemFilter.Draw("Item Name"); 188 | 189 | // Use a child window to limit the size of the item list 190 | ImGui::BeginChild("Items", ImVec2(0, ImGui::GetContentRegionAvail().y * 0.7), true); 191 | 192 | for (int i = 0; i < _items.size(); i++) { 193 | // Filter 194 | if (_itemFilter.PassFilter(_items[i].first.data())) { 195 | ImGui::Selectable(_items[i].first.data()); 196 | 197 | if (ImGui::IsItemClicked()) { 198 | _selectedItem = _items[i].second; 199 | } 200 | 201 | if (ImGui::IsItemHovered()) { 202 | ImGui::BeginTooltip(); 203 | ImGui::Text(fmt::format("{:x}", _items[i].second->GetFormID()).c_str()); 204 | ImGui::EndTooltip(); 205 | } 206 | 207 | // show item texture 208 | } 209 | } 210 | ImGui::EndChild(); 211 | 212 | // Show selected item info and spawn button 213 | if (_selectedItem != nullptr) { 214 | ImGui::Text("Selected Item: "); 215 | ImGui::SameLine(); 216 | ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "%s", _selectedItem->GetName()); 217 | static char buf[16] = "1"; 218 | if (ImGui::Button("Spawn", ImVec2(ImGui::GetContentRegionAvail().x * 0.2, 0))) { 219 | if (RE::PlayerCharacter::GetSingleton() != nullptr) { 220 | // Spawn item 221 | uint32_t formIDuint = _selectedItem->GetFormID(); 222 | std::string formID = fmt::format("{:x}", formIDuint); 223 | std::string addCmd = _selectedItem->GetFormType() == RE::FormType::NPC ? "placeatme" : "additem"; 224 | std::string cmd = "player." + addCmd + " " + formID + " " + buf; 225 | sendConsoleCommand(cmd); 226 | } 227 | } 228 | ImGui::SameLine(); 229 | ImGui::InputText("Amount", buf, 16, ImGuiInputTextFlags_CharsDecimal); 230 | } 231 | //todo: make this work 232 | //if (ImGui::Button("Inspect", ImVec2(ImGui::GetContentRegionAvail().x * 0.2, 0))) { 233 | // QUIHelper::inspect(); 234 | //} 235 | // 236 | 237 | if (!_cached) { 238 | cache(); 239 | } 240 | 241 | // Use consistent padding and alignment 242 | ImGui::PopStyleVar(2); 243 | } 244 | -------------------------------------------------------------------------------- /src/bin/menus/AIM.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class AIM 4 | { 5 | public: 6 | static void show(); 7 | 8 | static void init(); 9 | }; 10 | -------------------------------------------------------------------------------- /src/bin/menus/ModSettings.cpp: -------------------------------------------------------------------------------- 1 | #include "imgui_internal.h" 2 | #include 3 | #include 4 | #include "imgui_stdlib.h" 5 | 6 | 7 | #include "SimpleIni.h" 8 | #include 9 | #include "ModSettings.h" 10 | 11 | #include "bin/Utils.h" 12 | #include "Settings.h" 13 | static RE::GameSettingCollection* gsc = nullptr; 14 | inline bool ModSettings::entry_base::Control::Req::satisfied() 15 | { 16 | bool val = false; 17 | if (type == kReqType_Checkbox) { 18 | auto it = ModSettings::m_checkbox_toggle.find(id); 19 | if (it == ModSettings::m_checkbox_toggle.end()) { 20 | return false; 21 | } 22 | val = it->second->value; 23 | } else if (type == kReqType_GameSetting) { 24 | if (!gsc) { 25 | gsc = RE::GameSettingCollection::GetSingleton(); 26 | if (!gsc) 27 | return false; 28 | } 29 | auto setting = gsc->GetSetting(id.c_str()); 30 | if (!setting) 31 | return false; 32 | val = setting->GetBool(); 33 | } 34 | return this->_not ? !val : val; 35 | } 36 | 37 | inline bool ModSettings::entry_base::Control::satisfied() 38 | { 39 | for (auto& req : reqs) { 40 | if (!req.satisfied()) { 41 | return false; 42 | } 43 | } 44 | return true; 45 | } 46 | 47 | 48 | using json = nlohmann::json; 49 | 50 | void ModSettings::SendAllSettingsUpdateEvent() 51 | { 52 | for (auto& mod : mods) { 53 | SendSettingsUpdateEvent(mod->name); 54 | } 55 | } 56 | void ModSettings::show_reloadTranslationButton() 57 | { 58 | if (ImGui::Button("Reload Translation")) { 59 | Translator::ReLoadTranslations(); 60 | } 61 | } 62 | // not used anymore, we auto save. 63 | void ModSettings::show_saveButton() 64 | { 65 | bool unsaved_changes = !ini_dirty_mods.empty(); 66 | 67 | if (unsaved_changes) { 68 | ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.7f, 0.3f, 1.0f)); 69 | ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.8f, 0.4f, 1.0f)); 70 | ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.4f, 0.9f, 0.5f, 1.0f)); 71 | } 72 | 73 | if (ImGui::Button("Save")) { 74 | for (auto& mod : ini_dirty_mods) { 75 | flush_ini(mod); 76 | flush_game_setting(mod); 77 | for (auto callback : mod->callbacks) { 78 | callback(); 79 | } 80 | SendSettingsUpdateEvent(mod->name); 81 | } 82 | ini_dirty_mods.clear(); 83 | } 84 | 85 | if (unsaved_changes) { 86 | ImGui::PopStyleColor(3); 87 | } 88 | } 89 | 90 | void ModSettings::show_cancelButton() 91 | { 92 | bool unsaved_changes = !ini_dirty_mods.empty(); 93 | 94 | if (ImGui::Button("Cancel")) { 95 | for (auto& mod : ini_dirty_mods) { 96 | load_ini(mod); 97 | } 98 | ini_dirty_mods.clear(); 99 | } 100 | } 101 | 102 | void ModSettings::show_saveJsonButton() 103 | { 104 | bool unsaved_changes = !json_dirty_mods.empty(); 105 | 106 | if (unsaved_changes) { 107 | ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.7f, 0.3f, 1.0f)); 108 | ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.8f, 0.4f, 1.0f)); 109 | ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.4f, 0.9f, 0.5f, 1.0f)); 110 | } 111 | 112 | if (ImGui::Button("Save Config")) { 113 | for (auto& mod : json_dirty_mods) { 114 | flush_json(mod); 115 | } 116 | json_dirty_mods.clear(); 117 | } 118 | 119 | if (unsaved_changes) { 120 | ImGui::PopStyleColor(3); 121 | } 122 | } 123 | 124 | void ModSettings::show_buttons_window() 125 | { 126 | ImVec2 mainWindowSize = ImGui::GetWindowSize(); 127 | ImVec2 mainWindowPos = ImGui::GetWindowPos(); 128 | 129 | ImVec2 buttonsWindowSize = ImVec2(mainWindowSize.x * 0.15, mainWindowSize.y * 0.1); 130 | 131 | ImGui::SetNextWindowPos(ImVec2(mainWindowPos.x + mainWindowSize.x, mainWindowPos.y + mainWindowSize.y - buttonsWindowSize.y)); 132 | ImGui::SetNextWindowSize(buttonsWindowSize); // Adjust the height as needed 133 | ImGui::Begin("Buttons", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings); 134 | show_saveButton(); 135 | ImGui::SameLine(); 136 | show_cancelButton(); 137 | if (edit_mode) { 138 | ImGui::SameLine(); 139 | show_saveJsonButton(); 140 | ImGui::SameLine(); 141 | //show_reloadTranslationButton(); 142 | } 143 | ImGui::End(); 144 | } 145 | 146 | 147 | void ModSettings::show_entry_edit(entry_base* entry, mod_setting* mod) 148 | { 149 | ImGui::PushID(entry); 150 | 151 | bool edited = false; 152 | 153 | // Show input fields to edit the setting name and ini id 154 | if (ImGui::InputTextWithPasteRequired("Name", entry->name.def)) 155 | edited = true; 156 | if (ImGui::InputTextWithPaste("Description", entry->desc.def, ImVec2(ImGui::GetCurrentWindow()->Size.x * 0.8, ImGui::GetTextLineHeight() * 3), true, ImGuiInputTextFlags_AutoSelectAll)) 157 | edited = true; 158 | int current_type = entry->type; 159 | 160 | // Show fields specific to the selected setting type 161 | switch (current_type) { 162 | case kEntryType_Checkbox: 163 | { 164 | setting_checkbox* checkbox = dynamic_cast(entry); 165 | if (ImGui::Checkbox("Default", &checkbox->default_value)) { 166 | edited = true; 167 | } 168 | std::string old_control_id = checkbox->control_id; 169 | if (ImGui::InputTextWithPaste("Control ID", checkbox->control_id)) { // on change of control id 170 | if (!old_control_id.empty()) { // remove old control id 171 | m_checkbox_toggle.erase(old_control_id); 172 | } 173 | if (!checkbox->control_id.empty()) { 174 | m_checkbox_toggle[checkbox->control_id] = checkbox; // update control id 175 | } 176 | edited = true; 177 | } 178 | break; 179 | } 180 | case kEntryType_Slider: 181 | { 182 | setting_slider* slider = dynamic_cast(entry); 183 | if (ImGui::InputFloat("Default", &slider->default_value)) 184 | edited = true; 185 | if (ImGui::InputFloat("Min", &slider->min)) 186 | edited = true; 187 | if (ImGui::InputFloat("Max", &slider->max)) 188 | edited = true; 189 | if (ImGui::InputFloat("Step", &slider->step)) 190 | edited = true; 191 | break; 192 | } 193 | case kEntryType_Textbox: 194 | { 195 | setting_textbox* textbox = dynamic_cast(entry); 196 | if (ImGui::InputTextWithPaste("Default", textbox->default_value)) 197 | edited = true; 198 | break; 199 | } 200 | case kEntryType_Dropdown: 201 | { 202 | setting_dropdown* dropdown = dynamic_cast(entry); 203 | ImGui::Text("Dropdown options"); 204 | if (ImGui::BeginChild("##dropdown_items", ImVec2(0, 200), true, ImGuiWindowFlags_AlwaysAutoResize)) 205 | { 206 | int buf; 207 | if (ImGui::InputInt("Default", &buf, 0, 100)) { 208 | dropdown->default_value = buf; 209 | edited = true; 210 | } 211 | if (ImGui::Button("Add")) { 212 | dropdown->options.emplace_back(); 213 | edited = true; 214 | } 215 | for (int i = 0; i < dropdown->options.size(); i++) { 216 | ImGui::PushID(i); 217 | if (ImGui::InputText("Option", &dropdown->options[i])) 218 | edited = true; 219 | ImGui::SameLine(); 220 | if (ImGui::Button("-")) { 221 | dropdown->options.erase(dropdown->options.begin() + i); 222 | if (dropdown->value == i) { // erased current value 223 | dropdown->value = 0; // reset to 1st value 224 | } 225 | edited = true; 226 | } 227 | 228 | ImGui::PopID(); 229 | } 230 | } 231 | ImGui::EndChild(); 232 | break; 233 | } 234 | case kEntryType_Text: 235 | { 236 | // color palette to set text color 237 | ImGui::Text("Text color"); 238 | if (ImGui::BeginChild("##text_color", ImVec2(0, 100), true, ImGuiWindowFlags_AlwaysAutoResize)) 239 | { 240 | entry_text* text = dynamic_cast(entry); 241 | float colorArray[4] = { text->_color.x, text->_color.y, text->_color.z, text->_color.w }; 242 | if (ImGui::ColorEdit4("Color", colorArray)) { 243 | text->_color = ImVec4(colorArray[0], colorArray[1], colorArray[2], colorArray[3]); 244 | edited = true; 245 | } 246 | ImGui::EndChild(); 247 | } 248 | break; 249 | } 250 | case kEntryType_Color: 251 | { 252 | // color palette to set text color 253 | ImGui::Text("Color"); 254 | if (ImGui::BeginChild("##color", ImVec2(0, 100), true, ImGuiWindowFlags_AlwaysAutoResize)) 255 | { 256 | setting_color* color = dynamic_cast(entry); 257 | float colorArray[4] = { color->default_color.x, color->default_color.y, color->default_color.z, color->default_color.w }; 258 | if (ImGui::ColorEdit4("Default Color", colorArray)) { 259 | color->default_color = ImVec4(colorArray[0], colorArray[1], colorArray[2], colorArray[3]); 260 | edited = true; 261 | } 262 | ImGui::EndChild(); 263 | } 264 | break; 265 | } 266 | case kEntryType_Keymap: 267 | { 268 | // color palette to set text color 269 | ImGui::Text("Keymap"); 270 | if (ImGui::BeginChild("##keymap", ImVec2(0, 100), true, ImGuiWindowFlags_AlwaysAutoResize)) 271 | { 272 | setting_keymap* keymap = dynamic_cast(entry); 273 | if (ImGui::InputInt("Default Key ID", &(keymap->default_value))) { 274 | edited = true; 275 | } 276 | ImGui::EndChild(); 277 | } 278 | break; 279 | } 280 | case kEntryType_Button: 281 | { 282 | ImGui::Text("Button"); 283 | if (ImGui::BeginChild("##button", ImVec2(0, 100), true, ImGuiWindowFlags_AlwaysAutoResize)) 284 | { 285 | entry_button* button = dynamic_cast(entry); 286 | if (ImGui::InputText("ID", &(button->id))) { 287 | edited = true; 288 | } 289 | ImGui::EndChild(); 290 | } 291 | } 292 | default: 293 | break; 294 | } 295 | 296 | ImGui::Text("Control"); 297 | // choose fail action 298 | static const char* failActions[] = { "Disable", "Hide" }; 299 | if (ImGui::BeginCombo("Fail Action", failActions[(int)entry->control.failAction])) { 300 | for (int i = 0; i < 2; i++) { 301 | bool is_selected = ((int)entry->control.failAction == i); 302 | if (ImGui::Selectable(failActions[i], is_selected)) 303 | entry->control.failAction = static_cast(i); 304 | if (is_selected) 305 | ImGui::SetItemDefaultFocus(); 306 | } 307 | ImGui::EndCombo(); 308 | } 309 | if (ImGui::Button("Add")) { 310 | entry->control.reqs.emplace_back(); 311 | edited = true; 312 | } 313 | 314 | if (ImGui::BeginChild("##control_requirements", ImVec2(0, 100), true, ImGuiWindowFlags_AlwaysAutoResize)) { 315 | // set the number of columns to 3 316 | 317 | 318 | for (int i = 0; i < entry->control.reqs.size(); i++) { 319 | auto& req = entry->control.reqs[i]; // get a reference to the requirement at index i 320 | ImGui::PushID(&req); 321 | const int numColumns = 3; 322 | ImGui::Columns(numColumns, nullptr, false); 323 | // set the width of each column to be the same 324 | ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.5f); 325 | ImGui::SetColumnWidth(1, ImGui::GetWindowWidth() * 0.25f); 326 | ImGui::SetColumnWidth(2, ImGui::GetWindowWidth() * 0.25f); 327 | 328 | if (ImGui::InputTextWithPaste("id", req.id)) { 329 | edited = true; 330 | } 331 | 332 | // add the second column 333 | ImGui::NextColumn(); 334 | static const char* reqTypes[] = { "Checkbox", "GameSetting" }; 335 | if (ImGui::BeginCombo("Type", reqTypes[(int)req.type])) { 336 | for (int i = 0; i < 2; i++) { 337 | bool is_selected = ((int)req.type == i); 338 | if (ImGui::Selectable(reqTypes[i], is_selected)) 339 | req.type = static_cast(i); 340 | if (is_selected) 341 | ImGui::SetItemDefaultFocus(); 342 | } 343 | ImGui::EndCombo(); 344 | } 345 | 346 | // add the third column 347 | ImGui::NextColumn(); 348 | if (ImGui::Checkbox("not", &req._not)) { 349 | edited = true; 350 | } 351 | ImGui::SameLine(); 352 | if (ImGui::Button("-")) { 353 | entry->control.reqs.erase(entry->control.reqs.begin() + i); // erase the requirement at index i 354 | edited = true; 355 | i--; // update the loop index to account for the erased element 356 | } 357 | ImGui::Columns(1); 358 | 359 | ImGui::PopID(); 360 | } 361 | 362 | ImGui::EndChild(); 363 | } 364 | 365 | 366 | 367 | ImGui::Text("Localization"); 368 | if (ImGui::BeginChild((std::string(entry->name.def) + "##Localization").c_str(), ImVec2(0, 100), true, ImGuiWindowFlags_AlwaysAutoResize)) { 369 | if (ImGui::InputTextWithPaste("Name", entry->name.key)) 370 | edited = true; 371 | if (ImGui::InputTextWithPaste("Description", entry->desc.key)) 372 | edited = true; 373 | ImGui::EndChild(); 374 | } 375 | 376 | if (entry->is_setting()) { 377 | setting_base* setting = dynamic_cast(entry); 378 | ImGui::Text("Serialization"); 379 | if (ImGui::BeginChild((std::string(setting->name.def) + "##serialization").c_str(), ImVec2(0, 100), true, ImGuiWindowFlags_AlwaysAutoResize)) { 380 | if (ImGui::InputTextWithPasteRequired("ini ID", setting->ini_id)) 381 | edited = true; 382 | 383 | if (ImGui::InputTextWithPasteRequired("ini Section", setting->ini_section)) 384 | edited = true; 385 | 386 | if (ImGui::InputTextWithPaste("Game Setting", setting->gameSetting)) { 387 | edited = true; 388 | } 389 | if (setting->type == kEntryType_Slider) { 390 | if (!setting->gameSetting.empty()) { 391 | switch (setting->gameSetting[0]) { 392 | case 'f': 393 | case 'i': 394 | case 'u': 395 | break; 396 | default: 397 | ImGui::TextColored(ImVec4(1, 0, 0, 1), "For sliders, game setting must start with f, i, or u for float, int, or uint respectively for type specification."); 398 | break; 399 | } 400 | } 401 | } 402 | ImGui::EndChild(); 403 | } 404 | } 405 | if (edited) { 406 | json_dirty_mods.insert(mod); 407 | } 408 | //ImGui::EndChild(); 409 | ImGui::PopID(); 410 | } 411 | 412 | 413 | 414 | void ModSettings::show_entry(entry_base* entry, mod_setting* mod) 415 | { 416 | ImGui::PushID(entry); 417 | bool edited = false; 418 | 419 | // check if all control requirements are met 420 | bool available = entry->control.satisfied(); 421 | if (!available) { 422 | switch (entry->control.failAction) { 423 | case entry_base::Control::FailAction::kFailAction_Hide: 424 | if (!edit_mode) { // use disable fail action under edit mode 425 | ImGui::PopID(); 426 | return; 427 | } 428 | default: 429 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); 430 | ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); 431 | ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f); 432 | } 433 | } 434 | 435 | float width = ImGui::GetContentRegionAvail().x * 0.5f; 436 | switch (entry->type) { 437 | case kEntryType_Checkbox: 438 | { 439 | setting_checkbox* checkbox = dynamic_cast(entry); 440 | 441 | if (ImGui::Checkbox(checkbox->name.get(), &checkbox->value)) { 442 | edited = true; 443 | } 444 | if (ImGui::IsItemHovered()) { 445 | if (ImGui::IsKeyPressed(ImGuiKey_R)) { 446 | edited |= checkbox->reset(); 447 | } 448 | } 449 | 450 | if (!checkbox->desc.empty()) { 451 | ImGui::SameLine(); 452 | ImGui::HoverNote(checkbox->desc.get()); 453 | } 454 | 455 | } 456 | break; 457 | 458 | case kEntryType_Slider: 459 | { 460 | setting_slider* slider = dynamic_cast(entry); 461 | 462 | // Set the width of the slider to a fraction of the available width 463 | ImGui::SetNextItemWidth(width); 464 | if (ImGui::SliderFloatWithSteps(slider->name.get(), &slider->value, slider->min, slider->max, slider->step)) { 465 | edited = true; 466 | } 467 | 468 | if (ImGui::IsItemHovered()) { 469 | if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_LeftArrow))) { 470 | if (slider->value > slider->min) { 471 | slider->value -= slider->step; 472 | edited = true; 473 | } 474 | } 475 | if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_RightArrow))) { 476 | if (slider->value < slider->max) { 477 | slider->value += slider->step; 478 | edited = true; 479 | } 480 | } 481 | 482 | if (ImGui::IsKeyPressed(ImGuiKey_R)) { 483 | edited |= slider->reset(); 484 | } 485 | } 486 | 487 | 488 | if (!slider->desc.empty()) { 489 | ImGui::SameLine(); 490 | ImGui::HoverNote(slider->desc.get()); 491 | } 492 | } 493 | break; 494 | 495 | case kEntryType_Textbox: 496 | { 497 | setting_textbox* textbox = dynamic_cast(entry); 498 | 499 | ImGui::SetNextItemWidth(width); 500 | if (ImGui::InputText(textbox->name.get(), &textbox->value)) { 501 | edited = true; 502 | } 503 | 504 | if (!textbox->desc.empty()) { 505 | ImGui::SameLine(); 506 | ImGui::HoverNote(textbox->desc.get()); 507 | } 508 | 509 | if (ImGui::IsItemHovered()) { 510 | if (ImGui::IsKeyPressed(ImGuiKey_R)) { 511 | edited |= textbox->reset(); 512 | 513 | } 514 | } 515 | 516 | } 517 | break; 518 | 519 | case kEntryType_Dropdown: 520 | { 521 | setting_dropdown* dropdown = dynamic_cast(entry); 522 | const char* name = dropdown->name.get(); 523 | int selected = dropdown->value; 524 | 525 | std::vector options = dropdown->options; 526 | std::vector cstrings; 527 | for (auto& option : options) { 528 | cstrings.push_back(option.c_str()); 529 | } 530 | const char* preview_value = ""; 531 | if (selected >= 0 && selected < options.size()) { 532 | preview_value = cstrings[selected]; 533 | } 534 | ImGui::SetNextItemWidth(width); 535 | if (ImGui::BeginCombo(name, preview_value)) { 536 | for (int i = 0; i < options.size(); i++) { 537 | bool is_selected = (selected == i); 538 | 539 | if (ImGui::Selectable(cstrings[i], is_selected)) { 540 | selected = i; 541 | dropdown->value = selected; 542 | edited = true; 543 | } 544 | 545 | if (is_selected) { 546 | ImGui::SetItemDefaultFocus(); 547 | } 548 | } 549 | 550 | ImGui::EndCombo(); 551 | } 552 | 553 | if (ImGui::IsItemHovered()) { 554 | if (ImGui::IsKeyPressed(ImGuiKey_R)) { 555 | edited |= dropdown->reset(); 556 | } 557 | } 558 | 559 | if (!dropdown->desc.empty()) { 560 | ImGui::SameLine(); 561 | ImGui::HoverNote(dropdown->desc.get()); 562 | } 563 | } 564 | break; 565 | case kEntryType_Text: 566 | { 567 | entry_text* t = dynamic_cast(entry); 568 | ImGui::TextColored(t->_color, t->name.get()); 569 | } 570 | break; 571 | case kEntryType_Group: 572 | { 573 | entry_group* g = dynamic_cast(entry); 574 | ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); 575 | if (ImGui::CollapsingHeader(g->name.get())) { 576 | if (ImGui::IsItemHovered() && !g->desc.empty()) { 577 | ImGui::SetTooltip(g->desc.get()); 578 | } 579 | ImGui::PopStyleColor(); 580 | show_entries(g->entries, mod); 581 | } else { 582 | ImGui::PopStyleColor(); 583 | } 584 | } 585 | break; 586 | case kEntryType_Keymap: 587 | { 588 | setting_keymap* k = dynamic_cast(entry); 589 | std::string hash = std::to_string((unsigned long long)(void**)k); 590 | if (ImGui::Button("Remap")) { 591 | ImGui::OpenPopup(hash.data()); 592 | keyMapListening = k; 593 | } 594 | ImGui::SameLine(); 595 | if (ImGui::Button("Unmap")) { 596 | k->value = 0; 597 | edited = true; 598 | } 599 | ImGui::SameLine(); 600 | ImGui::Text("%s:", k->name.get()); 601 | ImGui::SameLine(); 602 | ImGui::Text(setting_keymap::keyid_to_str(k->value)); 603 | 604 | if (!k->desc.empty()) { 605 | ImGui::SameLine(); 606 | ImGui::HoverNote(k->desc.get()); 607 | } 608 | 609 | if (ImGui::BeginPopupModal(hash.data())) { 610 | ImGui::Text("Enter the key you wish to map"); 611 | if (keyMapListening == nullptr) { 612 | edited = true; 613 | ImGui::CloseCurrentPopup(); 614 | } 615 | ImGui::EndPopup(); 616 | } 617 | } 618 | break; 619 | case kEntryType_Color: 620 | { 621 | setting_color* c = dynamic_cast(entry); 622 | ImGui::SetNextItemWidth(width); 623 | setting_color* color = dynamic_cast(entry); 624 | float colorArray[4] = { color->color.x, color->color.y, color->color.z, color->color.w }; 625 | if (ImGui::ColorEdit4(color->name.get(), colorArray)) { 626 | color->color = ImVec4(colorArray[0], colorArray[1], colorArray[2], colorArray[3]); 627 | edited = true; 628 | } 629 | if (ImGui::IsItemHovered()) { 630 | if (ImGui::IsKeyPressed(ImGuiKey_R)) { 631 | edited |= color->reset(); 632 | } 633 | } 634 | if (!c->desc.empty()) { 635 | ImGui::SameLine(); 636 | ImGui::HoverNote(c->desc.get()); 637 | } 638 | } 639 | break; 640 | case kEntryType_Button: 641 | { 642 | entry_button* b = dynamic_cast(entry); 643 | if (ImGui::Button(b->name.get())) { 644 | // trigger button callback 645 | std::string custom_event_name = "dmenu_buttonCallback"; 646 | send_mod_callback_event(custom_event_name, b->id); 647 | } 648 | if (!b->desc.empty()) { 649 | ImGui::SameLine(); 650 | ImGui::HoverNote(b->desc.get()); 651 | } 652 | } 653 | default: 654 | break; 655 | } 656 | if (!available) { // disabled before 657 | ImGui::PopStyleVar(); 658 | ImGui::PopItemFlag(); 659 | ImGui::PopStyleColor(); 660 | } 661 | if (edited) { 662 | ini_dirty_mods.insert(mod); 663 | } 664 | ImGui::PopID(); 665 | } 666 | 667 | void ModSettings::show_entries(std::vector& entries, mod_setting* mod) 668 | { 669 | ImGui::PushID(&entries); 670 | bool edited = false; 671 | 672 | 673 | for (auto it = entries.begin(); it != entries.end(); it++) { 674 | ModSettings::entry_base* entry = *it; 675 | 676 | ImGui::PushID(entry); 677 | 678 | ImGui::Indent(); 679 | // edit entry 680 | bool entry_deleted = false; 681 | bool all_entries_deleted = false; 682 | 683 | if (edit_mode) { 684 | if (ImGui::ArrowButton("##up", ImGuiDir_Up)) { 685 | if (it != entries.begin()) { 686 | std::iter_swap(it, it - 1); 687 | edited = true; 688 | } 689 | } 690 | ImGui::SameLine(); 691 | if (ImGui::ArrowButton("##down", ImGuiDir_Down)) { 692 | if (it != entries.end() - 1) { 693 | std::iter_swap(it, it + 1); 694 | edited = true; 695 | } 696 | } 697 | ImGui::SameLine(); 698 | 699 | if (ImGui::Button("Edit")) { // Get the size of the current window 700 | ImGui::OpenPopup("Edit Setting"); 701 | ImVec2 windowSize = ImGui::GetWindowSize(); 702 | // Set the size of the pop-up to be proportional to the window size 703 | ImVec2 popupSize(windowSize.x * 0.5f, windowSize.y * 0.5f); 704 | ImGui::SetNextWindowSize(popupSize); 705 | } 706 | if (ImGui::BeginPopup("Edit Setting", ImGuiWindowFlags_AlwaysAutoResize)) { 707 | // Get the size of the current window 708 | show_entry_edit(entry, mod); 709 | ImGui::EndPopup(); 710 | } 711 | 712 | 713 | ImGui::SameLine(); 714 | 715 | // delete button 716 | ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); 717 | if (ImGui::Button("Delete")) { 718 | // delete entry 719 | ImGui::OpenPopup("Delete Confirmation"); 720 | // move popup mousepos 721 | ImVec2 mousePos = ImGui::GetMousePos(); 722 | ImGui::SetNextWindowPos(mousePos); 723 | } 724 | if (ImGui::BeginPopupModal("Delete Confirmation", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { 725 | ImGui::Text("Are you sure you want to delete this setting?"); 726 | ImGui::Separator(); 727 | 728 | if (ImGui::Button("Yes", ImVec2(120, 0))) { 729 | if (entry->type == entry_type::kEntryType_Checkbox) { 730 | setting_checkbox* checkbox = (setting_checkbox*)entry; 731 | m_checkbox_toggle.erase(checkbox->control_id); 732 | } 733 | bool should_decrement = it != entries.begin(); 734 | it = entries.erase(it); 735 | if (should_decrement) { 736 | it--; 737 | } 738 | if (it == entries.end()) { 739 | all_entries_deleted = true; 740 | } 741 | edited = true; 742 | delete entry; 743 | entry_deleted = true; 744 | // TODO: delete everything in a group if deleting group. 745 | ImGui::CloseCurrentPopup(); 746 | } 747 | ImGui::SetItemDefaultFocus(); 748 | ImGui::SameLine(); 749 | if (ImGui::Button("No", ImVec2(120, 0))) { 750 | ImGui::CloseCurrentPopup(); 751 | } 752 | ImGui::EndPopup(); 753 | } 754 | ImGui::PopStyleColor(); 755 | ImGui::SameLine(); 756 | 757 | } 758 | 759 | if (!entry_deleted) { 760 | show_entry(entry, mod); 761 | } 762 | 763 | ImGui::Unindent(); 764 | ImGui::PopID(); 765 | if (all_entries_deleted) { 766 | break; 767 | } 768 | } 769 | 770 | // add entry 771 | if (edit_mode) { 772 | if (ImGui::Button("New Entry")) { 773 | // correct popup position 774 | ImGui::SetNextWindowPos(ImGui::GetCursorScreenPos()); 775 | ImGui::OpenPopup("Add Entry"); 776 | } 777 | if (ImGui::BeginPopup("Add Entry")) { 778 | if (ImGui::Selectable("Checkbox")) { 779 | setting_checkbox* checkbox = new setting_checkbox(); 780 | entries.push_back(checkbox); 781 | edited = true; 782 | INFO("added entry"); 783 | } 784 | if (ImGui::Selectable("Slider")) { 785 | setting_slider* slider = new setting_slider(); 786 | entries.push_back(slider); 787 | edited = true; 788 | INFO("added entry"); 789 | } 790 | if (ImGui::Selectable("Textbox")) { 791 | setting_textbox* textbox = new setting_textbox(); 792 | entries.push_back(textbox); 793 | edited = true; 794 | INFO("added entry"); 795 | } 796 | if (ImGui::Selectable("Dropdown")) { 797 | setting_dropdown* dropdown = new setting_dropdown(); 798 | dropdown->options.push_back("Option 0"); 799 | dropdown->options.push_back("Option 1"); 800 | dropdown->options.push_back("Option 2"); 801 | entries.push_back(dropdown); 802 | edited = true; 803 | INFO("added entry"); 804 | } 805 | if (ImGui::Selectable("Keymap")) { 806 | setting_keymap* keymap = new setting_keymap(); 807 | entries.push_back(keymap); 808 | edited = true; 809 | INFO("added entry"); 810 | } 811 | if (ImGui::Selectable("Text")) { 812 | entry_text* text = new entry_text(); 813 | entries.push_back(text); 814 | edited = true; 815 | INFO("added entry"); 816 | } 817 | if (ImGui::Selectable("Group")) { 818 | entry_group* group = new entry_group(); 819 | entries.push_back(group); 820 | edited = true; 821 | INFO("added entry"); 822 | } 823 | if (ImGui::Selectable("Color")) { 824 | setting_color* color = new setting_color(); 825 | entries.push_back(color); 826 | edited = true; 827 | INFO("added entry"); 828 | } 829 | if (ImGui::Selectable("Button")) { 830 | entry_button* button = new entry_button(); 831 | entries.push_back(button); 832 | edited = true; 833 | INFO("added entry"); 834 | } 835 | ImGui::EndPopup(); 836 | } 837 | } 838 | if (edited) { 839 | json_dirty_mods.insert(mod); 840 | } 841 | ImGui::PopID(); 842 | } 843 | 844 | inline void ModSettings::SendSettingsUpdateEvent(std::string& modName) 845 | { 846 | auto eventSource = SKSE::GetModCallbackEventSource(); 847 | if (!eventSource) { 848 | return; 849 | } 850 | SKSE::ModCallbackEvent callbackEvent; 851 | callbackEvent.eventName = "dmenu_updateSettings"; 852 | callbackEvent.strArg = modName; 853 | eventSource->SendEvent(&callbackEvent); 854 | } 855 | 856 | void ModSettings::send_mod_callback_event(std::string& mod_name, std::string& str_arg) 857 | { 858 | auto eventSource = SKSE::GetModCallbackEventSource(); 859 | if (!eventSource) { 860 | return; 861 | } 862 | SKSE::ModCallbackEvent callbackEvent; 863 | callbackEvent.eventName = mod_name.data(); 864 | callbackEvent.strArg = str_arg.data(); 865 | eventSource->SendEvent(&callbackEvent); 866 | } 867 | 868 | 869 | 870 | void ModSettings::show_modSetting(mod_setting* mod) 871 | { 872 | 873 | show_entries(mod->entries, mod); 874 | 875 | } 876 | 877 | 878 | 879 | void ModSettings::submitInput(uint32_t id) 880 | { 881 | if (keyMapListening == nullptr) { 882 | return; 883 | } 884 | 885 | 886 | keyMapListening->value = id; 887 | keyMapListening = nullptr; 888 | } 889 | 890 | inline std::string ModSettings::get_type_str(entry_type t) 891 | { 892 | switch (t) { 893 | case entry_type::kEntryType_Checkbox: 894 | return "checkbox"; 895 | case entry_type::kEntryType_Slider: 896 | return "slider"; 897 | case entry_type::kEntryType_Textbox: 898 | return "textbox"; 899 | case entry_type::kEntryType_Dropdown: 900 | return "dropdown"; 901 | case entry_type::kEntryType_Text: 902 | return "text"; 903 | case entry_type::kEntryType_Group: 904 | return "group"; 905 | case entry_type::kEntryType_Color: 906 | return "color"; 907 | case entry_type::kEntryType_Keymap: 908 | return "keymap"; 909 | case entry_type::kEntryType_Button: 910 | return "button"; 911 | default: 912 | return "invalid"; 913 | } 914 | } 915 | 916 | void ModSettings::show() 917 | { 918 | 919 | // a button on the rhs of this same line 920 | ImGui::SameLine(ImGui::GetWindowWidth() - 100.0f); // Move cursor to the right side of the window 921 | ImGui::ToggleButton("Edit Config", &edit_mode); 922 | 923 | // Set window padding and item spacing 924 | const float padding = 8.0f; 925 | const float spacing = 8.0f; 926 | ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding, padding)); 927 | ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing)); 928 | 929 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); 930 | for (auto& mod : mods) { 931 | if (ImGui::CollapsingHeader(mod->name.c_str())) { 932 | show_modSetting(mod); 933 | } 934 | } 935 | ImGui::PopStyleColor(); 936 | 937 | ImGui::PopStyleVar(); 938 | 939 | show_buttons_window(); 940 | } 941 | 942 | static const std::string SETTINGS_DIR = "Data\\SKSE\\Plugins\\\dmenu\\customSettings"; 943 | void ModSettings::init() 944 | { 945 | // Load all mods from the "Mods" directory 946 | 947 | INFO("Loading .json configurations..."); 948 | namespace fs = std::filesystem; 949 | for (auto& file : std::filesystem::directory_iterator(SETTINGS_DIR)) { 950 | if (file.path().extension() == ".json") { 951 | load_json(file.path()); 952 | } 953 | } 954 | 955 | INFO("Loading .ini config serializations..."); 956 | // Read serialized settings from the .ini file for each mod 957 | for (auto& mod : mods) { 958 | load_ini(mod); 959 | insert_game_setting(mod); 960 | flush_game_setting(mod); 961 | } 962 | INFO("Mod settings initialized"); 963 | } 964 | 965 | ModSettings::entry_base* ModSettings::load_json_non_group(nlohmann::json& json) 966 | { 967 | entry_base* e = nullptr; 968 | std::string type_str = json["type"].get(); 969 | if (type_str == "checkbox") { 970 | setting_checkbox* scb = new setting_checkbox(); 971 | scb->value = json.contains("default") ? json["default"].get() : false; 972 | scb->default_value = scb->value; 973 | if (json.contains("control")) { 974 | if (json["control"].contains("id")) { 975 | scb->control_id = json["control"]["id"].get(); 976 | m_checkbox_toggle[scb->control_id] = scb; 977 | } 978 | } 979 | e = scb; 980 | } else if (type_str == "slider") { 981 | setting_slider* ssl = new setting_slider(); 982 | ssl->value = json.contains("default") ? json["default"].get() : 0; 983 | ssl->min = json["style"]["min"].get(); 984 | ssl->max = json["style"]["max"].get(); 985 | ssl->step = json["style"]["step"].get(); 986 | ssl->default_value = ssl->value; 987 | e = ssl; 988 | } else if (type_str == "textbox") { 989 | setting_textbox* stb = new setting_textbox(); 990 | size_t buf_size = json.contains("size") ? json["size"].get() : 64; 991 | stb->value = json.contains("default") ? json["default"].get() : ""; 992 | stb->default_value = stb->value; 993 | e = stb; 994 | } else if (type_str == "dropdown") { 995 | setting_dropdown* sdd = new setting_dropdown(); 996 | sdd->value = json.contains("default") ? json["default"].get() : 0; 997 | for (auto& option_json : json["options"]) { 998 | sdd->options.push_back(option_json.get()); 999 | } 1000 | sdd->default_value = sdd->value; 1001 | e = sdd; 1002 | } else if (type_str == "text") { 1003 | entry_text* et = new entry_text(); 1004 | if (json.contains("style") && json["style"].contains("color")) { 1005 | et->_color = ImVec4(json["style"]["color"]["r"].get(), json["style"]["color"]["g"].get(), json["style"]["color"]["b"].get(), json["style"]["color"]["a"].get()); 1006 | } 1007 | e = et; 1008 | } else if (type_str == "color") { 1009 | setting_color* sc = new setting_color(); 1010 | sc->default_color = ImVec4(json["default"]["r"].get(), json["default"]["g"].get(), json["default"]["b"].get(), json["default"]["a"].get()); 1011 | sc->color = sc->default_color; 1012 | e = sc; 1013 | } else if (type_str == "keymap") { 1014 | setting_keymap* skm = new setting_keymap(); 1015 | skm->default_value = json["default"].get(); 1016 | skm->value = skm->default_value; 1017 | e = skm; 1018 | } else if (type_str == "button") { 1019 | entry_button* sb = new entry_button(); 1020 | sb->id = json["id"].get(); 1021 | e = sb; 1022 | } else { 1023 | INFO("Unknown setting type: {}", type_str); 1024 | return nullptr; 1025 | } 1026 | 1027 | if (e->is_setting()) { 1028 | setting_base* s = dynamic_cast(e); 1029 | if (json.contains("gameSetting")) { 1030 | s->gameSetting = json["gameSetting"].get(); 1031 | } 1032 | s->ini_section = json["ini"]["section"].get(); 1033 | s->ini_id = json["ini"]["id"].get(); 1034 | } 1035 | return e; 1036 | 1037 | } 1038 | 1039 | ModSettings::entry_group* ModSettings::load_json_group(nlohmann::json& group_json) 1040 | { 1041 | entry_group* group = new entry_group(); 1042 | for (auto& entry_json : group_json["entries"]) { 1043 | entry_base* entry = load_json_entry(entry_json); 1044 | if (entry) { 1045 | group->entries.push_back(entry); 1046 | } 1047 | } 1048 | return group; 1049 | } 1050 | 1051 | ModSettings::entry_base* ModSettings::load_json_entry(nlohmann::json& entry_json) 1052 | { 1053 | ModSettings::entry_base* entry = nullptr; 1054 | if (entry_json["type"].get() == "group") { 1055 | entry = load_json_group(entry_json); 1056 | } else { 1057 | entry = load_json_non_group(entry_json); 1058 | } 1059 | if (entry == nullptr) { 1060 | INFO("ERROR: Failed to load json entry."); 1061 | return nullptr; 1062 | } 1063 | 1064 | entry->name.def = entry_json["text"]["name"].get(); 1065 | if (entry_json["text"].contains("desc")) { 1066 | entry->desc.def = entry_json["text"]["desc"].get(); 1067 | } 1068 | 1069 | if (entry_json.contains("translation")) { 1070 | if (entry_json["translation"].contains("name")) { 1071 | entry->name.key = entry_json["translation"]["name"].get(); 1072 | } 1073 | if (entry_json["translation"].contains("desc")) { 1074 | entry->desc.key = entry_json["translation"]["desc"].get(); 1075 | } 1076 | } 1077 | entry->control.failAction = entry_base::Control::kFailAction_Disable; 1078 | 1079 | if (entry_json.contains("control")) { 1080 | if (entry_json["control"].contains("requirements")) { 1081 | for (auto& req_json : entry_json["control"]["requirements"]) { 1082 | entry_base::Control::Req req; 1083 | req.id = req_json["id"].get(); 1084 | req._not = !req_json["value"].get(); 1085 | std::string req_type = req_json["type"].get(); 1086 | if (req_type == "checkbox") { 1087 | req.type = entry_base::Control::Req::ReqType::kReqType_Checkbox; 1088 | } else if (req_type == "gameSetting") { 1089 | req.type = entry_base::Control::Req::ReqType::kReqType_GameSetting; 1090 | } 1091 | else { 1092 | INFO("Error: unknown requirement type: {}", req_type); 1093 | continue; 1094 | } 1095 | entry->control.reqs.push_back(req); 1096 | } 1097 | } 1098 | if (entry_json["control"].contains("failAction")) { 1099 | std::string failAction = entry_json["control"]["failAction"].get(); 1100 | if (failAction == "disable") { 1101 | entry->control.failAction = entry_base::Control::kFailAction_Disable; 1102 | } else if (failAction == "hide") { 1103 | entry->control.failAction = entry_base::Control::kFailAction_Hide; 1104 | } 1105 | } 1106 | } 1107 | 1108 | return entry; 1109 | } 1110 | 1111 | void ModSettings::load_json(std::filesystem::path path) 1112 | { 1113 | std::string mod_path = path.string(); 1114 | // Load the JSON file for this mod 1115 | std::ifstream json_file(mod_path); 1116 | if (!json_file.is_open()) { 1117 | // Handle error opening file 1118 | return; 1119 | } 1120 | 1121 | // Parse the JSON file 1122 | nlohmann::json mod_json; 1123 | try { 1124 | json_file >> mod_json; 1125 | } catch (const nlohmann::json::exception& e) { 1126 | // Handle error parsing JSON 1127 | return; 1128 | } 1129 | // Create a mod_setting object to hold the settings for this mod 1130 | mod_setting* mod = new mod_setting(); 1131 | 1132 | // name is .json's name 1133 | mod->name = path.stem().string(); 1134 | mod->json_path = SETTINGS_DIR + "\\" + path.filename().string(); 1135 | try { 1136 | if (mod_json.contains("ini")) { 1137 | mod->ini_path = mod_json["ini"].get(); 1138 | } 1139 | else { 1140 | mod->ini_path = SETTINGS_DIR + "\\ini\\" + mod->name.data() + ".ini"; 1141 | } 1142 | 1143 | for (auto& entry_json : mod_json["data"]) { 1144 | entry_base* entry = load_json_entry(entry_json); 1145 | if (entry) { 1146 | mod->entries.push_back(entry); 1147 | } 1148 | } 1149 | // Add the mod to the list of mods 1150 | mods.push_back(mod); 1151 | } catch (const nlohmann::json::exception& e) { 1152 | // Handle error parsing JSON 1153 | INFO("Exception parsing {} : {}", mod_path, e.what()); 1154 | return; 1155 | } 1156 | INFO("Loaded mod {}", mod->name); 1157 | } 1158 | 1159 | void ModSettings::populate_non_group_json(entry_base* entry, nlohmann::json& json) 1160 | { 1161 | if (!entry->is_setting()) { 1162 | if (entry->type == entry_type::kEntryType_Text) { 1163 | json["style"]["color"]["r"] = dynamic_cast(entry)->_color.x; 1164 | json["style"]["color"]["g"] = dynamic_cast(entry)->_color.y; 1165 | json["style"]["color"]["b"] = dynamic_cast(entry)->_color.z; 1166 | json["style"]["color"]["a"] = dynamic_cast(entry)->_color.w; 1167 | } else if (entry->type == entry_type::kEntryType_Button) { 1168 | auto button = dynamic_cast(entry); 1169 | json["id"] = button->id; 1170 | } 1171 | return; // no need to continue, the following fields are only for settings 1172 | } 1173 | 1174 | setting_base* setting = dynamic_cast(entry); 1175 | json["ini"]["section"] = setting->ini_section; 1176 | json["ini"]["id"] = setting->ini_id; 1177 | if (setting->gameSetting != "") { 1178 | json["gameSetting"] = dynamic_cast(entry)->gameSetting; 1179 | } 1180 | if (setting->type == entry_type::kEntryType_Checkbox) { 1181 | auto cb_setting = dynamic_cast(entry); 1182 | json["default"] = cb_setting->default_value; 1183 | if (cb_setting->control_id != "") { 1184 | json["control"]["id"] = cb_setting->control_id; 1185 | } 1186 | } else if (setting->type == entry_type::kEntryType_Slider) { 1187 | auto slider_setting = dynamic_cast(entry); 1188 | json["default"] = slider_setting->default_value; 1189 | json["style"]["min"] = slider_setting->min; 1190 | json["style"]["max"] = slider_setting->max; 1191 | json["style"]["step"] = slider_setting->step; 1192 | 1193 | } else if (setting->type == entry_type::kEntryType_Textbox) { 1194 | auto textbox_setting = dynamic_cast(entry); 1195 | json["default"] = textbox_setting->default_value; 1196 | 1197 | } else if (setting->type == entry_type::kEntryType_Dropdown) { 1198 | auto dropdown_setting = dynamic_cast(entry); 1199 | json["default"] = dropdown_setting->default_value; 1200 | for (auto& option : dropdown_setting->options) { 1201 | json["options"].push_back(option); 1202 | } 1203 | } else if (setting->type == entry_type::kEntryType_Color) { 1204 | auto color_setting = dynamic_cast(entry); 1205 | json["default"]["r"] = color_setting->default_color.x; 1206 | json["default"]["g"] = color_setting->default_color.y; 1207 | json["default"]["b"] = color_setting->default_color.z; 1208 | json["default"]["a"] = color_setting->default_color.w; 1209 | } else if (setting->type == entry_type::kEntryType_Keymap) { 1210 | auto keymap_setting = dynamic_cast(entry); 1211 | json["default"] = keymap_setting->default_value; 1212 | } 1213 | 1214 | } 1215 | 1216 | void ModSettings::populate_group_json(entry_group* group, nlohmann::json& group_json) 1217 | { 1218 | for (auto& entry : group->entries) { 1219 | nlohmann::json entry_json; 1220 | populate_entry_json(entry, entry_json); 1221 | group_json["entries"].push_back(entry_json); 1222 | } 1223 | } 1224 | void ModSettings::populate_entry_json(entry_base* entry, nlohmann::json& entry_json) 1225 | { 1226 | // common fields for entry 1227 | entry_json["text"]["name"] = entry->name.def; 1228 | entry_json["text"]["desc"] = entry->desc.def; 1229 | entry_json["translation"]["name"] = entry->name.key; 1230 | entry_json["translation"]["desc"] = entry->desc.key; 1231 | entry_json["type"] = get_type_str(entry->type); 1232 | 1233 | auto control_json = entry_json["control"]; 1234 | 1235 | for (auto& req : entry->control.reqs) { 1236 | nlohmann::json req_json; 1237 | std::string req_type = ""; 1238 | switch (req.type) { 1239 | case entry_base::Control::Req::kReqType_Checkbox: 1240 | req_type = "checkbox"; 1241 | break; 1242 | case entry_base::Control::Req::kReqType_GameSetting: 1243 | req_type = "gameSetting"; 1244 | break; 1245 | default: 1246 | req_type = "ERRORTYPE"; 1247 | break; 1248 | } 1249 | req_json["type"] = req_type; 1250 | req_json["value"] = !req._not; 1251 | req_json["id"] = req.id; 1252 | control_json["requirements"].push_back(req_json); 1253 | } 1254 | switch (entry->control.failAction) { 1255 | case entry_base::Control::kFailAction_Disable: 1256 | control_json["failAction"] = "disable"; 1257 | break; 1258 | case entry_base::Control::kFailAction_Hide: 1259 | control_json["failAction"] = "hide"; 1260 | break; 1261 | default: 1262 | control_json["failAction"] = "ERROR"; 1263 | break; 1264 | } 1265 | entry_json["control"] = control_json; 1266 | if (entry->is_group()) { 1267 | populate_group_json(dynamic_cast(entry), entry_json); 1268 | } else { 1269 | populate_non_group_json(entry, entry_json); 1270 | } 1271 | } 1272 | /* Serialize config to .json*/ 1273 | void ModSettings::flush_json(mod_setting* mod) 1274 | { 1275 | nlohmann::json mod_json; 1276 | mod_json["name"] = mod->name; 1277 | mod_json["ini"] = mod->ini_path; 1278 | 1279 | nlohmann::json data_json; 1280 | 1281 | for (auto& entry : mod->entries) { 1282 | nlohmann:json entry_json; 1283 | populate_entry_json(entry, entry_json); 1284 | data_json.push_back(entry_json); 1285 | } 1286 | 1287 | 1288 | std::ofstream json_file(mod->json_path); 1289 | if (!json_file.is_open()) { 1290 | // Handle error opening file 1291 | INFO("error: failed to open {}", mod->json_path); 1292 | return; 1293 | } 1294 | 1295 | mod_json["data"] = data_json; 1296 | 1297 | try { 1298 | json_file << mod_json; 1299 | } catch (const nlohmann::json::exception& e) { 1300 | // Handle error parsing JSON 1301 | ERROR("Exception dumping {} : {}", mod->json_path, e.what()); 1302 | } 1303 | 1304 | insert_game_setting(mod); 1305 | flush_game_setting(mod); 1306 | 1307 | INFO("Saved config for {}", mod->name); 1308 | } 1309 | 1310 | void ModSettings::get_all_settings(mod_setting* mod, std::vector& r_vec) 1311 | { 1312 | std::stack group_stack; 1313 | for (auto& entry : mod->entries) { 1314 | if (entry->is_group()) { 1315 | group_stack.push(dynamic_cast(entry)); 1316 | } else if (entry->is_setting()) { 1317 | r_vec.push_back(dynamic_cast(entry)); 1318 | } 1319 | } 1320 | while (!group_stack.empty()) { // get all groups 1321 | auto group = group_stack.top(); 1322 | group_stack.pop(); 1323 | for (auto& entry : group->entries) { 1324 | if (entry->is_group()) { 1325 | group_stack.push(dynamic_cast(entry)); 1326 | } else if (entry->is_setting()) { 1327 | r_vec.push_back(dynamic_cast(entry)); 1328 | } 1329 | } 1330 | } 1331 | } 1332 | 1333 | void ModSettings::get_all_entries(mod_setting* mod, std::vector& r_vec) 1334 | { 1335 | std::stack group_stack; 1336 | for (auto& entry : mod->entries) { 1337 | if (entry->is_group()) { 1338 | group_stack.push(dynamic_cast(entry)); 1339 | } 1340 | r_vec.push_back(entry); 1341 | } 1342 | while (!group_stack.empty()) { // get all groups 1343 | auto group = group_stack.top(); 1344 | group_stack.pop(); 1345 | for (auto& entry : group->entries) { 1346 | if (entry->is_group()) { 1347 | group_stack.push(dynamic_cast(entry)); 1348 | } 1349 | r_vec.push_back(entry); 1350 | } 1351 | } 1352 | } 1353 | 1354 | 1355 | void ModSettings::load_ini(mod_setting* mod) 1356 | { 1357 | // Create the path to the ini file for this mod 1358 | INFO("loading .ini for {}", mod->name); 1359 | // Load the ini file 1360 | CSimpleIniA ini; 1361 | ini.SetUnicode(); 1362 | SI_Error rc = ini.LoadFile(mod->ini_path.c_str()); 1363 | if (rc != SI_OK) { 1364 | // Handle error loading file 1365 | INFO(".ini file for {} not found. Creating a new .ini file.", mod->name); 1366 | flush_ini(mod); 1367 | return; 1368 | } 1369 | 1370 | std::vector settings; 1371 | get_all_settings(mod, settings); 1372 | // Iterate through each setting in the group 1373 | for (auto& setting_ptr : settings) { 1374 | if (setting_ptr->ini_id.empty() || setting_ptr->ini_section.empty()) { 1375 | ERROR("Undefined .ini serialization for setting {}; failed to load value.", setting_ptr->name.def); 1376 | continue; 1377 | } 1378 | // Get the value of this setting from the ini file 1379 | std::string value; 1380 | bool use_default = false; 1381 | if (ini.KeyExists(setting_ptr->ini_section.c_str(), setting_ptr->ini_id.c_str())) { 1382 | value = ini.GetValue(setting_ptr->ini_section.c_str(), setting_ptr->ini_id.c_str(), ""); 1383 | } else { 1384 | INFO("Value not found for setting {} in .ini file. Using default entry value.", setting_ptr->name.def); 1385 | use_default = true; 1386 | } 1387 | if (use_default) { 1388 | // Convert the value to the appropriate type and assign it to the setting 1389 | if (setting_ptr->type == kEntryType_Checkbox) { 1390 | dynamic_cast(setting_ptr)->value = dynamic_cast(setting_ptr)->default_value; 1391 | } else if (setting_ptr->type == kEntryType_Slider) { 1392 | dynamic_cast(setting_ptr)->value = dynamic_cast(setting_ptr)->default_value; 1393 | } else if (setting_ptr->type == kEntryType_Textbox) { 1394 | dynamic_cast(setting_ptr)->value = dynamic_cast(setting_ptr)->default_value; 1395 | } else if (setting_ptr->type == kEntryType_Dropdown) { 1396 | dynamic_cast(setting_ptr)->value = dynamic_cast(setting_ptr)->default_value; 1397 | } else if (setting_ptr->type == kEntryType_Color) { 1398 | dynamic_cast(setting_ptr)->color.x = dynamic_cast(setting_ptr)->default_color.x; 1399 | dynamic_cast(setting_ptr)->color.y = dynamic_cast(setting_ptr)->default_color.y; 1400 | dynamic_cast(setting_ptr)->color.z = dynamic_cast(setting_ptr)->default_color.z; 1401 | dynamic_cast(setting_ptr)->color.w = dynamic_cast(setting_ptr)->default_color.w; 1402 | } else if (setting_ptr->type == kEntryType_Keymap) { 1403 | dynamic_cast(setting_ptr)->value = dynamic_cast(setting_ptr)->default_value; 1404 | } 1405 | } else { 1406 | // Convert the value to the appropriate type and assign it to the setting 1407 | if (setting_ptr->type == kEntryType_Checkbox) { 1408 | dynamic_cast(setting_ptr)->value = (value == "true"); 1409 | } else if (setting_ptr->type == kEntryType_Slider) { 1410 | dynamic_cast(setting_ptr)->value = std::stof(value); 1411 | } else if (setting_ptr->type == kEntryType_Textbox) { 1412 | dynamic_cast(setting_ptr)->value = value; 1413 | } else if (setting_ptr->type == kEntryType_Dropdown) { 1414 | dynamic_cast(setting_ptr)->value = std::stoi(value); 1415 | } else if (setting_ptr->type == kEntryType_Color) { 1416 | uint32_t colUInt = std::stoul(value); 1417 | dynamic_cast(setting_ptr)->color.x = ((colUInt >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f; 1418 | dynamic_cast(setting_ptr)->color.y = ((colUInt >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f; 1419 | dynamic_cast(setting_ptr)->color.z = ((colUInt >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f; 1420 | dynamic_cast(setting_ptr)->color.w = ((colUInt >> IM_COL32_A_SHIFT) & 0xFF) / 255.0f; 1421 | } else if (setting_ptr->type == kEntryType_Keymap) { 1422 | dynamic_cast(setting_ptr)->value = std::stoi(value); 1423 | } 1424 | } 1425 | 1426 | } 1427 | INFO(".ini loaded."); 1428 | } 1429 | 1430 | 1431 | 1432 | /* Flush changes to MOD into its .ini file*/ 1433 | void ModSettings::flush_ini(mod_setting* mod) 1434 | { 1435 | // Create a SimpleIni object to write to the ini file 1436 | CSimpleIniA ini; 1437 | ini.SetUnicode(); 1438 | 1439 | std::vector settings; 1440 | get_all_settings(mod, settings); 1441 | for (auto& setting : settings) { 1442 | if (setting->ini_id.empty() || setting->ini_section.empty()) { 1443 | ERROR("Undefined .ini serialization for setting {}; failed to save value.", setting->name.def); 1444 | continue; 1445 | } 1446 | // Get the value of this setting from the ini file 1447 | std::string value; 1448 | if (setting->type == kEntryType_Checkbox) { 1449 | value = dynamic_cast(setting)->value ? "true" : "false"; 1450 | } else if (setting->type == kEntryType_Slider) { 1451 | value = std::to_string(dynamic_cast(setting)->value); 1452 | } else if (setting->type == kEntryType_Textbox) { 1453 | value = dynamic_cast(setting)->value; 1454 | } else if (setting->type == kEntryType_Dropdown) { 1455 | value = std::to_string(dynamic_cast(setting)->value); 1456 | } 1457 | else if (setting->type == kEntryType_Color) { 1458 | // from imvec4 float to imu32 1459 | auto sc = dynamic_cast(setting); 1460 | // Step 1: Extract components 1461 | uint32_t r = sc->color.x * 255.0f; 1462 | uint32_t g = sc->color.y * 255.f; 1463 | uint32_t b = sc->color.z * 255.f; 1464 | uint32_t a = sc->color.w * 255.f; 1465 | ImU32 col = IM_COL32(r, g, b, a); 1466 | value = std::to_string(col); 1467 | } else if (setting->type == kEntryType_Keymap) { 1468 | value = std::to_string(dynamic_cast(setting)->value); 1469 | } 1470 | ini.SetValue(setting->ini_section.c_str(), setting->ini_id.c_str(), value.c_str()); 1471 | } 1472 | 1473 | // Save the ini file 1474 | ini.SaveFile(mod->ini_path.c_str()); 1475 | } 1476 | 1477 | 1478 | 1479 | 1480 | /* Flush changes to MOD's settings to game settings.*/ 1481 | void ModSettings::flush_game_setting(mod_setting* mod) 1482 | { 1483 | std::vector settings; 1484 | get_all_settings(mod, settings); 1485 | auto gsc = RE::GameSettingCollection::GetSingleton(); 1486 | if (!gsc) { 1487 | INFO("Game setting collection not found when trying to save game setting."); 1488 | return; 1489 | } 1490 | for (auto& setting : settings) { 1491 | if (setting->gameSetting.empty()) { 1492 | continue; 1493 | } 1494 | RE::Setting* s = gsc->GetSetting(setting->gameSetting.c_str()); 1495 | if (!s) { 1496 | INFO("Error: Game setting not found when trying to save game setting {} for setting {}", setting->gameSetting, setting->name.def); 1497 | return; 1498 | } 1499 | if (setting->type == kEntryType_Checkbox) { 1500 | s->SetBool(dynamic_cast(setting)->value); 1501 | } else if (setting->type == kEntryType_Slider) { 1502 | float val = dynamic_cast(setting)->value; 1503 | switch (s->GetType()) { 1504 | case RE::Setting::Type::kUnsignedInteger: 1505 | s->SetUnsignedInteger((uint32_t)val); 1506 | break; 1507 | case RE::Setting::Type::kInteger: 1508 | s->SetInteger(static_cast(val)); 1509 | break; 1510 | case RE::Setting::Type::kFloat: 1511 | s->SetFloat(val); 1512 | break; 1513 | default: 1514 | ERROR("Game setting variable for slider has bad type prefix. Prefix it with f(float), i(integer), or u(unsigned integer) to specify the game setting type."); 1515 | break; 1516 | } 1517 | } else if (setting->type == kEntryType_Textbox) { 1518 | s->SetString(dynamic_cast(setting)->value.c_str()); 1519 | } else if (setting->type == kEntryType_Dropdown) { 1520 | //s->SetUnsignedInteger(dynamic_cast(setting_ptr)->value); 1521 | s->SetInteger(dynamic_cast(setting)->value); 1522 | } else if (setting->type == kEntryType_Keymap) { 1523 | s->SetInteger(dynamic_cast(setting)->value); 1524 | } else if (setting->type == kEntryType_Color) { 1525 | setting_color* sc = dynamic_cast(setting); 1526 | uint32_t r = sc->color.x * 255.0f; 1527 | uint32_t g = sc->color.y * 255.f; 1528 | uint32_t b = sc->color.z * 255.f; 1529 | uint32_t a = sc->color.w * 255.f; 1530 | ImU32 col = IM_COL32(r, g, b, a); 1531 | s->SetUnsignedInteger(col); 1532 | } 1533 | } 1534 | } 1535 | 1536 | void ModSettings::insert_game_setting(mod_setting* mod) 1537 | { 1538 | std::vector entries; 1539 | get_all_entries(mod, entries); // must get all entries to inject settings for the entry's control requirements 1540 | auto gsc = RE::GameSettingCollection::GetSingleton(); 1541 | if (!gsc) { 1542 | INFO("Game setting collection not found when trying to insert game setting."); 1543 | return; 1544 | } 1545 | for (auto& entry : entries) { 1546 | if (entry->is_setting()) { 1547 | auto es = dynamic_cast(entry); 1548 | // insert setting setting 1549 | if (!es->gameSetting.empty()) { // gamesetting mapping 1550 | if (gsc->GetSetting(es->gameSetting.c_str())) { // setting already exists 1551 | return; 1552 | } 1553 | RE::Setting* s = new RE::Setting(es->gameSetting.c_str()); 1554 | gsc->InsertSetting(s); 1555 | if (!gsc->GetSetting(es->gameSetting.c_str())) { 1556 | INFO("ERROR: Failed to insert game setting."); 1557 | } 1558 | } 1559 | } 1560 | 1561 | // inject setting for setting's req 1562 | for (auto req : entry->control.reqs) { 1563 | if (req.type == entry_base::Control::Req::kReqType_GameSetting) { 1564 | if (!gsc->GetSetting(req.id.c_str())) { 1565 | RE::Setting* s = new RE::Setting(req.id.c_str()); 1566 | gsc->InsertSetting(s); 1567 | } 1568 | } 1569 | } 1570 | } 1571 | } 1572 | 1573 | void ModSettings::save_all_game_setting() 1574 | { 1575 | for (auto mod : mods) { 1576 | flush_game_setting(mod); 1577 | } 1578 | } 1579 | 1580 | void ModSettings::insert_all_game_setting() 1581 | { 1582 | for (auto mod : mods) { 1583 | insert_game_setting(mod); 1584 | } 1585 | } 1586 | // 1587 | //bool ModSettings::API_RegisterForSettingUpdate(std::string a_mod, std::function a_callback) 1588 | //{ 1589 | // INFO("Received registration for {} update.", a_mod); 1590 | // for (auto mod : mods) { 1591 | // if (mod->name == a_mod) { 1592 | // mod->callbacks.push_back(a_callback); 1593 | // INFO("Successfully added callback for {} update.", a_mod); 1594 | // return true; 1595 | // } 1596 | // } 1597 | // ERROR("{} mod not found.", a_mod); 1598 | // return false; 1599 | //} 1600 | // 1601 | // 1602 | // 1603 | //namespace Example 1604 | //{ 1605 | // bool RegisterForSettingUpdate(std::string a_mod, std::function a_callback) 1606 | // { 1607 | // static auto dMenu = GetModuleHandle("dmenu"); 1608 | // using _RegisterForSettingUpdate = bool (*)(std::string, std::function); 1609 | // static auto func = reinterpret_cast<_RegisterForSettingUpdate>(GetProcAddress(dMenu, "RegisterForSettingUpdate")); 1610 | // if (func) { 1611 | // return func(a_mod, a_callback); 1612 | // } 1613 | // return false; 1614 | // } 1615 | //} 1616 | // 1617 | //extern "C" DLLEXPORT bool RegisterForSettingUpdate(std::string a_mod, std::function a_callback) 1618 | //{ 1619 | // return ModSettings::API_RegisterForSettingUpdate(a_mod, a_callback); 1620 | //} 1621 | 1622 | const char* ModSettings::setting_keymap::keyid_to_str(int key_id) 1623 | { 1624 | switch (key_id) { 1625 | case 0: 1626 | return "Unmapped"; 1627 | case 1: 1628 | return "Escape"; 1629 | case 2: 1630 | return "1"; 1631 | case 3: 1632 | return "2"; 1633 | case 4: 1634 | return "3"; 1635 | case 5: 1636 | return "4"; 1637 | case 6: 1638 | return "5"; 1639 | case 7: 1640 | return "6"; 1641 | case 8: 1642 | return "7"; 1643 | case 9: 1644 | return "8"; 1645 | case 10: 1646 | return "9"; 1647 | case 11: 1648 | return "0"; 1649 | case 12: 1650 | return "Minus"; 1651 | case 13: 1652 | return "Equals"; 1653 | case 14: 1654 | return "Backspace"; 1655 | case 15: 1656 | return "Tab"; 1657 | case 16: 1658 | return "Q"; 1659 | case 17: 1660 | return "W"; 1661 | case 18: 1662 | return "E"; 1663 | case 19: 1664 | return "R"; 1665 | case 20: 1666 | return "T"; 1667 | case 21: 1668 | return "Y"; 1669 | case 22: 1670 | return "U"; 1671 | case 23: 1672 | return "I"; 1673 | case 24: 1674 | return "O"; 1675 | case 25: 1676 | return "P"; 1677 | case 26: 1678 | return "Left Bracket"; 1679 | case 27: 1680 | return "Right Bracket"; 1681 | case 28: 1682 | return "Enter"; 1683 | case 29: 1684 | return "Left Control"; 1685 | case 30: 1686 | return "A"; 1687 | case 31: 1688 | return "S"; 1689 | case 32: 1690 | return "D"; 1691 | case 33: 1692 | return "F"; 1693 | case 34: 1694 | return "G"; 1695 | case 35: 1696 | return "H"; 1697 | case 36: 1698 | return "J"; 1699 | case 37: 1700 | return "K"; 1701 | case 38: 1702 | return "L"; 1703 | case 39: 1704 | return "Semicolon"; 1705 | case 40: 1706 | return "Apostrophe"; 1707 | case 41: 1708 | return "~ (Console)"; 1709 | case 42: 1710 | return "Left Shift"; 1711 | case 43: 1712 | return "Back Slash"; 1713 | case 44: 1714 | return "Z"; 1715 | case 45: 1716 | return "X"; 1717 | case 46: 1718 | return "C"; 1719 | case 47: 1720 | return "V"; 1721 | case 48: 1722 | return "B"; 1723 | case 49: 1724 | return "N"; 1725 | case 50: 1726 | return "M"; 1727 | case 51: 1728 | return "Comma"; 1729 | case 52: 1730 | return "Period"; 1731 | case 53: 1732 | return "Forward Slash"; 1733 | case 54: 1734 | return "Right Shift"; 1735 | case 55: 1736 | return "NUM*"; 1737 | case 56: 1738 | return "Left Alt"; 1739 | case 57: 1740 | return "Spacebar"; 1741 | case 58: 1742 | return "Caps Lock"; 1743 | case 59: 1744 | return "F1"; 1745 | case 60: 1746 | return "F2"; 1747 | case 61: 1748 | return "F3"; 1749 | case 62: 1750 | return "F4"; 1751 | case 63: 1752 | return "F5"; 1753 | case 64: 1754 | return "F6"; 1755 | case 65: 1756 | return "F7"; 1757 | case 66: 1758 | return "F8"; 1759 | case 67: 1760 | return "F9"; 1761 | case 68: 1762 | return "F10"; 1763 | case 69: 1764 | return "Num Lock"; 1765 | case 70: 1766 | return "Scroll Lock"; 1767 | case 71: 1768 | return "NUM7"; 1769 | case 72: 1770 | return "NUM8"; 1771 | case 73: 1772 | return "NUM9"; 1773 | case 74: 1774 | return "NUM-"; 1775 | case 75: 1776 | return "NUM4"; 1777 | case 76: 1778 | return "NUM5"; 1779 | case 77: 1780 | return "NUM6"; 1781 | case 78: 1782 | return "NUM+"; 1783 | case 79: 1784 | return "NUM1"; 1785 | case 80: 1786 | return "NUM2"; 1787 | case 81: 1788 | return "NUM3"; 1789 | case 82: 1790 | return "NUM0"; 1791 | case 83: 1792 | return "NUM."; 1793 | case 87: 1794 | return "F11"; 1795 | case 88: 1796 | return "F12"; 1797 | case 156: 1798 | return "NUM Enter"; 1799 | case 157: 1800 | return "Right Control"; 1801 | case 181: 1802 | return "NUM/"; 1803 | case 183: 1804 | return "SysRq / PtrScr"; 1805 | case 184: 1806 | return "Right Alt"; 1807 | case 197: 1808 | return "Pause"; 1809 | case 199: 1810 | return "Home"; 1811 | case 200: 1812 | return "Up Arrow"; 1813 | case 201: 1814 | return "PgUp"; 1815 | case 203: 1816 | return "Left Arrow"; 1817 | case 205: 1818 | return "Right Arrow"; 1819 | case 207: 1820 | return "End"; 1821 | case 208: 1822 | return "Down Arrow"; 1823 | case 209: 1824 | return "PgDown"; 1825 | case 210: 1826 | return "Insert"; 1827 | case 211: 1828 | return "Delete"; 1829 | case 256: 1830 | return "Left Mouse Button"; 1831 | case 257: 1832 | return "Right Mouse Button"; 1833 | case 258: 1834 | return "Middle/Wheel Mouse Button"; 1835 | case 259: 1836 | return "Mouse Button 3"; 1837 | case 260: 1838 | return "Mouse Button 4"; 1839 | case 261: 1840 | return "Mouse Button 5"; 1841 | case 262: 1842 | return "Mouse Button 6"; 1843 | case 263: 1844 | return "Mouse Button 7"; 1845 | case 264: 1846 | return "Mouse Wheel Up"; 1847 | case 265: 1848 | return "Mouse Wheel Down"; 1849 | case 266: 1850 | return "DPAD_UP"; 1851 | case 267: 1852 | return "DPAD_DOWN"; 1853 | case 268: 1854 | return "DPAD_LEFT"; 1855 | case 269: 1856 | return "DPAD_RIGHT"; 1857 | case 270: 1858 | return "START"; 1859 | case 271: 1860 | return "BACK"; 1861 | case 272: 1862 | return "LEFT_THUMB"; 1863 | case 273: 1864 | return "RIGHT_THUMB"; 1865 | case 274: 1866 | return "LEFT_SHOULDER"; 1867 | case 275: 1868 | return "RIGHT_SHOULDER"; 1869 | case 276: 1870 | return "A"; 1871 | case 277: 1872 | return "B"; 1873 | case 278: 1874 | return "X"; 1875 | case 279: 1876 | return "Y"; 1877 | case 280: 1878 | return "LT"; 1879 | case 281: 1880 | return "RT"; 1881 | default: 1882 | return "Unknown Key"; 1883 | } 1884 | } 1885 | -------------------------------------------------------------------------------- /src/bin/menus/ModSettings.h: -------------------------------------------------------------------------------- 1 | #include "PCH.h" 2 | #include 3 | #include "Translator.h" 4 | #include "imgui.h" 5 | #include "nlohmann/json.hpp" 6 | 7 | class ModSettings 8 | { 9 | class setting_base; 10 | class setting_checkbox; 11 | class setting_slider; 12 | class setting_keymap; 13 | 14 | // checkboxs' control key -> checkbox 15 | static inline std::unordered_map m_checkbox_toggle; 16 | 17 | public: 18 | static inline setting_keymap* keyMapListening = nullptr; 19 | static void submitInput(uint32_t id); 20 | 21 | enum entry_type 22 | { 23 | kEntryType_Checkbox, 24 | kEntryType_Slider, 25 | kEntryType_Textbox, 26 | kEntryType_Dropdown, 27 | kEntryType_Text, 28 | kEntryType_Group, 29 | kEntryType_Keymap, 30 | kEntryType_Color, 31 | kEntryType_Button, 32 | kSettingType_Invalid 33 | }; 34 | 35 | static std::string get_type_str(entry_type t); 36 | 37 | class entry_base 38 | { 39 | 40 | public: 41 | class Control 42 | { 43 | public: 44 | class Req 45 | { 46 | public: 47 | enum ReqType 48 | { 49 | kReqType_Checkbox, 50 | kReqType_GameSetting 51 | }; 52 | ReqType type; 53 | std::string id; 54 | bool _not = false; // the requirement needs to be off for satisfied() to return true 55 | bool satisfied(); 56 | Req() 57 | { 58 | id = "New Requirement"; 59 | type = kReqType_Checkbox; 60 | } 61 | }; 62 | enum FailAction 63 | { 64 | kFailAction_Disable, 65 | kFailAction_Hide, 66 | }; 67 | FailAction failAction; 68 | std::vector reqs; 69 | bool satisfied(); 70 | }; 71 | 72 | entry_type type; 73 | Translatable name; 74 | Translatable desc; 75 | Control control; 76 | virtual bool is_setting() const { return false; } 77 | virtual ~entry_base() = default; 78 | virtual bool is_group() const { return false; } 79 | }; 80 | 81 | class entry_text : public entry_base 82 | { 83 | public: 84 | ImVec4 _color; 85 | 86 | entry_text() 87 | { 88 | type = kEntryType_Text; 89 | name = Translatable("New Text"); 90 | _color = ImVec4(1, 1, 1, 1); 91 | } 92 | 93 | }; 94 | 95 | 96 | class entry_group : public entry_base 97 | { 98 | public: 99 | std::vector entries; 100 | 101 | entry_group() 102 | { 103 | type = kEntryType_Group; 104 | name = Translatable("New Group"); 105 | } 106 | 107 | bool is_group() const override { return true; } 108 | }; 109 | 110 | 111 | class setting_base : public entry_base 112 | { 113 | public: 114 | std::string ini_section; 115 | std::string ini_id; 116 | 117 | std::string gameSetting; 118 | bool is_setting() const override { return true; } 119 | virtual ~setting_base() = default; 120 | virtual bool reset() { return false; }; 121 | }; 122 | 123 | class setting_checkbox : public setting_base 124 | { 125 | public: 126 | setting_checkbox() 127 | { 128 | type = kEntryType_Checkbox; 129 | name = Translatable("New Checkbox"); 130 | value = true; 131 | default_value = true; 132 | } 133 | bool value; 134 | bool default_value; 135 | std::string control_id; 136 | bool reset() override 137 | { 138 | bool changed = value != default_value; 139 | value = default_value; 140 | return changed; 141 | } 142 | }; 143 | 144 | class setting_slider : public setting_base 145 | { 146 | public: 147 | setting_slider() 148 | { 149 | type = kEntryType_Slider; 150 | name = Translatable("New Slider"); 151 | value = 0.0f; 152 | min = 0.0f; 153 | max = 1.0f; 154 | step = 0.1f; 155 | default_value = 0.f; 156 | } 157 | float value; 158 | float min; 159 | float max; 160 | float step; 161 | float default_value; 162 | uint8_t precision = 2; // number of decimal places 163 | bool reset() override 164 | { 165 | bool changed = value != default_value; 166 | value = default_value; 167 | return changed; 168 | } 169 | }; 170 | 171 | class setting_textbox : public setting_base 172 | { 173 | public: 174 | std::string value; 175 | char* buf; 176 | std::string default_value; 177 | setting_textbox() 178 | { 179 | type = kEntryType_Textbox; 180 | name = Translatable("New Textbox"); 181 | value = ""; 182 | default_value = ""; 183 | } 184 | bool reset() override 185 | { 186 | bool changed = value != default_value; 187 | value = default_value; 188 | return changed; 189 | } 190 | }; 191 | 192 | class setting_dropdown : public setting_base 193 | { 194 | public: 195 | setting_dropdown() 196 | { 197 | type = kEntryType_Dropdown; 198 | name = Translatable("New Dropdown"); 199 | value = 0; 200 | default_value = 0; 201 | } 202 | std::vector options; 203 | int value; // index into options 204 | int default_value; 205 | bool reset() override 206 | { 207 | bool changed = value != default_value; 208 | value = default_value; 209 | return changed; 210 | } 211 | }; 212 | 213 | class setting_color : public setting_base 214 | { 215 | public: 216 | setting_color() { 217 | type = kEntryType_Color; 218 | name = Translatable("New Color"); 219 | color = { 0.f, 220 | 0.f, 221 | 0.f, 222 | 1.f }; 223 | } 224 | bool reset() override 225 | { 226 | bool changed = color.x != default_color.x || color.y != default_color.y || color.z != default_color.z || color.w != default_color.w; 227 | color = default_color; 228 | return changed; 229 | } 230 | ImVec4 color; 231 | ImVec4 default_color; 232 | }; 233 | 234 | class setting_keymap : public setting_base 235 | { 236 | public: 237 | setting_keymap() { 238 | type = kEntryType_Keymap; 239 | name = Translatable("New Keymap"); 240 | value = 0; 241 | default_value = 0; 242 | } 243 | int value; 244 | int default_value; 245 | static const char* keyid_to_str(int key_id); 246 | }; 247 | 248 | class entry_button : public entry_base 249 | { 250 | public: 251 | entry_button() 252 | { 253 | type = kEntryType_Button; 254 | name = Translatable("New Button"); 255 | id = ""; 256 | } 257 | std::string id; 258 | bool is_setting() const override { 259 | return false; 260 | } 261 | }; 262 | 263 | /* Settings of one mod, represented by one .json file and serialized to one .ini file.*/ 264 | class mod_setting 265 | { 266 | public: 267 | std::string name; 268 | std::vector entries; 269 | std::string ini_path; 270 | std::string json_path; 271 | 272 | std::vector> callbacks; 273 | }; 274 | 275 | /* Settings of all mods*/ 276 | static inline std::vector mods; 277 | 278 | static inline std::unordered_set json_dirty_mods; // mods whose changes need to be flushed to .json file. i.e. author has changed its setting 279 | static inline std::unordered_set ini_dirty_mods; // mods whose changes need to be flushed to .ini or gamesetting. i.e. user has changed its setting 280 | 281 | public: 282 | 283 | static void show(); // called by imgui per tick 284 | 285 | /* Load settings config from .json files and saved settings from .ini files*/ 286 | static void init(); 287 | 288 | private: 289 | /* Load a single mod from .json file*/ 290 | 291 | /* Read everything in group_json and populate entries*/ 292 | static entry_base* load_json_non_group(nlohmann::json& json); 293 | static entry_group* load_json_group(nlohmann::json& group_json); 294 | static entry_base* load_json_entry(nlohmann::json& json); 295 | static void load_json(std::filesystem::path a_path); 296 | 297 | static void populate_non_group_json(entry_base* group, nlohmann::json& group_json); 298 | static void populate_group_json(entry_group* group, nlohmann::json& group_json); 299 | static void populate_entry_json(entry_base* entry, nlohmann::json& entry_json); 300 | 301 | static void flush_json(mod_setting* mod); 302 | 303 | static void get_all_settings(mod_setting* mod, std::vector& r_vec); 304 | static void get_all_entries(mod_setting* mod, std::vector& r_vec); 305 | 306 | 307 | static void load_ini(mod_setting* mod); 308 | static void flush_ini(mod_setting* mod); 309 | 310 | static void flush_game_setting(mod_setting* mod); 311 | 312 | static void insert_game_setting(mod_setting* mod); 313 | 314 | public: 315 | static void save_all_game_setting(); 316 | static void insert_all_game_setting(); 317 | 318 | public: 319 | static bool API_RegisterForSettingUpdate(std::string a_mod, std::function a_callback) = delete; 320 | 321 | static void SendAllSettingsUpdateEvent(); 322 | 323 | private: 324 | static void show_reloadTranslationButton(); 325 | static void show_saveButton(); 326 | static void show_cancelButton(); 327 | static void show_saveJsonButton(); 328 | 329 | static void show_buttons_window(); 330 | 331 | 332 | static void show_modSetting(mod_setting* mod); 333 | static void show_entry_edit(entry_base* base, mod_setting* mod); 334 | static void show_entry(entry_base* base, mod_setting* mod); 335 | static void show_entries(std::vector& entries, mod_setting* mod); 336 | 337 | static void SendSettingsUpdateEvent(std::string& modName); 338 | static void send_mod_callback_event(std::string& mod_name, std::string& str_arg); 339 | 340 | 341 | static inline bool edit_mode = false; 342 | }; 343 | -------------------------------------------------------------------------------- /src/bin/menus/Settings.cpp: -------------------------------------------------------------------------------- 1 | #include "imgui.h" 2 | #include "imgui_internal.h" 3 | #include 4 | #include 5 | 6 | #include "Settings.h" 7 | #include "bin/Utils.h" 8 | 9 | #define SETTINGFILE_PATH "Data\\SKSE\\Plugins\\dmenu\\dmenu.ini" 10 | 11 | namespace ini 12 | { 13 | 14 | void flush() 15 | { 16 | settingsLoader loader(SETTINGFILE_PATH); 17 | loader.setActiveSection("UI"); 18 | loader.save(Settings::relative_window_size_h, "relative_window_size_h"); 19 | loader.save(Settings::relative_window_size_v, "relative_window_size_v"); 20 | loader.save(Settings::windowPos_x, "windowPos_x"); 21 | loader.save(Settings::windowPos_y, "windowPos_y"); 22 | loader.save(Settings::lockWindowSize, "lockWindowSize"); 23 | loader.save(Settings::lockWindowPos, "lockWindowPos"); 24 | loader.save(Settings::key_toggle_dmenu, "key_toggle_dmenu"); 25 | loader.save(Settings::fontScale, "fontScale"); 26 | 27 | 28 | loader.flush(); 29 | } 30 | 31 | void load() 32 | { 33 | settingsLoader loader(SETTINGFILE_PATH); 34 | loader.setActiveSection("UI"); 35 | loader.load(Settings::relative_window_size_h, "relative_window_size_h"); 36 | loader.load(Settings::relative_window_size_v, "relative_window_size_v"); 37 | loader.load(Settings::windowPos_x, "windowPos_x"); 38 | loader.load(Settings::windowPos_y, "windowPos_y"); 39 | loader.load(Settings::lockWindowSize, "lockWindowSize"); 40 | loader.load(Settings::lockWindowPos, "lockWindowPos"); 41 | loader.load(Settings::key_toggle_dmenu, "key_toggle_dmenu"); 42 | loader.load(Settings::fontScale, "fontScale"); 43 | 44 | 45 | } 46 | 47 | void init() 48 | { 49 | load(); 50 | } 51 | } 52 | 53 | namespace UI 54 | { 55 | void init() 56 | { 57 | } 58 | 59 | void show() 60 | { 61 | ImVec2 parentSize = ImGui::GetMainViewport()->Size; 62 | 63 | // Calculate the relative sizes 64 | Settings::relative_window_size_h = ImGui::GetWindowWidth() / parentSize.x; 65 | Settings::relative_window_size_v = ImGui::GetWindowHeight() / parentSize.y; 66 | 67 | auto windowPos = ImGui::GetWindowPos(); 68 | // get windowpos 69 | Settings::windowPos_x = windowPos.x; 70 | Settings::windowPos_y = windowPos.y; 71 | 72 | // Display the relative sizes in real-time 73 | ImGui::Text("Width: %.2f%%", Settings::relative_window_size_h * 100.0f); 74 | ImGui::SameLine(); 75 | ImGui::Text("Height: %.2f%%", Settings::relative_window_size_v * 100.0f); 76 | 77 | //ImGui::SameLine(ImGui::GetWindowWidth() - 100); 78 | ImGui::Checkbox("Lock Size", &Settings::lockWindowSize); 79 | //ImGui::PopStyleVar(); 80 | 81 | // Display the relative positions in real-time 82 | ImGui::Text("X pos: %f", Settings::windowPos_x); 83 | ImGui::SameLine(); 84 | ImGui::Text("Y pos: %f", Settings::windowPos_y); 85 | 86 | //ImGui::SameLine(ImGui::GetWindowWidth() - 100); 87 | ImGui::Checkbox("Lock Pos", &Settings::lockWindowPos); 88 | //ImGui::PopStyleVar(); 89 | 90 | if (ImGui::SliderFloat("Font Size", &Settings::fontScale, 0.5f, 2.f)) { 91 | ImGui::GetIO().FontGlobalScale = Settings::fontScale; 92 | } 93 | 94 | ImGui::InputInt("Toggle Key", (int*)&Settings::key_toggle_dmenu); 95 | } 96 | } 97 | 98 | 99 | void Settings::show() 100 | { 101 | // Use consistent padding and alignment 102 | ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10, 10)); 103 | ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(5, 5)); 104 | 105 | // Indent the contents of the window 106 | ImGui::Indent(); 107 | ImGui::Spacing(); 108 | 109 | // Display size controls 110 | ImGui::Text("UI"); 111 | UI::show(); 112 | ImGui::Spacing(); 113 | ImGui::Separator(); 114 | ImGui::Spacing(); 115 | 116 | // Unindent the contents of the window 117 | ImGui::Unindent(); 118 | 119 | // Position the "Save" button at the bottom-right corner of the window 120 | ImGui::SameLine(ImGui::GetWindowWidth() - 100); 121 | ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10); 122 | 123 | // Set the button background and text colors 124 | ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 1.0f)); 125 | ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.3f, 1.0f)); 126 | ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.4f, 0.4f, 0.4f, 1.0f)); 127 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); 128 | 129 | 130 | if (ImGui::Button("Save")) { 131 | ini::flush(); 132 | } 133 | 134 | // Restore the previous style colors 135 | ImGui::PopStyleColor(4); 136 | 137 | // Use consistent padding and alignment 138 | ImGui::PopStyleVar(2); 139 | } 140 | 141 | void Settings::init() 142 | { 143 | ini::init(); // load all settings first 144 | UI::init(); 145 | 146 | INFO("Settings initialized."); 147 | } 148 | -------------------------------------------------------------------------------- /src/bin/menus/Settings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Translator.h" 3 | class Settings 4 | { 5 | public: 6 | static inline float relative_window_size_h = 1.f; 7 | static inline float relative_window_size_v = 1.f; 8 | static inline float windowPos_x = 0.f; 9 | static inline float windowPos_y = 0.f; 10 | 11 | static inline bool lockWindowPos = false; 12 | static inline bool lockWindowSize = false; 13 | 14 | static inline float fontScale = 1.f; 15 | 16 | static inline uint32_t key_toggle_dmenu = 199; 17 | 18 | static void show(); 19 | 20 | static void init(); 21 | }; 22 | -------------------------------------------------------------------------------- /src/bin/menus/Trainer.cpp: -------------------------------------------------------------------------------- 1 | #include "imgui.h" 2 | #include "imgui_internal.h" 3 | #include 4 | #include 5 | 6 | #include "bin/Utils.h" 7 | #include "Trainer.h" 8 | 9 | namespace World 10 | { 11 | namespace Time 12 | { 13 | bool _sliderActive = false; 14 | void show() 15 | { 16 | auto calendar = RE::Calendar::GetSingleton(); 17 | float* ptr = nullptr; 18 | if (calendar) { 19 | ptr = &(calendar->gameHour->value); 20 | if (ptr) { 21 | ImGui::SliderFloat("Time", ptr, 0.0f, 24.f); 22 | _sliderActive = ImGui::IsItemActive(); 23 | return; 24 | } 25 | } 26 | ImGui::Text("Time address not found"); 27 | } 28 | 29 | } 30 | 31 | namespace Weather 32 | { 33 | RE::TESRegion* _currRegionCache = nullptr; // after fw `region` field of sky gets reset. restore from this. 34 | RE::TESRegion* getCurrentRegion() 35 | { 36 | if (RE::Sky::GetSingleton()) { 37 | auto ret = RE::Sky::GetSingleton()->region; 38 | if (ret) { 39 | _currRegionCache = ret; 40 | return ret; 41 | } 42 | } 43 | return _currRegionCache; 44 | } 45 | // maps to store formEditorID as names for regions and weathers as they're discarded by bethesda 46 | std::unordered_map _regionNames; 47 | std::unordered_map _weatherNames; 48 | 49 | std::vector _weathersToSelect; 50 | static std::vector _mods; // plugins containing weathers 51 | static uint8_t _mods_i = 0; // selected weather plugin 52 | bool _cached = false; 53 | 54 | bool _showCurrRegionOnly = true; // only show weather corresponding to current region 55 | 56 | bool _lockWeather = false; 57 | 58 | static std::vector> _filters = { 59 | { "Pleasant", false }, 60 | { "Cloudy", false }, 61 | { "Rainy", false }, 62 | { "Snow", false }, 63 | { "Permanent aurora", false }, 64 | { "Aurora follows sun", false } 65 | }; 66 | 67 | void cache() 68 | { 69 | _weathersToSelect.clear(); 70 | auto regionDataManager = RE::TESDataHandler::GetSingleton()->regionDataManager; 71 | if (!regionDataManager) { 72 | return; 73 | } 74 | for (auto weather : RE::TESDataHandler::GetSingleton()->GetFormArray()) { 75 | if (!weather) { 76 | continue; 77 | } 78 | auto flags = weather->data.flags; 79 | if (_filters[0].second) { 80 | if (!flags.any(RE::TESWeather::WeatherDataFlag::kPleasant)) { 81 | continue; 82 | } 83 | } 84 | if (_filters[1].second) { 85 | if (!flags.any(RE::TESWeather::WeatherDataFlag::kCloudy)) { 86 | continue; 87 | } 88 | } 89 | if (_filters[2].second) { 90 | if (!flags.any(RE::TESWeather::WeatherDataFlag::kRainy)) { 91 | continue; 92 | } 93 | } 94 | if (_filters[3].second) { 95 | if (!flags.any(RE::TESWeather::WeatherDataFlag::kSnow)) { 96 | continue; 97 | } 98 | } 99 | if (_filters[4].second) { 100 | if (!flags.any(RE::TESWeather::WeatherDataFlag::kPermAurora)) { 101 | continue; 102 | } 103 | } 104 | if (_filters[5].second) { 105 | if (!flags.any(RE::TESWeather::WeatherDataFlag::kAuroraFollowsSun)) { 106 | continue; 107 | } 108 | } 109 | if (_showCurrRegionOnly) { 110 | bool belongsToCurrRegion = false; 111 | auto currRegion = getCurrentRegion(); 112 | if (currRegion) { // could've cached regionData -> weathers at the cost of a bit of extra space, but this runs fine in O(n2) since there's limited # of weathers 113 | for (RE::TESRegionData* regionData : currRegion->dataList->regionDataList) { 114 | if (regionData->GetType() == RE::TESRegionData::Type::kWeather) { 115 | RE::TESRegionDataWeather* weatherData = regionDataManager->AsRegionDataWeather(regionData); 116 | for (auto t : weatherData->weatherTypes) { 117 | if (t->weather == weather) { 118 | belongsToCurrRegion = true; 119 | } 120 | } 121 | } 122 | } 123 | } 124 | if (!belongsToCurrRegion) { 125 | continue; 126 | } 127 | } 128 | _weathersToSelect.push_back(weather); 129 | } 130 | } 131 | 132 | 133 | void show() 134 | { 135 | RE::TESRegion* currRegion = nullptr; 136 | RE::TESWeather* currWeather = nullptr; 137 | if (RE::Sky::GetSingleton()) { 138 | currRegion = getCurrentRegion(); 139 | currWeather = RE::Sky::GetSingleton()->currentWeather; 140 | } 141 | 142 | // List of weathers to choose 143 | if (ImGui::BeginCombo("##Weathers", _weatherNames[currWeather].c_str())) { 144 | for (int i = 0; i < _weathersToSelect.size(); i++) { 145 | bool isSelected = (_weathersToSelect[i] == currWeather); 146 | if (ImGui::Selectable(_weatherNames[_weathersToSelect[i]].c_str(), isSelected)) { 147 | if (!isSelected) { 148 | auto sky = RE::Sky::GetSingleton(); 149 | sky->ForceWeather(_weathersToSelect[i], true); 150 | } 151 | } 152 | if (isSelected) { 153 | ImGui::SetItemDefaultFocus(); 154 | } 155 | } 156 | ImGui::EndCombo(); 157 | } 158 | //ImGui::SameLine(); 159 | //ImGui::Checkbox("Lock Weather", &_lockWeather); 160 | 161 | // Display filtering controls 162 | ImGui::Text("Flags:"); 163 | 164 | for (int i = 0; i < _filters.size(); i++) { 165 | if (ImGui::Checkbox(_filters[i].first.c_str(), &_filters[i].second)) { 166 | _cached = false; 167 | } 168 | if (i < _filters.size() - 1) { 169 | ImGui::SameLine(); 170 | } 171 | } 172 | 173 | // Display current region 174 | if (currRegion) { 175 | ImGui::Text("Current Region:"); 176 | ImGui::SameLine(); 177 | ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s", _regionNames[currRegion]); 178 | } else { 179 | ImGui::Text("Current region not found"); 180 | } 181 | 182 | ImGui::SameLine(); 183 | 184 | ImGui::Checkbox("Current Region Only", &_showCurrRegionOnly); 185 | if (_showCurrRegionOnly) { 186 | ImGui::SameLine(); 187 | ImGui::TextDisabled("(?!)"); 188 | if (ImGui::IsItemHovered()) { 189 | ImGui::BeginTooltip(); 190 | ImGui::Text("Only display weather suitable for the current region."); 191 | ImGui::EndTooltip(); 192 | } 193 | } 194 | 195 | if (!_cached) { 196 | cache(); 197 | } 198 | } 199 | 200 | void init() 201 | { 202 | _cached = false; 203 | // load list of plugins with available weathers 204 | std::unordered_set plugins; 205 | Utils::loadUsefulPlugins(plugins); 206 | for (auto plugin : plugins) { 207 | _mods.push_back(plugin); 208 | } 209 | 210 | auto data = RE::TESDataHandler::GetSingleton(); 211 | // load region&weather names 212 | for (RE::TESWeather* weather : data->GetFormArray()) { 213 | _weatherNames.insert({ weather, weather->GetFormEditorID() }); 214 | } 215 | for (RE::TESRegion* region : data->GetFormArray()) { 216 | _regionNames.insert({ region, region->GetFormEditorID() }); 217 | } 218 | } 219 | } 220 | 221 | void show() 222 | { 223 | if (!RE::Sky::GetSingleton() || !RE::Sky::GetSingleton()->currentWeather) { 224 | ImGui::Text("World not loaded"); 225 | return; 226 | } 227 | // Use consistent padding and alignment 228 | ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10, 10)); 229 | ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(5, 5)); 230 | 231 | // Indent the contents of the window 232 | ImGui::Indent(); 233 | ImGui::Spacing(); 234 | 235 | // Display time controls 236 | ImGui::Text("Time:"); 237 | Time::show(); 238 | ImGui::Spacing(); 239 | ImGui::Separator(); 240 | ImGui::Spacing(); 241 | 242 | // Display weather controls 243 | ImGui::Text("Weather:"); 244 | Weather::show(); 245 | 246 | // Unindent the contents of the window 247 | ImGui::Unindent(); 248 | 249 | // Use consistent padding and alignment 250 | ImGui::PopStyleVar(2); 251 | } 252 | 253 | void init() 254 | { 255 | Weather::init(); 256 | } 257 | } 258 | 259 | 260 | void Trainer::show() 261 | { 262 | ImGui::PushID("World"); 263 | if (ImGui::CollapsingHeader("World")) { 264 | World::show(); 265 | } 266 | ImGui::PopID(); 267 | } 268 | 269 | void Trainer::init() 270 | { 271 | // Weather 272 | World::init(); 273 | 274 | 275 | INFO("Trainer initialized."); 276 | } 277 | 278 | bool Trainer::isWeatherLocked() 279 | { 280 | return World::Time::_sliderActive; 281 | } 282 | 283 | 284 | // deprecated code 285 | //if (ImGui::BeginCombo("WeatherMod", _mods[_mods_i]->GetFilename().data())) { 286 | // for (int i = 0; i < _mods.size(); i++) { 287 | // bool isSelected = (_mods[_mods_i] == _mods[i]); 288 | // if (ImGui::Selectable(_mods[i]->GetFilename().data(), isSelected)) { 289 | // _mods_i = i; 290 | // _cached = false; 291 | // } 292 | // if (isSelected) { 293 | // ImGui::SetItemDefaultFocus(); 294 | // } 295 | // } 296 | // ImGui::EndCombo(); 297 | //} 298 | -------------------------------------------------------------------------------- /src/bin/menus/Trainer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | class Trainer 3 | { 4 | public: 5 | static void show(); 6 | 7 | static void init(); 8 | 9 | static bool isWeatherLocked(); 10 | }; 11 | -------------------------------------------------------------------------------- /src/bin/menus/Translator.cpp: -------------------------------------------------------------------------------- 1 | #include "Translator.h" 2 | 3 | void Translator::ReLoadTranslations() 4 | { 5 | _translations.clear(); 6 | } 7 | 8 | const char* Translator::Translate(std::string id) 9 | { 10 | auto it = _translations.find(id); 11 | if (it != _translations.end()) { 12 | return it->second.first ? it->second.second.c_str() : nullptr; 13 | } else { 14 | std::string res = ""; 15 | SKSE::Translation::Translate(id, res); 16 | bool hasTranslation = !res.empty(); 17 | _translations[id] = { hasTranslation, res }; 18 | return hasTranslation ? res.c_str() : nullptr; 19 | } 20 | } 21 | 22 | 23 | Translatable::Translatable(std::string def, std::string key) 24 | { 25 | this->def = def; 26 | this->key = key; 27 | } 28 | 29 | Translatable::Translatable(std::string def) 30 | { 31 | this->def = def; 32 | this->key = ""; 33 | } 34 | Translatable::Translatable() 35 | { 36 | this->def = ""; 37 | this->key = ""; 38 | } 39 | 40 | const char* Translatable::get() const 41 | { 42 | const char* ret = Translator::Translate(key); 43 | return ret ? ret : def.c_str(); 44 | } 45 | 46 | bool Translatable::empty() 47 | { 48 | return def.empty() && key.empty(); 49 | } 50 | -------------------------------------------------------------------------------- /src/bin/menus/Translator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class Translator 4 | { 5 | public: 6 | static void ReLoadTranslations(); 7 | 8 | // translate a string to the current language. Returns a null-pointer if the matching translation is not found. 9 | static const char* Translate(std::string id); 10 | 11 | private: 12 | static inline std::unordered_map> _translations; // id -> {has_translation, translated text} 13 | }; 14 | 15 | 16 | struct Translatable 17 | { 18 | Translatable(); 19 | Translatable(std::string def, std::string key); 20 | Translatable(std::string def); 21 | std::string def; // default string 22 | std::string key; // key to look up in the translation file 23 | const char* get() const; // get the translated string, or the default if no translation is found note the ptr points to the original data. 24 | bool empty(); 25 | }; 26 | -------------------------------------------------------------------------------- /src/include/Utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #define CONSOLELOG(msg) RE::ConsoleLog::GetSingleton()->Print(msg); 6 | #define PI 3.1415926535897932384626 7 | //TODO:clear this up a bit 8 | namespace RE 9 | { 10 | enum DIFFICULTY 11 | { 12 | kNovice = 0, 13 | kApprentice = 1, 14 | kAdept = 2, 15 | kExpert = 3, 16 | kMaster = 4, 17 | kLegendary = 5 18 | }; 19 | }; 20 | namespace inlineUtils 21 | { 22 | inline static bool isEquippedShield(RE::Actor* a_actor) { 23 | return RE::Offset::getEquippedShield(a_actor) != nullptr; 24 | } 25 | 26 | /*Send the target flying based on causer's location. 27 | @param magnitude: strength of a push.*/ 28 | inline static void PushActorAway(RE::Actor* causer, RE::Actor* target, float magnitude) 29 | { 30 | auto targetPoint = causer->GetNodeByName(causer->race->bodyPartData->parts[0]->targetName.c_str()); 31 | RE::NiPoint3 vec = targetPoint->world.translate; 32 | //RE::NiPoint3 vec = causer->GetPosition(); 33 | RE::Offset::pushActorAway(causer->currentProcess, target, vec, magnitude); 34 | } 35 | 36 | inline void SetRotationMatrix(RE::NiMatrix3& a_matrix, float sacb, float cacb, float sb) 37 | { 38 | float cb = std::sqrtf(1 - sb * sb); 39 | float ca = cacb / cb; 40 | float sa = sacb / cb; 41 | a_matrix.entry[0][0] = ca; 42 | a_matrix.entry[0][1] = -sacb; 43 | a_matrix.entry[0][2] = sa * sb; 44 | a_matrix.entry[1][0] = sa; 45 | a_matrix.entry[1][1] = cacb; 46 | a_matrix.entry[1][2] = -ca * sb; 47 | a_matrix.entry[2][0] = 0.0; 48 | a_matrix.entry[2][1] = sb; 49 | a_matrix.entry[2][2] = cb; 50 | } 51 | 52 | template 53 | Iter select_randomly(Iter start, Iter end, RandomGenerator& g) { 54 | std::uniform_int_distribution<> dis(0, std::distance(start, end) - 1); 55 | std::advance(start, dis(g)); 56 | return start; 57 | } 58 | 59 | template 60 | Iter select_randomly(Iter start, Iter end) { 61 | static std::random_device rd; 62 | static std::mt19937 gen(rd()); 63 | return select_randomly(start, end, gen); 64 | } 65 | 66 | 67 | inline bool isPowerAttacking(RE::Actor* a_actor) { 68 | auto currentProcess = a_actor->currentProcess; 69 | if (currentProcess) { 70 | auto highProcess = currentProcess->high; 71 | if (highProcess) { 72 | auto attackData = highProcess->attackData; 73 | if (attackData) { 74 | auto flags = attackData->data.flags; 75 | return flags.any(RE::AttackData::AttackFlag::kPowerAttack) && !flags.any(RE::AttackData::AttackFlag::kBashAttack); 76 | } 77 | } 78 | } 79 | return false; 80 | } 81 | inline void damageav(RE::Actor* a, RE::ActorValue av, float val) 82 | { 83 | if (val == 0) { 84 | return; 85 | } 86 | if (a) { 87 | a->As()->RestoreActorValue(RE::ACTOR_VALUE_MODIFIER::kDamage, av, -val); 88 | } 89 | } 90 | 91 | /*Try to damage this actor's actorvalue. If the actor does not have enough value, do not damage and return false;*/ 92 | inline bool tryDamageAv(RE::Actor* a_actor, RE::ActorValue a_actorValue, float a_damage) { 93 | auto currentAv = a_actor->GetActorValue(a_actorValue); 94 | if (currentAv - a_damage <= 0) { 95 | return false; 96 | } 97 | damageav(a_actor, a_actorValue, a_damage); 98 | return true; 99 | } 100 | 101 | inline void restoreav(RE::Actor* a_actor, RE::ActorValue a_actorValue, float a_value) 102 | { 103 | if (a_value == 0) { 104 | return; 105 | } 106 | if (a_actor) { 107 | a_actor->As()->RestoreActorValue(RE::ACTOR_VALUE_MODIFIER::kDamage, a_actorValue, a_value); 108 | } 109 | } 110 | 111 | /*Slow down game time for a set period. 112 | @param a_duration: duration of the slow time. 113 | @param a_percentage: relative time speed to normal time(1).*/ 114 | inline void slowTime(float a_duration, float a_percentage) { 115 | int duration_milisec = static_cast(a_duration * 1000); 116 | RE::Offset::SGTM(a_percentage); 117 | /*Reset time here*/ 118 | auto resetSlowTime = [](int a_duration) { 119 | std::this_thread::sleep_for(std::chrono::milliseconds(a_duration)); 120 | RE::Offset::SGTM(1); 121 | }; 122 | std::jthread resetThread(resetSlowTime, duration_milisec); 123 | resetThread.detach(); 124 | } 125 | 126 | /*Calculate the real hit damage based on game difficulty settings, and whether the player is aggressor/victim, 127 | * assuming the damage is uncalculated raw damage. 128 | @param damage: raw damage taken from hitdata. 129 | @param aggressor: aggressor of this damage. 130 | @param victim: victim of this damage.*/ 131 | inline void offsetRealDamage(float& damage, RE::Actor* aggressor, RE::Actor* victim) { 132 | if ((aggressor) && (aggressor->IsPlayerRef() || aggressor->IsPlayerTeammate())) { 133 | switch (RE::PlayerCharacter::GetSingleton()->difficulty) { 134 | case RE::DIFFICULTY::kNovice: damage *= data::fDiffMultHPByPCVE; break; 135 | case RE::DIFFICULTY::kApprentice: damage *= data::fDiffMultHPByPCE; break; 136 | case RE::DIFFICULTY::kAdept: damage *= data::fDiffMultHPByPCN; break; 137 | case RE::DIFFICULTY::kExpert: damage *= data::fDiffMultHPByPCH; break; 138 | case RE::DIFFICULTY::kMaster: damage *= data::fDiffMultHPByPCVH; break; 139 | case RE::DIFFICULTY::kLegendary: damage *= data::fDiffMultHPByPCL; break; 140 | } 141 | } 142 | else if ((victim) && (victim->IsPlayerRef() || victim->IsPlayerTeammate())) { 143 | switch (RE::PlayerCharacter::GetSingleton()->difficulty) { 144 | case RE::DIFFICULTY::kNovice: damage *= data::fDiffMultHPToPCVE; break; 145 | case RE::DIFFICULTY::kApprentice: damage *= data::fDiffMultHPToPCE; break; 146 | case RE::DIFFICULTY::kAdept: damage *= data::fDiffMultHPToPCN; break; 147 | case RE::DIFFICULTY::kExpert: damage *= data::fDiffMultHPToPCH; break; 148 | case RE::DIFFICULTY::kMaster: damage *= data::fDiffMultHPToPCVH; break; 149 | case RE::DIFFICULTY::kLegendary: damage *= data::fDiffMultHPToPCL; break; 150 | } 151 | } 152 | } 153 | 154 | inline void offsetRealDamageForPc(float& damage) { 155 | offsetRealDamage(damage, nullptr, RE::PlayerCharacter::GetSingleton()); 156 | } 157 | 158 | inline void safeApplySpell(RE::SpellItem* a_spell, RE::Actor* a_actor) { 159 | if (a_actor && a_spell) { 160 | a_actor->AddSpell(a_spell); 161 | } 162 | } 163 | 164 | inline void safeRemoveSpell(RE::SpellItem* a_spell, RE::Actor* a_actor) { 165 | if (a_actor && a_spell) { 166 | a_actor->RemoveSpell(a_spell); 167 | } 168 | } 169 | 170 | inline void safeApplyPerk(RE::BGSPerk* a_perk, RE::Actor* a_actor) { 171 | if (a_actor && a_perk && !a_actor->HasPerk(a_perk)) { 172 | a_actor->AddPerk(a_perk); 173 | } 174 | } 175 | 176 | inline void safeRemovePerk(RE::BGSPerk* a_perk, RE::Actor* a_actor) { 177 | if (a_actor && a_perk && a_actor->HasPerk(a_perk)) { 178 | a_actor->RemovePerk(a_perk); 179 | } 180 | } 181 | 182 | /*Complete refills this actor's actor value. 183 | @param a_actor actor whose actorValue will be refilled. 184 | @param actorValue type of actor value to refill.*/ 185 | inline void refillActorValue(RE::Actor* a_actor, RE::ActorValue a_actorValue) { 186 | float av = a_actor->GetActorValue(a_actorValue); 187 | float pav = a_actor->GetPermanentActorValue(a_actorValue); 188 | if (av >= pav) { 189 | return; 190 | } 191 | float avToRestore = pav - av; 192 | restoreav(a_actor, a_actorValue, avToRestore); 193 | } 194 | 195 | namespace actor 196 | { 197 | inline RE::TESObjectWEAP* getWieldingWeapon(RE::Actor* a_actor) 198 | { 199 | auto weapon = a_actor->GetAttackingWeapon(); 200 | if (weapon) { 201 | return weapon->object->As(); 202 | } 203 | auto rhs = a_actor->GetEquippedObject(false); 204 | if (rhs && rhs->IsWeapon()) { 205 | return rhs->As(); 206 | } 207 | auto lhs = a_actor->GetEquippedObject(true); 208 | if (lhs && lhs->IsWeapon()) { 209 | return lhs->As(); 210 | } 211 | return nullptr; 212 | } 213 | 214 | inline bool isDualWielding(RE::Actor* a_actor) { 215 | auto lhs = a_actor->GetEquippedObject(true); 216 | auto rhs = a_actor->GetEquippedObject(false); 217 | if (lhs && rhs && lhs->IsWeapon() && rhs->IsWeapon()) { 218 | auto weaponType = rhs->As()->GetWeaponType(); 219 | return weaponType != RE::WEAPON_TYPE::kTwoHandAxe && weaponType != RE::WEAPON_TYPE::kTwoHandSword;//can't be two hand sword. 220 | } 221 | return false; 222 | } 223 | 224 | inline bool isEquippedShield(RE::Actor* a_actor) { 225 | return RE::Offset::getEquippedShield(a_actor); 226 | } 227 | } 228 | 229 | }; 230 | 231 | namespace TrueHUDUtils 232 | { 233 | 234 | inline void flashActorValue(RE::Actor* a_actor, RE::ActorValue actorValue) { 235 | if (!settings::facts::TrueHudAPI_Obtained) { 236 | return; 237 | } 238 | if (a_actor) { 239 | ValhallaCombat::GetSingleton()->ersh_TrueHUD->FlashActorValue(a_actor->GetHandle(), actorValue, true); 240 | } 241 | } 242 | 243 | inline void greyoutAvMeter(RE::Actor* a_actor, RE::ActorValue actorValue) { 244 | if (!settings::facts::TrueHudAPI_Obtained) { 245 | return; 246 | } 247 | auto ersh = ValhallaCombat::GetSingleton()->ersh_TrueHUD; 248 | ersh->OverrideBarColor(a_actor->GetHandle(), actorValue, TRUEHUD_API::BarColorType::FlashColor, 0xd72a2a); 249 | ersh->OverrideBarColor(a_actor->GetHandle(), actorValue, TRUEHUD_API::BarColorType::BarColor, 0x7d7e7d); 250 | ersh->OverrideBarColor(a_actor->GetHandle(), actorValue, TRUEHUD_API::BarColorType::PhantomColor, 0xb30d10); 251 | } 252 | 253 | inline void revertAvMeter(RE::Actor* a_actor, RE::ActorValue actorValue) { 254 | if (!settings::facts::TrueHudAPI_Obtained) { 255 | return; 256 | } 257 | auto ersh = ValhallaCombat::GetSingleton()->ersh_TrueHUD; 258 | ersh->RevertBarColor(a_actor->GetHandle(), actorValue, TRUEHUD_API::BarColorType::FlashColor); 259 | ersh->RevertBarColor(a_actor->GetHandle(), actorValue, TRUEHUD_API::BarColorType::BarColor); 260 | ersh->RevertBarColor(a_actor->GetHandle(), actorValue, TRUEHUD_API::BarColorType::PhantomColor); 261 | } 262 | 263 | inline void greyOutSpecialMeter(RE::Actor* a_actor) { 264 | if (!settings::facts::TrueHudAPI_Obtained || !settings::facts::TrueHudAPI_HasSpecialBarControl) { 265 | return; 266 | } 267 | auto ersh = ValhallaCombat::GetSingleton()->ersh_TrueHUD; 268 | ersh->OverrideSpecialBarColor(a_actor->GetHandle(), TRUEHUD_API::BarColorType::FlashColor, 0xd72a2a); 269 | ersh->OverrideSpecialBarColor(a_actor->GetHandle(), TRUEHUD_API::BarColorType::BarColor, 0x7d7e7d); 270 | ersh->OverrideSpecialBarColor(a_actor->GetHandle(), TRUEHUD_API::BarColorType::PhantomColor, 0xb30d10); 271 | ersh->OverrideBarColor(a_actor->GetHandle(), RE::ActorValue::kHealth, TRUEHUD_API::BarColorType::FlashColor, 0xd72a2a); 272 | } 273 | 274 | inline void revertSpecialMeter(RE::Actor* a_actor) { 275 | if (!settings::facts::TrueHudAPI_Obtained || !settings::facts::TrueHudAPI_HasSpecialBarControl) { 276 | return; 277 | } 278 | auto ersh = ValhallaCombat::GetSingleton()->ersh_TrueHUD; 279 | ersh->RevertSpecialBarColor(a_actor->GetHandle(), TRUEHUD_API::BarColorType::FlashColor); 280 | ersh->RevertSpecialBarColor(a_actor->GetHandle(), TRUEHUD_API::BarColorType::BarColor); 281 | ersh->RevertSpecialBarColor(a_actor->GetHandle(), TRUEHUD_API::BarColorType::PhantomColor); 282 | ersh->RevertBarColor(a_actor->GetHandle(), RE::ActorValue::kHealth, TRUEHUD_API::BarColorType::FlashColor); 283 | } 284 | 285 | }; 286 | 287 | class ValhallaUtils 288 | { 289 | public: 290 | /*Whether the actor's back is facing the other actor's front. 291 | @param actor1: actor whose facing will be returned 292 | @param actor2: actor whose relative location to actor1 will be calculated.*/ 293 | static bool isBackFacing(RE::Actor* actor1, RE::Actor* actor2) { 294 | auto angle = actor1->GetHeadingAngle(actor2->GetPosition(), false); 295 | if (90 < angle || angle < -90) { 296 | return true; 297 | } 298 | else { 299 | return false; 300 | } 301 | } 302 | 303 | 304 | 305 | 306 | /*Play sound with formid at a certain actor's position. 307 | @param a: actor on which to play sonud. 308 | @param formid: formid of the sound descriptor.*/ 309 | static void playSound(RE::Actor* a, RE::BGSSoundDescriptorForm* a_descriptor, float a_volumeOverride = 1) 310 | { 311 | 312 | RE::BSSoundHandle handle; 313 | handle.soundID = static_cast(-1); 314 | handle.assumeSuccess = false; 315 | *(uint32_t*)&handle.state = 0; 316 | 317 | RE::Offset::soundHelper_a(RE::BSAudioManager::GetSingleton(), &handle, a_descriptor->GetFormID(), 16); 318 | if (RE::Offset::set_sound_position(&handle, a->data.location.x, a->data.location.y, a->data.location.z)) { 319 | handle.SetVolume(a_volumeOverride); 320 | RE::Offset::soundHelper_b(&handle, a->Get3D()); 321 | RE::Offset::soundHelper_c(&handle); 322 | } 323 | } 324 | 325 | static void playSound(RE::Actor* a, std::vector sounds) 326 | { 327 | playSound(a, *inlineUtils::select_randomly(sounds.begin(), sounds.end())); 328 | } 329 | 330 | /* 331 | @return actor a and actor b's absolute distance, if the radius is bigger than distance. 332 | @return -1, if the distance exceeds radius.*/ 333 | static float getInRange(RE::Actor* a, RE::Actor* b, float maxRange) { 334 | float dist = a->GetPosition().GetDistance(b->GetPosition()); 335 | if (dist <= maxRange) { 336 | return dist; 337 | } 338 | else { 339 | return -1; 340 | } 341 | } 342 | 343 | static void queueMessageBox(RE::BSFixedString a_message) { 344 | auto factoryManager = RE::MessageDataFactoryManager::GetSingleton(); 345 | auto uiStrHolder = RE::InterfaceStrings::GetSingleton(); 346 | auto factory = factoryManager->GetCreator(uiStrHolder->messageBoxData); 347 | auto messageBox = factory->Create(); 348 | messageBox->unk4C = 4; 349 | messageBox->unk38 = 10; 350 | messageBox->bodyText = a_message; 351 | auto gameSettings = RE::GameSettingCollection::GetSingleton(); 352 | auto sOk = gameSettings->GetSetting("sOk"); 353 | messageBox->buttonText.push_back(sOk->GetString()); 354 | messageBox->QueueMessage(); 355 | } 356 | #pragma endregion 357 | /*Clamp the raw damage to be no more than the aggressor's max raw melee damage output.*/ 358 | static void clampDmg(float& dmg, RE::Actor* aggressor) { 359 | auto a_weapon = inlineUtils::actor::getWieldingWeapon(aggressor); 360 | if (a_weapon) { 361 | //DEBUG("weapon to clamp damage: {}", a_weapon->GetName()); 362 | dmg = min(dmg, a_weapon->GetAttackDamage()); 363 | } 364 | } 365 | 366 | static bool isCasting(RE::Actor* a_actor) { 367 | for (int i = 0; i < 3; i++) 368 | { 369 | auto caster = a_actor->GetMagicCaster(RE::MagicSystem::CastingSource(i)); 370 | if (!caster) 371 | continue; 372 | if (caster->state) { 373 | return true; 374 | } 375 | } 376 | return false; 377 | } 378 | 379 | /*Set the projectile's cause to a new actor; reset the projectile's collision mask. 380 | @param a_projectile: projectile to be reset. 381 | @param a_actor: new owner of the projectile. 382 | @param a_projectile_collidable: pointer to the projectile collidable to rset its collision mask.*/ 383 | static void resetProjectileOwner(RE::Projectile* a_projectile, RE::Actor* a_actor, RE::hkpCollidable* a_projectile_collidable) { 384 | a_projectile->SetActorCause(a_actor->GetActorCause()); 385 | a_projectile->shooter = a_actor->GetHandle(); 386 | uint32_t a_collisionFilterInfo; 387 | a_actor->GetCollisionFilterInfo(a_collisionFilterInfo); 388 | a_projectile_collidable->broadPhaseHandle.collisionFilterInfo &= (0x0000FFFF); 389 | a_projectile_collidable->broadPhaseHandle.collisionFilterInfo |= (a_collisionFilterInfo << 16); 390 | } 391 | 392 | static inline bool ApproximatelyEqual(float A, float B) 393 | { 394 | return ((A - B) < FLT_EPSILON) && ((B - A) < FLT_EPSILON); 395 | } 396 | 397 | static bool PredictAimProjectile(RE::NiPoint3 a_projectilePos, RE::NiPoint3 a_targetPosition, RE::NiPoint3 a_targetVelocity, float a_gravity, RE::NiPoint3& a_projectileVelocity) 398 | { 399 | // http://ringofblades.com/Blades/Code/PredictiveAim.cs 400 | 401 | float projectileSpeedSquared = a_projectileVelocity.SqrLength(); 402 | float projectileSpeed = std::sqrtf(projectileSpeedSquared); 403 | 404 | if (projectileSpeed <= 0.f || a_projectilePos == a_targetPosition) { 405 | return false; 406 | } 407 | 408 | float targetSpeedSquared = a_targetVelocity.SqrLength(); 409 | float targetSpeed = std::sqrtf(targetSpeedSquared); 410 | RE::NiPoint3 targetToProjectile = a_projectilePos - a_targetPosition; 411 | float distanceSquared = targetToProjectile.SqrLength(); 412 | float distance = std::sqrtf(distanceSquared); 413 | RE::NiPoint3 direction = targetToProjectile; 414 | direction.Unitize(); 415 | RE::NiPoint3 targetVelocityDirection = a_targetVelocity; 416 | targetVelocityDirection.Unitize(); 417 | 418 | float cosTheta = (targetSpeedSquared > 0) 419 | ? direction.Dot(targetVelocityDirection) 420 | : 1.0f; 421 | 422 | bool bValidSolutionFound = true; 423 | float t; 424 | 425 | if (ApproximatelyEqual(projectileSpeedSquared, targetSpeedSquared)) { 426 | // We want to avoid div/0 that can result from target and projectile traveling at the same speed 427 | //We know that cos(theta) of zero or less means there is no solution, since that would mean B goes backwards or leads to div/0 (infinity) 428 | if (cosTheta > 0) { 429 | t = 0.5f * distance / (targetSpeed * cosTheta); 430 | } 431 | else { 432 | bValidSolutionFound = false; 433 | t = 1; 434 | } 435 | } 436 | else { 437 | float a = projectileSpeedSquared - targetSpeedSquared; 438 | float b = 2.0f * distance * targetSpeed * cosTheta; 439 | float c = -distanceSquared; 440 | float discriminant = b * b - 4.0f * a * c; 441 | 442 | if (discriminant < 0) { 443 | // NaN 444 | bValidSolutionFound = false; 445 | t = 1; 446 | } 447 | else { 448 | // a will never be zero 449 | float uglyNumber = sqrtf(discriminant); 450 | float t0 = 0.5f * (-b + uglyNumber) / a; 451 | float t1 = 0.5f * (-b - uglyNumber) / a; 452 | 453 | // Assign the lowest positive time to t to aim at the earliest hit 454 | t = min(t0, t1); 455 | if (t < FLT_EPSILON) { 456 | t = max(t0, t1); 457 | } 458 | 459 | if (t < FLT_EPSILON) { 460 | // Time can't flow backwards when it comes to aiming. 461 | // No real solution was found, take a wild shot at the target's future location 462 | bValidSolutionFound = false; 463 | t = 1; 464 | } 465 | } 466 | } 467 | 468 | a_projectileVelocity = a_targetVelocity + (-targetToProjectile / t); 469 | 470 | if (!bValidSolutionFound) 471 | { 472 | a_projectileVelocity.Unitize(); 473 | a_projectileVelocity *= projectileSpeed; 474 | } 475 | 476 | if (!ApproximatelyEqual(a_gravity, 0.f)) 477 | { 478 | float netFallDistance = (a_projectileVelocity * t).z; 479 | float gravityCompensationSpeed = (netFallDistance + 0.5f * a_gravity * t * t) / t; 480 | a_projectileVelocity.z = gravityCompensationSpeed; 481 | } 482 | 483 | return bValidSolutionFound; 484 | } 485 | 486 | 487 | 488 | static void ReflectProjectile(RE::Projectile* a_projectile) 489 | { 490 | a_projectile->linearVelocity *= -1.f; 491 | 492 | // rotate model 493 | auto projectileNode = a_projectile->Get3D2(); 494 | if (projectileNode) 495 | { 496 | RE::NiPoint3 direction = a_projectile->linearVelocity; 497 | direction.Unitize(); 498 | 499 | a_projectile->data.angle.x = asin(direction.z); 500 | a_projectile->data.angle.z = atan2(direction.x, direction.y); 501 | 502 | if (a_projectile->data.angle.z < 0.0) { 503 | a_projectile->data.angle.z += PI; 504 | } 505 | 506 | if (direction.x < 0.0) { 507 | a_projectile->data.angle.z += PI; 508 | } 509 | 510 | inlineUtils::SetRotationMatrix(projectileNode->local.rotate, -direction.x, direction.y, direction.z); 511 | } 512 | } 513 | 514 | /*Get the body position of this actor.*/ 515 | static void getBodyPos(RE::Actor* a_actor, RE::NiPoint3& pos) 516 | { 517 | if (!a_actor->race) { 518 | return; 519 | } 520 | RE::BGSBodyPart* bodyPart = a_actor->race->bodyPartData->parts[0]; 521 | if (!bodyPart) { 522 | return; 523 | } 524 | auto targetPoint = a_actor->GetNodeByName(bodyPart->targetName.c_str()); 525 | if (!targetPoint) { 526 | return; 527 | } 528 | 529 | pos = targetPoint->world.translate; 530 | } 531 | 532 | /// 533 | /// Change the projectile's trajectory, aiming it at the target. 534 | /// 535 | /// Projectile whose trajectory will be changed. 536 | /// New target to be aimed at. 537 | static void RetargetProjectile(RE::Projectile* a_projectile, RE::TESObjectREFR* a_target) 538 | { 539 | a_projectile->desiredTarget = a_target; 540 | 541 | auto projectileNode = a_projectile->Get3D2(); 542 | auto targetHandle = a_target->GetHandle(); 543 | 544 | RE::NiPoint3 targetPos = a_target->GetPosition(); 545 | 546 | if (a_target->GetFormType() == RE::FormType::ActorCharacter) { 547 | getBodyPos(a_target->As(), targetPos); 548 | } 549 | 550 | RE::NiPoint3 targetVelocity; 551 | targetHandle.get()->GetLinearVelocity(targetVelocity); 552 | 553 | float projectileGravity = 0.f; 554 | if (auto ammo = a_projectile->ammoSource) { 555 | if (auto bgsProjectile = ammo->data.projectile) { 556 | projectileGravity = bgsProjectile->data.gravity; 557 | if (auto bhkWorld = a_projectile->parentCell->GetbhkWorld()) { 558 | if (auto hkpWorld = bhkWorld->GetWorld1()) { 559 | auto vec4 = hkpWorld->gravity; 560 | float quad[4]; 561 | _mm_store_ps(quad, vec4.quad); 562 | float gravity = -quad[2] * *RE::Offset::g_worldScaleInverse; 563 | projectileGravity *= gravity; 564 | } 565 | } 566 | } 567 | } 568 | 569 | PredictAimProjectile(a_projectile->data.location, targetPos, targetVelocity, projectileGravity, a_projectile->linearVelocity); 570 | 571 | // rotate 572 | RE::NiPoint3 direction = a_projectile->linearVelocity; 573 | direction.Unitize(); 574 | 575 | a_projectile->data.angle.x = asin(direction.z); 576 | a_projectile->data.angle.z = atan2(direction.x, direction.y); 577 | 578 | if (a_projectile->data.angle.z < 0.0) { 579 | a_projectile->data.angle.z += PI; 580 | } 581 | 582 | if (direction.x < 0.0) { 583 | a_projectile->data.angle.z += PI; 584 | } 585 | 586 | inlineUtils::SetRotationMatrix(projectileNode->local.rotate, -direction.x, direction.y, direction.z); 587 | } 588 | }; 589 | 590 | namespace DtryUtils 591 | { 592 | /*Helper class to batch load forms from plugin records.*/ 593 | class formLoader 594 | { 595 | private: 596 | RE::BSFixedString _pluginName; 597 | RE::TESDataHandler* _dataHandler; 598 | int _loadedForms; 599 | 600 | public: 601 | formLoader(RE::BSFixedString pluginName) 602 | { 603 | _pluginName = pluginName; 604 | _dataHandler = RE::TESDataHandler::GetSingleton(); 605 | if (!_dataHandler) { 606 | logger::critical("Error: TESDataHandler not found."); 607 | } 608 | if (!_dataHandler->LookupModByName(pluginName)) { 609 | logger::critical("Error: {} not found.", pluginName); 610 | } 611 | logger::info("Loading from plugin {}...", pluginName); 612 | } 613 | 614 | void log() 615 | { 616 | logger::info("Loaded {} forms from {}", _loadedForms, _pluginName); 617 | } 618 | 619 | /*Load a form from the plugin.*/ 620 | template 621 | void load(formType*& formRet, RE::FormID formID) 622 | { 623 | formRet = _dataHandler->LookupForm(formID, _pluginName); 624 | if (!formRet) { 625 | logger::critical("Error: null formID or wrong form type when loading {} from {}", formID, _pluginName); 626 | } 627 | _loadedForms++; 628 | } 629 | }; 630 | 631 | 632 | } 633 | -------------------------------------------------------------------------------- /src/include/lib/nanosvgrast.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013-14 Mikko Mononen memon@inside.org 3 | * 4 | * This software is provided 'as-is', without any express or implied 5 | * warranty. In no event will the authors be held liable for any damages 6 | * arising from the use of this software. 7 | * 8 | * Permission is granted to anyone to use this software for any purpose, 9 | * including commercial applications, and to alter it and redistribute it 10 | * freely, subject to the following restrictions: 11 | * 12 | * 1. The origin of this software must not be misrepresented; you must not 13 | * claim that you wrote the original software. If you use this software 14 | * in a product, an acknowledgment in the product documentation would be 15 | * appreciated but is not required. 16 | * 2. Altered source versions must be plainly marked as such, and must not be 17 | * misrepresented as being the original software. 18 | * 3. This notice may not be removed or altered from any source distribution. 19 | * 20 | * The polygon rasterization is heavily based on stb_truetype rasterizer 21 | * by Sean Barrett - http://nothings.org/ 22 | * 23 | */ 24 | 25 | #ifndef NANOSVGRAST_H 26 | #define NANOSVGRAST_H 27 | 28 | #include "nanosvg.h" 29 | 30 | #ifndef NANOSVGRAST_CPLUSPLUS 31 | #ifdef __cplusplus 32 | extern "C" { 33 | #endif 34 | #endif 35 | 36 | typedef struct NSVGrasterizer NSVGrasterizer; 37 | 38 | /* Example Usage: 39 | // Load SVG 40 | NSVGimage* image; 41 | image = nsvgParseFromFile("test.svg", "px", 96); 42 | 43 | // Create rasterizer (can be used to render multiple images). 44 | struct NSVGrasterizer* rast = nsvgCreateRasterizer(); 45 | // Allocate memory for image 46 | unsigned char* img = malloc(w*h*4); 47 | // Rasterize 48 | nsvgRasterize(rast, image, 0,0,1, img, w, h, w*4); 49 | */ 50 | 51 | // Allocated rasterizer context. 52 | NSVGrasterizer* nsvgCreateRasterizer(void); 53 | 54 | // Rasterizes SVG image, returns RGBA image (non-premultiplied alpha) 55 | // r - pointer to rasterizer context 56 | // image - pointer to image to rasterize 57 | // tx,ty - image offset (applied after scaling) 58 | // scale - image scale 59 | // dst - pointer to destination image data, 4 bytes per pixel (RGBA) 60 | // w - width of the image to render 61 | // h - height of the image to render 62 | // stride - number of bytes per scaleline in the destination buffer 63 | void nsvgRasterize(NSVGrasterizer* r, 64 | NSVGimage* image, float tx, float ty, float scale, 65 | unsigned char* dst, int w, int h, int stride); 66 | 67 | // Deletes rasterizer context. 68 | void nsvgDeleteRasterizer(NSVGrasterizer*); 69 | 70 | 71 | #ifndef NANOSVGRAST_CPLUSPLUS 72 | #ifdef __cplusplus 73 | } 74 | #endif 75 | #endif 76 | 77 | #ifdef NANOSVGRAST_IMPLEMENTATION 78 | 79 | #include 80 | #include 81 | #include 82 | 83 | #define NSVG__SUBSAMPLES 5 84 | #define NSVG__FIXSHIFT 10 85 | #define NSVG__FIX (1 << NSVG__FIXSHIFT) 86 | #define NSVG__FIXMASK (NSVG__FIX-1) 87 | #define NSVG__MEMPAGE_SIZE 1024 88 | 89 | typedef struct NSVGedge { 90 | float x0,y0, x1,y1; 91 | int dir; 92 | struct NSVGedge* next; 93 | } NSVGedge; 94 | 95 | typedef struct NSVGpoint { 96 | float x, y; 97 | float dx, dy; 98 | float len; 99 | float dmx, dmy; 100 | unsigned char flags; 101 | } NSVGpoint; 102 | 103 | typedef struct NSVGactiveEdge { 104 | int x,dx; 105 | float ey; 106 | int dir; 107 | struct NSVGactiveEdge *next; 108 | } NSVGactiveEdge; 109 | 110 | typedef struct NSVGmemPage { 111 | unsigned char mem[NSVG__MEMPAGE_SIZE]; 112 | int size; 113 | struct NSVGmemPage* next; 114 | } NSVGmemPage; 115 | 116 | typedef struct NSVGcachedPaint { 117 | signed char type; 118 | char spread; 119 | float xform[6]; 120 | unsigned int colors[256]; 121 | } NSVGcachedPaint; 122 | 123 | struct NSVGrasterizer 124 | { 125 | float px, py; 126 | 127 | float tessTol; 128 | float distTol; 129 | 130 | NSVGedge* edges; 131 | int nedges; 132 | int cedges; 133 | 134 | NSVGpoint* points; 135 | int npoints; 136 | int cpoints; 137 | 138 | NSVGpoint* points2; 139 | int npoints2; 140 | int cpoints2; 141 | 142 | NSVGactiveEdge* freelist; 143 | NSVGmemPage* pages; 144 | NSVGmemPage* curpage; 145 | 146 | unsigned char* scanline; 147 | int cscanline; 148 | 149 | unsigned char* bitmap; 150 | int width, height, stride; 151 | }; 152 | 153 | NSVGrasterizer* nsvgCreateRasterizer(void) 154 | { 155 | NSVGrasterizer* r = (NSVGrasterizer*)malloc(sizeof(NSVGrasterizer)); 156 | if (r == NULL) goto error; 157 | memset(r, 0, sizeof(NSVGrasterizer)); 158 | 159 | r->tessTol = 0.25f; 160 | r->distTol = 0.01f; 161 | 162 | return r; 163 | 164 | error: 165 | nsvgDeleteRasterizer(r); 166 | return NULL; 167 | } 168 | 169 | void nsvgDeleteRasterizer(NSVGrasterizer* r) 170 | { 171 | NSVGmemPage* p; 172 | 173 | if (r == NULL) return; 174 | 175 | p = r->pages; 176 | while (p != NULL) { 177 | NSVGmemPage* next = p->next; 178 | free(p); 179 | p = next; 180 | } 181 | 182 | if (r->edges) free(r->edges); 183 | if (r->points) free(r->points); 184 | if (r->points2) free(r->points2); 185 | if (r->scanline) free(r->scanline); 186 | 187 | free(r); 188 | } 189 | 190 | static NSVGmemPage* nsvg__nextPage(NSVGrasterizer* r, NSVGmemPage* cur) 191 | { 192 | NSVGmemPage *newp; 193 | 194 | // If using existing chain, return the next page in chain 195 | if (cur != NULL && cur->next != NULL) { 196 | return cur->next; 197 | } 198 | 199 | // Alloc new page 200 | newp = (NSVGmemPage*)malloc(sizeof(NSVGmemPage)); 201 | if (newp == NULL) return NULL; 202 | memset(newp, 0, sizeof(NSVGmemPage)); 203 | 204 | // Add to linked list 205 | if (cur != NULL) 206 | cur->next = newp; 207 | else 208 | r->pages = newp; 209 | 210 | return newp; 211 | } 212 | 213 | static void nsvg__resetPool(NSVGrasterizer* r) 214 | { 215 | NSVGmemPage* p = r->pages; 216 | while (p != NULL) { 217 | p->size = 0; 218 | p = p->next; 219 | } 220 | r->curpage = r->pages; 221 | } 222 | 223 | static unsigned char* nsvg__alloc(NSVGrasterizer* r, int size) 224 | { 225 | unsigned char* buf; 226 | if (size > NSVG__MEMPAGE_SIZE) return NULL; 227 | if (r->curpage == NULL || r->curpage->size+size > NSVG__MEMPAGE_SIZE) { 228 | r->curpage = nsvg__nextPage(r, r->curpage); 229 | } 230 | buf = &r->curpage->mem[r->curpage->size]; 231 | r->curpage->size += size; 232 | return buf; 233 | } 234 | 235 | static int nsvg__ptEquals(float x1, float y1, float x2, float y2, float tol) 236 | { 237 | float dx = x2 - x1; 238 | float dy = y2 - y1; 239 | return dx*dx + dy*dy < tol*tol; 240 | } 241 | 242 | static void nsvg__addPathPoint(NSVGrasterizer* r, float x, float y, int flags) 243 | { 244 | NSVGpoint* pt; 245 | 246 | if (r->npoints > 0) { 247 | pt = &r->points[r->npoints-1]; 248 | if (nsvg__ptEquals(pt->x,pt->y, x,y, r->distTol)) { 249 | pt->flags = (unsigned char)(pt->flags | flags); 250 | return; 251 | } 252 | } 253 | 254 | if (r->npoints+1 > r->cpoints) { 255 | r->cpoints = r->cpoints > 0 ? r->cpoints * 2 : 64; 256 | r->points = (NSVGpoint*)realloc(r->points, sizeof(NSVGpoint) * r->cpoints); 257 | if (r->points == NULL) return; 258 | } 259 | 260 | pt = &r->points[r->npoints]; 261 | pt->x = x; 262 | pt->y = y; 263 | pt->flags = (unsigned char)flags; 264 | r->npoints++; 265 | } 266 | 267 | static void nsvg__appendPathPoint(NSVGrasterizer* r, NSVGpoint pt) 268 | { 269 | if (r->npoints+1 > r->cpoints) { 270 | r->cpoints = r->cpoints > 0 ? r->cpoints * 2 : 64; 271 | r->points = (NSVGpoint*)realloc(r->points, sizeof(NSVGpoint) * r->cpoints); 272 | if (r->points == NULL) return; 273 | } 274 | r->points[r->npoints] = pt; 275 | r->npoints++; 276 | } 277 | 278 | static void nsvg__duplicatePoints(NSVGrasterizer* r) 279 | { 280 | if (r->npoints > r->cpoints2) { 281 | r->cpoints2 = r->npoints; 282 | r->points2 = (NSVGpoint*)realloc(r->points2, sizeof(NSVGpoint) * r->cpoints2); 283 | if (r->points2 == NULL) return; 284 | } 285 | 286 | memcpy(r->points2, r->points, sizeof(NSVGpoint) * r->npoints); 287 | r->npoints2 = r->npoints; 288 | } 289 | 290 | static void nsvg__addEdge(NSVGrasterizer* r, float x0, float y0, float x1, float y1) 291 | { 292 | NSVGedge* e; 293 | 294 | // Skip horizontal edges 295 | if (y0 == y1) 296 | return; 297 | 298 | if (r->nedges+1 > r->cedges) { 299 | r->cedges = r->cedges > 0 ? r->cedges * 2 : 64; 300 | r->edges = (NSVGedge*)realloc(r->edges, sizeof(NSVGedge) * r->cedges); 301 | if (r->edges == NULL) return; 302 | } 303 | 304 | e = &r->edges[r->nedges]; 305 | r->nedges++; 306 | 307 | if (y0 < y1) { 308 | e->x0 = x0; 309 | e->y0 = y0; 310 | e->x1 = x1; 311 | e->y1 = y1; 312 | e->dir = 1; 313 | } else { 314 | e->x0 = x1; 315 | e->y0 = y1; 316 | e->x1 = x0; 317 | e->y1 = y0; 318 | e->dir = -1; 319 | } 320 | } 321 | 322 | static float nsvg__normalize(float *x, float* y) 323 | { 324 | float d = sqrtf((*x)*(*x) + (*y)*(*y)); 325 | if (d > 1e-6f) { 326 | float id = 1.0f / d; 327 | *x *= id; 328 | *y *= id; 329 | } 330 | return d; 331 | } 332 | 333 | static float nsvg__absf(float x) { return x < 0 ? -x : x; } 334 | 335 | static void nsvg__flattenCubicBez(NSVGrasterizer* r, 336 | float x1, float y1, float x2, float y2, 337 | float x3, float y3, float x4, float y4, 338 | int level, int type) 339 | { 340 | float x12,y12,x23,y23,x34,y34,x123,y123,x234,y234,x1234,y1234; 341 | float dx,dy,d2,d3; 342 | 343 | if (level > 10) return; 344 | 345 | x12 = (x1+x2)*0.5f; 346 | y12 = (y1+y2)*0.5f; 347 | x23 = (x2+x3)*0.5f; 348 | y23 = (y2+y3)*0.5f; 349 | x34 = (x3+x4)*0.5f; 350 | y34 = (y3+y4)*0.5f; 351 | x123 = (x12+x23)*0.5f; 352 | y123 = (y12+y23)*0.5f; 353 | 354 | dx = x4 - x1; 355 | dy = y4 - y1; 356 | d2 = nsvg__absf(((x2 - x4) * dy - (y2 - y4) * dx)); 357 | d3 = nsvg__absf(((x3 - x4) * dy - (y3 - y4) * dx)); 358 | 359 | if ((d2 + d3)*(d2 + d3) < r->tessTol * (dx*dx + dy*dy)) { 360 | nsvg__addPathPoint(r, x4, y4, type); 361 | return; 362 | } 363 | 364 | x234 = (x23+x34)*0.5f; 365 | y234 = (y23+y34)*0.5f; 366 | x1234 = (x123+x234)*0.5f; 367 | y1234 = (y123+y234)*0.5f; 368 | 369 | nsvg__flattenCubicBez(r, x1,y1, x12,y12, x123,y123, x1234,y1234, level+1, 0); 370 | nsvg__flattenCubicBez(r, x1234,y1234, x234,y234, x34,y34, x4,y4, level+1, type); 371 | } 372 | 373 | static void nsvg__flattenShape(NSVGrasterizer* r, NSVGshape* shape, float scale) 374 | { 375 | int i, j; 376 | NSVGpath* path; 377 | 378 | for (path = shape->paths; path != NULL; path = path->next) { 379 | r->npoints = 0; 380 | // Flatten path 381 | nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); 382 | for (i = 0; i < path->npts-1; i += 3) { 383 | float* p = &path->pts[i*2]; 384 | nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, 0); 385 | } 386 | // Close path 387 | nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); 388 | // Build edges 389 | for (i = 0, j = r->npoints-1; i < r->npoints; j = i++) 390 | nsvg__addEdge(r, r->points[j].x, r->points[j].y, r->points[i].x, r->points[i].y); 391 | } 392 | } 393 | 394 | enum NSVGpointFlags 395 | { 396 | NSVG_PT_CORNER = 0x01, 397 | NSVG_PT_BEVEL = 0x02, 398 | NSVG_PT_LEFT = 0x04 399 | }; 400 | 401 | static void nsvg__initClosed(NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) 402 | { 403 | float w = lineWidth * 0.5f; 404 | float dx = p1->x - p0->x; 405 | float dy = p1->y - p0->y; 406 | float len = nsvg__normalize(&dx, &dy); 407 | float px = p0->x + dx*len*0.5f, py = p0->y + dy*len*0.5f; 408 | float dlx = dy, dly = -dx; 409 | float lx = px - dlx*w, ly = py - dly*w; 410 | float rx = px + dlx*w, ry = py + dly*w; 411 | left->x = lx; left->y = ly; 412 | right->x = rx; right->y = ry; 413 | } 414 | 415 | static void nsvg__buttCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int connect) 416 | { 417 | float w = lineWidth * 0.5f; 418 | float px = p->x, py = p->y; 419 | float dlx = dy, dly = -dx; 420 | float lx = px - dlx*w, ly = py - dly*w; 421 | float rx = px + dlx*w, ry = py + dly*w; 422 | 423 | nsvg__addEdge(r, lx, ly, rx, ry); 424 | 425 | if (connect) { 426 | nsvg__addEdge(r, left->x, left->y, lx, ly); 427 | nsvg__addEdge(r, rx, ry, right->x, right->y); 428 | } 429 | left->x = lx; left->y = ly; 430 | right->x = rx; right->y = ry; 431 | } 432 | 433 | static void nsvg__squareCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int connect) 434 | { 435 | float w = lineWidth * 0.5f; 436 | float px = p->x - dx*w, py = p->y - dy*w; 437 | float dlx = dy, dly = -dx; 438 | float lx = px - dlx*w, ly = py - dly*w; 439 | float rx = px + dlx*w, ry = py + dly*w; 440 | 441 | nsvg__addEdge(r, lx, ly, rx, ry); 442 | 443 | if (connect) { 444 | nsvg__addEdge(r, left->x, left->y, lx, ly); 445 | nsvg__addEdge(r, rx, ry, right->x, right->y); 446 | } 447 | left->x = lx; left->y = ly; 448 | right->x = rx; right->y = ry; 449 | } 450 | 451 | #ifndef NSVG_PI 452 | #define NSVG_PI (3.14159265358979323846264338327f) 453 | #endif 454 | 455 | static void nsvg__roundCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int ncap, int connect) 456 | { 457 | int i; 458 | float w = lineWidth * 0.5f; 459 | float px = p->x, py = p->y; 460 | float dlx = dy, dly = -dx; 461 | float lx = 0, ly = 0, rx = 0, ry = 0, prevx = 0, prevy = 0; 462 | 463 | for (i = 0; i < ncap; i++) { 464 | float a = (float)i/(float)(ncap-1)*NSVG_PI; 465 | float ax = cosf(a) * w, ay = sinf(a) * w; 466 | float x = px - dlx*ax - dx*ay; 467 | float y = py - dly*ax - dy*ay; 468 | 469 | if (i > 0) 470 | nsvg__addEdge(r, prevx, prevy, x, y); 471 | 472 | prevx = x; 473 | prevy = y; 474 | 475 | if (i == 0) { 476 | lx = x; ly = y; 477 | } else if (i == ncap-1) { 478 | rx = x; ry = y; 479 | } 480 | } 481 | 482 | if (connect) { 483 | nsvg__addEdge(r, left->x, left->y, lx, ly); 484 | nsvg__addEdge(r, rx, ry, right->x, right->y); 485 | } 486 | 487 | left->x = lx; left->y = ly; 488 | right->x = rx; right->y = ry; 489 | } 490 | 491 | static void nsvg__bevelJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) 492 | { 493 | float w = lineWidth * 0.5f; 494 | float dlx0 = p0->dy, dly0 = -p0->dx; 495 | float dlx1 = p1->dy, dly1 = -p1->dx; 496 | float lx0 = p1->x - (dlx0 * w), ly0 = p1->y - (dly0 * w); 497 | float rx0 = p1->x + (dlx0 * w), ry0 = p1->y + (dly0 * w); 498 | float lx1 = p1->x - (dlx1 * w), ly1 = p1->y - (dly1 * w); 499 | float rx1 = p1->x + (dlx1 * w), ry1 = p1->y + (dly1 * w); 500 | 501 | nsvg__addEdge(r, lx0, ly0, left->x, left->y); 502 | nsvg__addEdge(r, lx1, ly1, lx0, ly0); 503 | 504 | nsvg__addEdge(r, right->x, right->y, rx0, ry0); 505 | nsvg__addEdge(r, rx0, ry0, rx1, ry1); 506 | 507 | left->x = lx1; left->y = ly1; 508 | right->x = rx1; right->y = ry1; 509 | } 510 | 511 | static void nsvg__miterJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) 512 | { 513 | float w = lineWidth * 0.5f; 514 | float dlx0 = p0->dy, dly0 = -p0->dx; 515 | float dlx1 = p1->dy, dly1 = -p1->dx; 516 | float lx0, rx0, lx1, rx1; 517 | float ly0, ry0, ly1, ry1; 518 | 519 | if (p1->flags & NSVG_PT_LEFT) { 520 | lx0 = lx1 = p1->x - p1->dmx * w; 521 | ly0 = ly1 = p1->y - p1->dmy * w; 522 | nsvg__addEdge(r, lx1, ly1, left->x, left->y); 523 | 524 | rx0 = p1->x + (dlx0 * w); 525 | ry0 = p1->y + (dly0 * w); 526 | rx1 = p1->x + (dlx1 * w); 527 | ry1 = p1->y + (dly1 * w); 528 | nsvg__addEdge(r, right->x, right->y, rx0, ry0); 529 | nsvg__addEdge(r, rx0, ry0, rx1, ry1); 530 | } else { 531 | lx0 = p1->x - (dlx0 * w); 532 | ly0 = p1->y - (dly0 * w); 533 | lx1 = p1->x - (dlx1 * w); 534 | ly1 = p1->y - (dly1 * w); 535 | nsvg__addEdge(r, lx0, ly0, left->x, left->y); 536 | nsvg__addEdge(r, lx1, ly1, lx0, ly0); 537 | 538 | rx0 = rx1 = p1->x + p1->dmx * w; 539 | ry0 = ry1 = p1->y + p1->dmy * w; 540 | nsvg__addEdge(r, right->x, right->y, rx1, ry1); 541 | } 542 | 543 | left->x = lx1; left->y = ly1; 544 | right->x = rx1; right->y = ry1; 545 | } 546 | 547 | static void nsvg__roundJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth, int ncap) 548 | { 549 | int i, n; 550 | float w = lineWidth * 0.5f; 551 | float dlx0 = p0->dy, dly0 = -p0->dx; 552 | float dlx1 = p1->dy, dly1 = -p1->dx; 553 | float a0 = atan2f(dly0, dlx0); 554 | float a1 = atan2f(dly1, dlx1); 555 | float da = a1 - a0; 556 | float lx, ly, rx, ry; 557 | 558 | if (da < NSVG_PI) da += NSVG_PI*2; 559 | if (da > NSVG_PI) da -= NSVG_PI*2; 560 | 561 | n = (int)ceilf((nsvg__absf(da) / NSVG_PI) * (float)ncap); 562 | if (n < 2) n = 2; 563 | if (n > ncap) n = ncap; 564 | 565 | lx = left->x; 566 | ly = left->y; 567 | rx = right->x; 568 | ry = right->y; 569 | 570 | for (i = 0; i < n; i++) { 571 | float u = (float)i/(float)(n-1); 572 | float a = a0 + u*da; 573 | float ax = cosf(a) * w, ay = sinf(a) * w; 574 | float lx1 = p1->x - ax, ly1 = p1->y - ay; 575 | float rx1 = p1->x + ax, ry1 = p1->y + ay; 576 | 577 | nsvg__addEdge(r, lx1, ly1, lx, ly); 578 | nsvg__addEdge(r, rx, ry, rx1, ry1); 579 | 580 | lx = lx1; ly = ly1; 581 | rx = rx1; ry = ry1; 582 | } 583 | 584 | left->x = lx; left->y = ly; 585 | right->x = rx; right->y = ry; 586 | } 587 | 588 | static void nsvg__straightJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p1, float lineWidth) 589 | { 590 | float w = lineWidth * 0.5f; 591 | float lx = p1->x - (p1->dmx * w), ly = p1->y - (p1->dmy * w); 592 | float rx = p1->x + (p1->dmx * w), ry = p1->y + (p1->dmy * w); 593 | 594 | nsvg__addEdge(r, lx, ly, left->x, left->y); 595 | nsvg__addEdge(r, right->x, right->y, rx, ry); 596 | 597 | left->x = lx; left->y = ly; 598 | right->x = rx; right->y = ry; 599 | } 600 | 601 | static int nsvg__curveDivs(float r, float arc, float tol) 602 | { 603 | float da = acosf(r / (r + tol)) * 2.0f; 604 | int divs = (int)ceilf(arc / da); 605 | if (divs < 2) divs = 2; 606 | return divs; 607 | } 608 | 609 | static void nsvg__expandStroke(NSVGrasterizer* r, NSVGpoint* points, int npoints, int closed, int lineJoin, int lineCap, float lineWidth) 610 | { 611 | int ncap = nsvg__curveDivs(lineWidth*0.5f, NSVG_PI, r->tessTol); // Calculate divisions per half circle. 612 | NSVGpoint left = {0,0,0,0,0,0,0,0}, right = {0,0,0,0,0,0,0,0}, firstLeft = {0,0,0,0,0,0,0,0}, firstRight = {0,0,0,0,0,0,0,0}; 613 | NSVGpoint* p0, *p1; 614 | int j, s, e; 615 | 616 | // Build stroke edges 617 | if (closed) { 618 | // Looping 619 | p0 = &points[npoints-1]; 620 | p1 = &points[0]; 621 | s = 0; 622 | e = npoints; 623 | } else { 624 | // Add cap 625 | p0 = &points[0]; 626 | p1 = &points[1]; 627 | s = 1; 628 | e = npoints-1; 629 | } 630 | 631 | if (closed) { 632 | nsvg__initClosed(&left, &right, p0, p1, lineWidth); 633 | firstLeft = left; 634 | firstRight = right; 635 | } else { 636 | // Add cap 637 | float dx = p1->x - p0->x; 638 | float dy = p1->y - p0->y; 639 | nsvg__normalize(&dx, &dy); 640 | if (lineCap == NSVG_CAP_BUTT) 641 | nsvg__buttCap(r, &left, &right, p0, dx, dy, lineWidth, 0); 642 | else if (lineCap == NSVG_CAP_SQUARE) 643 | nsvg__squareCap(r, &left, &right, p0, dx, dy, lineWidth, 0); 644 | else if (lineCap == NSVG_CAP_ROUND) 645 | nsvg__roundCap(r, &left, &right, p0, dx, dy, lineWidth, ncap, 0); 646 | } 647 | 648 | for (j = s; j < e; ++j) { 649 | if (p1->flags & NSVG_PT_CORNER) { 650 | if (lineJoin == NSVG_JOIN_ROUND) 651 | nsvg__roundJoin(r, &left, &right, p0, p1, lineWidth, ncap); 652 | else if (lineJoin == NSVG_JOIN_BEVEL || (p1->flags & NSVG_PT_BEVEL)) 653 | nsvg__bevelJoin(r, &left, &right, p0, p1, lineWidth); 654 | else 655 | nsvg__miterJoin(r, &left, &right, p0, p1, lineWidth); 656 | } else { 657 | nsvg__straightJoin(r, &left, &right, p1, lineWidth); 658 | } 659 | p0 = p1++; 660 | } 661 | 662 | if (closed) { 663 | // Loop it 664 | nsvg__addEdge(r, firstLeft.x, firstLeft.y, left.x, left.y); 665 | nsvg__addEdge(r, right.x, right.y, firstRight.x, firstRight.y); 666 | } else { 667 | // Add cap 668 | float dx = p1->x - p0->x; 669 | float dy = p1->y - p0->y; 670 | nsvg__normalize(&dx, &dy); 671 | if (lineCap == NSVG_CAP_BUTT) 672 | nsvg__buttCap(r, &right, &left, p1, -dx, -dy, lineWidth, 1); 673 | else if (lineCap == NSVG_CAP_SQUARE) 674 | nsvg__squareCap(r, &right, &left, p1, -dx, -dy, lineWidth, 1); 675 | else if (lineCap == NSVG_CAP_ROUND) 676 | nsvg__roundCap(r, &right, &left, p1, -dx, -dy, lineWidth, ncap, 1); 677 | } 678 | } 679 | 680 | static void nsvg__prepareStroke(NSVGrasterizer* r, float miterLimit, int lineJoin) 681 | { 682 | int i, j; 683 | NSVGpoint* p0, *p1; 684 | 685 | p0 = &r->points[r->npoints-1]; 686 | p1 = &r->points[0]; 687 | for (i = 0; i < r->npoints; i++) { 688 | // Calculate segment direction and length 689 | p0->dx = p1->x - p0->x; 690 | p0->dy = p1->y - p0->y; 691 | p0->len = nsvg__normalize(&p0->dx, &p0->dy); 692 | // Advance 693 | p0 = p1++; 694 | } 695 | 696 | // calculate joins 697 | p0 = &r->points[r->npoints-1]; 698 | p1 = &r->points[0]; 699 | for (j = 0; j < r->npoints; j++) { 700 | float dlx0, dly0, dlx1, dly1, dmr2, cross; 701 | dlx0 = p0->dy; 702 | dly0 = -p0->dx; 703 | dlx1 = p1->dy; 704 | dly1 = -p1->dx; 705 | // Calculate extrusions 706 | p1->dmx = (dlx0 + dlx1) * 0.5f; 707 | p1->dmy = (dly0 + dly1) * 0.5f; 708 | dmr2 = p1->dmx*p1->dmx + p1->dmy*p1->dmy; 709 | if (dmr2 > 0.000001f) { 710 | float s2 = 1.0f / dmr2; 711 | if (s2 > 600.0f) { 712 | s2 = 600.0f; 713 | } 714 | p1->dmx *= s2; 715 | p1->dmy *= s2; 716 | } 717 | 718 | // Clear flags, but keep the corner. 719 | p1->flags = (p1->flags & NSVG_PT_CORNER) ? NSVG_PT_CORNER : 0; 720 | 721 | // Keep track of left turns. 722 | cross = p1->dx * p0->dy - p0->dx * p1->dy; 723 | if (cross > 0.0f) 724 | p1->flags |= NSVG_PT_LEFT; 725 | 726 | // Check to see if the corner needs to be beveled. 727 | if (p1->flags & NSVG_PT_CORNER) { 728 | if ((dmr2 * miterLimit*miterLimit) < 1.0f || lineJoin == NSVG_JOIN_BEVEL || lineJoin == NSVG_JOIN_ROUND) { 729 | p1->flags |= NSVG_PT_BEVEL; 730 | } 731 | } 732 | 733 | p0 = p1++; 734 | } 735 | } 736 | 737 | static void nsvg__flattenShapeStroke(NSVGrasterizer* r, NSVGshape* shape, float scale) 738 | { 739 | int i, j, closed; 740 | NSVGpath* path; 741 | NSVGpoint* p0, *p1; 742 | float miterLimit = shape->miterLimit; 743 | int lineJoin = shape->strokeLineJoin; 744 | int lineCap = shape->strokeLineCap; 745 | float lineWidth = shape->strokeWidth * scale; 746 | 747 | for (path = shape->paths; path != NULL; path = path->next) { 748 | // Flatten path 749 | r->npoints = 0; 750 | nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, NSVG_PT_CORNER); 751 | for (i = 0; i < path->npts-1; i += 3) { 752 | float* p = &path->pts[i*2]; 753 | nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, NSVG_PT_CORNER); 754 | } 755 | if (r->npoints < 2) 756 | continue; 757 | 758 | closed = path->closed; 759 | 760 | // If the first and last points are the same, remove the last, mark as closed path. 761 | p0 = &r->points[r->npoints-1]; 762 | p1 = &r->points[0]; 763 | if (nsvg__ptEquals(p0->x,p0->y, p1->x,p1->y, r->distTol)) { 764 | r->npoints--; 765 | p0 = &r->points[r->npoints-1]; 766 | closed = 1; 767 | } 768 | 769 | if (shape->strokeDashCount > 0) { 770 | int idash = 0, dashState = 1; 771 | float totalDist = 0, dashLen, allDashLen, dashOffset; 772 | NSVGpoint cur; 773 | 774 | if (closed) 775 | nsvg__appendPathPoint(r, r->points[0]); 776 | 777 | // Duplicate points -> points2. 778 | nsvg__duplicatePoints(r); 779 | 780 | r->npoints = 0; 781 | cur = r->points2[0]; 782 | nsvg__appendPathPoint(r, cur); 783 | 784 | // Figure out dash offset. 785 | allDashLen = 0; 786 | for (j = 0; j < shape->strokeDashCount; j++) 787 | allDashLen += shape->strokeDashArray[j]; 788 | if (shape->strokeDashCount & 1) 789 | allDashLen *= 2.0f; 790 | // Find location inside pattern 791 | dashOffset = fmodf(shape->strokeDashOffset, allDashLen); 792 | if (dashOffset < 0.0f) 793 | dashOffset += allDashLen; 794 | 795 | while (dashOffset > shape->strokeDashArray[idash]) { 796 | dashOffset -= shape->strokeDashArray[idash]; 797 | idash = (idash + 1) % shape->strokeDashCount; 798 | } 799 | dashLen = (shape->strokeDashArray[idash] - dashOffset) * scale; 800 | 801 | for (j = 1; j < r->npoints2; ) { 802 | float dx = r->points2[j].x - cur.x; 803 | float dy = r->points2[j].y - cur.y; 804 | float dist = sqrtf(dx*dx + dy*dy); 805 | 806 | if ((totalDist + dist) > dashLen) { 807 | // Calculate intermediate point 808 | float d = (dashLen - totalDist) / dist; 809 | float x = cur.x + dx * d; 810 | float y = cur.y + dy * d; 811 | nsvg__addPathPoint(r, x, y, NSVG_PT_CORNER); 812 | 813 | // Stroke 814 | if (r->npoints > 1 && dashState) { 815 | nsvg__prepareStroke(r, miterLimit, lineJoin); 816 | nsvg__expandStroke(r, r->points, r->npoints, 0, lineJoin, lineCap, lineWidth); 817 | } 818 | // Advance dash pattern 819 | dashState = !dashState; 820 | idash = (idash+1) % shape->strokeDashCount; 821 | dashLen = shape->strokeDashArray[idash] * scale; 822 | // Restart 823 | cur.x = x; 824 | cur.y = y; 825 | cur.flags = NSVG_PT_CORNER; 826 | totalDist = 0.0f; 827 | r->npoints = 0; 828 | nsvg__appendPathPoint(r, cur); 829 | } else { 830 | totalDist += dist; 831 | cur = r->points2[j]; 832 | nsvg__appendPathPoint(r, cur); 833 | j++; 834 | } 835 | } 836 | // Stroke any leftover path 837 | if (r->npoints > 1 && dashState) 838 | nsvg__expandStroke(r, r->points, r->npoints, 0, lineJoin, lineCap, lineWidth); 839 | } else { 840 | nsvg__prepareStroke(r, miterLimit, lineJoin); 841 | nsvg__expandStroke(r, r->points, r->npoints, closed, lineJoin, lineCap, lineWidth); 842 | } 843 | } 844 | } 845 | 846 | static int nsvg__cmpEdge(const void *p, const void *q) 847 | { 848 | const NSVGedge* a = (const NSVGedge*)p; 849 | const NSVGedge* b = (const NSVGedge*)q; 850 | 851 | if (a->y0 < b->y0) return -1; 852 | if (a->y0 > b->y0) return 1; 853 | return 0; 854 | } 855 | 856 | 857 | static NSVGactiveEdge* nsvg__addActive(NSVGrasterizer* r, NSVGedge* e, float startPoint) 858 | { 859 | NSVGactiveEdge* z; 860 | 861 | if (r->freelist != NULL) { 862 | // Restore from freelist. 863 | z = r->freelist; 864 | r->freelist = z->next; 865 | } else { 866 | // Alloc new edge. 867 | z = (NSVGactiveEdge*)nsvg__alloc(r, sizeof(NSVGactiveEdge)); 868 | if (z == NULL) return NULL; 869 | } 870 | 871 | float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); 872 | // STBTT_assert(e->y0 <= start_point); 873 | // round dx down to avoid going too far 874 | if (dxdy < 0) 875 | z->dx = (int)(-floorf(NSVG__FIX * -dxdy)); 876 | else 877 | z->dx = (int)floorf(NSVG__FIX * dxdy); 878 | z->x = (int)floorf(NSVG__FIX * (e->x0 + dxdy * (startPoint - e->y0))); 879 | // z->x -= off_x * FIX; 880 | z->ey = e->y1; 881 | z->next = 0; 882 | z->dir = e->dir; 883 | 884 | return z; 885 | } 886 | 887 | static void nsvg__freeActive(NSVGrasterizer* r, NSVGactiveEdge* z) 888 | { 889 | z->next = r->freelist; 890 | r->freelist = z; 891 | } 892 | 893 | static void nsvg__fillScanline(unsigned char* scanline, int len, int x0, int x1, int maxWeight, int* xmin, int* xmax) 894 | { 895 | int i = x0 >> NSVG__FIXSHIFT; 896 | int j = x1 >> NSVG__FIXSHIFT; 897 | if (i < *xmin) *xmin = i; 898 | if (j > *xmax) *xmax = j; 899 | if (i < len && j >= 0) { 900 | if (i == j) { 901 | // x0,x1 are the same pixel, so compute combined coverage 902 | scanline[i] = (unsigned char)(scanline[i] + ((x1 - x0) * maxWeight >> NSVG__FIXSHIFT)); 903 | } else { 904 | if (i >= 0) // add antialiasing for x0 905 | scanline[i] = (unsigned char)(scanline[i] + (((NSVG__FIX - (x0 & NSVG__FIXMASK)) * maxWeight) >> NSVG__FIXSHIFT)); 906 | else 907 | i = -1; // clip 908 | 909 | if (j < len) // add antialiasing for x1 910 | scanline[j] = (unsigned char)(scanline[j] + (((x1 & NSVG__FIXMASK) * maxWeight) >> NSVG__FIXSHIFT)); 911 | else 912 | j = len; // clip 913 | 914 | for (++i; i < j; ++i) // fill pixels between x0 and x1 915 | scanline[i] = (unsigned char)(scanline[i] + maxWeight); 916 | } 917 | } 918 | } 919 | 920 | // note: this routine clips fills that extend off the edges... ideally this 921 | // wouldn't happen, but it could happen if the truetype glyph bounding boxes 922 | // are wrong, or if the user supplies a too-small bitmap 923 | static void nsvg__fillActiveEdges(unsigned char* scanline, int len, NSVGactiveEdge* e, int maxWeight, int* xmin, int* xmax, char fillRule) 924 | { 925 | // non-zero winding fill 926 | int x0 = 0, w = 0; 927 | 928 | if (fillRule == NSVG_FILLRULE_NONZERO) { 929 | // Non-zero 930 | while (e != NULL) { 931 | if (w == 0) { 932 | // if we're currently at zero, we need to record the edge start point 933 | x0 = e->x; w += e->dir; 934 | } else { 935 | int x1 = e->x; w += e->dir; 936 | // if we went to zero, we need to draw 937 | if (w == 0) 938 | nsvg__fillScanline(scanline, len, x0, x1, maxWeight, xmin, xmax); 939 | } 940 | e = e->next; 941 | } 942 | } else if (fillRule == NSVG_FILLRULE_EVENODD) { 943 | // Even-odd 944 | while (e != NULL) { 945 | if (w == 0) { 946 | // if we're currently at zero, we need to record the edge start point 947 | x0 = e->x; w = 1; 948 | } else { 949 | int x1 = e->x; w = 0; 950 | nsvg__fillScanline(scanline, len, x0, x1, maxWeight, xmin, xmax); 951 | } 952 | e = e->next; 953 | } 954 | } 955 | } 956 | 957 | static float nsvg__clampf(float a, float mn, float mx) { return a < mn ? mn : (a > mx ? mx : a); } 958 | 959 | static unsigned int nsvg__RGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a) 960 | { 961 | return ((unsigned int)r) | ((unsigned int)g << 8) | ((unsigned int)b << 16) | ((unsigned int)a << 24); 962 | } 963 | 964 | static unsigned int nsvg__lerpRGBA(unsigned int c0, unsigned int c1, float u) 965 | { 966 | int iu = (int)(nsvg__clampf(u, 0.0f, 1.0f) * 256.0f); 967 | int r = (((c0) & 0xff)*(256-iu) + (((c1) & 0xff)*iu)) >> 8; 968 | int g = (((c0>>8) & 0xff)*(256-iu) + (((c1>>8) & 0xff)*iu)) >> 8; 969 | int b = (((c0>>16) & 0xff)*(256-iu) + (((c1>>16) & 0xff)*iu)) >> 8; 970 | int a = (((c0>>24) & 0xff)*(256-iu) + (((c1>>24) & 0xff)*iu)) >> 8; 971 | return nsvg__RGBA((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a); 972 | } 973 | 974 | static unsigned int nsvg__applyOpacity(unsigned int c, float u) 975 | { 976 | int iu = (int)(nsvg__clampf(u, 0.0f, 1.0f) * 256.0f); 977 | int r = (c) & 0xff; 978 | int g = (c>>8) & 0xff; 979 | int b = (c>>16) & 0xff; 980 | int a = (((c>>24) & 0xff)*iu) >> 8; 981 | return nsvg__RGBA((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a); 982 | } 983 | 984 | static inline int nsvg__div255(int x) 985 | { 986 | return ((x+1) * 257) >> 16; 987 | } 988 | 989 | static void nsvg__scanlineSolid(unsigned char* dst, int count, unsigned char* cover, int x, int y, 990 | float tx, float ty, float scale, NSVGcachedPaint* cache) 991 | { 992 | 993 | if (cache->type == NSVG_PAINT_COLOR) { 994 | int i, cr, cg, cb, ca; 995 | cr = cache->colors[0] & 0xff; 996 | cg = (cache->colors[0] >> 8) & 0xff; 997 | cb = (cache->colors[0] >> 16) & 0xff; 998 | ca = (cache->colors[0] >> 24) & 0xff; 999 | 1000 | for (i = 0; i < count; i++) { 1001 | int r,g,b; 1002 | int a = nsvg__div255((int)cover[0] * ca); 1003 | int ia = 255 - a; 1004 | // Premultiply 1005 | r = nsvg__div255(cr * a); 1006 | g = nsvg__div255(cg * a); 1007 | b = nsvg__div255(cb * a); 1008 | 1009 | // Blend over 1010 | r += nsvg__div255(ia * (int)dst[0]); 1011 | g += nsvg__div255(ia * (int)dst[1]); 1012 | b += nsvg__div255(ia * (int)dst[2]); 1013 | a += nsvg__div255(ia * (int)dst[3]); 1014 | 1015 | dst[0] = (unsigned char)r; 1016 | dst[1] = (unsigned char)g; 1017 | dst[2] = (unsigned char)b; 1018 | dst[3] = (unsigned char)a; 1019 | 1020 | cover++; 1021 | dst += 4; 1022 | } 1023 | } else if (cache->type == NSVG_PAINT_LINEAR_GRADIENT) { 1024 | // TODO: spread modes. 1025 | // TODO: plenty of opportunities to optimize. 1026 | float fx, fy, dx, gy; 1027 | float* t = cache->xform; 1028 | int i, cr, cg, cb, ca; 1029 | unsigned int c; 1030 | 1031 | fx = ((float)x - tx) / scale; 1032 | fy = ((float)y - ty) / scale; 1033 | dx = 1.0f / scale; 1034 | 1035 | for (i = 0; i < count; i++) { 1036 | int r,g,b,a,ia; 1037 | gy = fx*t[1] + fy*t[3] + t[5]; 1038 | c = cache->colors[(int)nsvg__clampf(gy*255.0f, 0, 255.0f)]; 1039 | cr = (c) & 0xff; 1040 | cg = (c >> 8) & 0xff; 1041 | cb = (c >> 16) & 0xff; 1042 | ca = (c >> 24) & 0xff; 1043 | 1044 | a = nsvg__div255((int)cover[0] * ca); 1045 | ia = 255 - a; 1046 | 1047 | // Premultiply 1048 | r = nsvg__div255(cr * a); 1049 | g = nsvg__div255(cg * a); 1050 | b = nsvg__div255(cb * a); 1051 | 1052 | // Blend over 1053 | r += nsvg__div255(ia * (int)dst[0]); 1054 | g += nsvg__div255(ia * (int)dst[1]); 1055 | b += nsvg__div255(ia * (int)dst[2]); 1056 | a += nsvg__div255(ia * (int)dst[3]); 1057 | 1058 | dst[0] = (unsigned char)r; 1059 | dst[1] = (unsigned char)g; 1060 | dst[2] = (unsigned char)b; 1061 | dst[3] = (unsigned char)a; 1062 | 1063 | cover++; 1064 | dst += 4; 1065 | fx += dx; 1066 | } 1067 | } else if (cache->type == NSVG_PAINT_RADIAL_GRADIENT) { 1068 | // TODO: spread modes. 1069 | // TODO: plenty of opportunities to optimize. 1070 | // TODO: focus (fx,fy) 1071 | float fx, fy, dx, gx, gy, gd; 1072 | float* t = cache->xform; 1073 | int i, cr, cg, cb, ca; 1074 | unsigned int c; 1075 | 1076 | fx = ((float)x - tx) / scale; 1077 | fy = ((float)y - ty) / scale; 1078 | dx = 1.0f / scale; 1079 | 1080 | for (i = 0; i < count; i++) { 1081 | int r,g,b,a,ia; 1082 | gx = fx*t[0] + fy*t[2] + t[4]; 1083 | gy = fx*t[1] + fy*t[3] + t[5]; 1084 | gd = sqrtf(gx*gx + gy*gy); 1085 | c = cache->colors[(int)nsvg__clampf(gd*255.0f, 0, 255.0f)]; 1086 | cr = (c) & 0xff; 1087 | cg = (c >> 8) & 0xff; 1088 | cb = (c >> 16) & 0xff; 1089 | ca = (c >> 24) & 0xff; 1090 | 1091 | a = nsvg__div255((int)cover[0] * ca); 1092 | ia = 255 - a; 1093 | 1094 | // Premultiply 1095 | r = nsvg__div255(cr * a); 1096 | g = nsvg__div255(cg * a); 1097 | b = nsvg__div255(cb * a); 1098 | 1099 | // Blend over 1100 | r += nsvg__div255(ia * (int)dst[0]); 1101 | g += nsvg__div255(ia * (int)dst[1]); 1102 | b += nsvg__div255(ia * (int)dst[2]); 1103 | a += nsvg__div255(ia * (int)dst[3]); 1104 | 1105 | dst[0] = (unsigned char)r; 1106 | dst[1] = (unsigned char)g; 1107 | dst[2] = (unsigned char)b; 1108 | dst[3] = (unsigned char)a; 1109 | 1110 | cover++; 1111 | dst += 4; 1112 | fx += dx; 1113 | } 1114 | } 1115 | } 1116 | 1117 | static void nsvg__rasterizeSortedEdges(NSVGrasterizer *r, float tx, float ty, float scale, NSVGcachedPaint* cache, char fillRule) 1118 | { 1119 | NSVGactiveEdge *active = NULL; 1120 | int y, s; 1121 | int e = 0; 1122 | int maxWeight = (255 / NSVG__SUBSAMPLES); // weight per vertical scanline 1123 | int xmin, xmax; 1124 | 1125 | for (y = 0; y < r->height; y++) { 1126 | memset(r->scanline, 0, r->width); 1127 | xmin = r->width; 1128 | xmax = 0; 1129 | for (s = 0; s < NSVG__SUBSAMPLES; ++s) { 1130 | // find center of pixel for this scanline 1131 | float scany = (float)(y*NSVG__SUBSAMPLES + s) + 0.5f; 1132 | NSVGactiveEdge **step = &active; 1133 | 1134 | // update all active edges; 1135 | // remove all active edges that terminate before the center of this scanline 1136 | while (*step) { 1137 | NSVGactiveEdge *z = *step; 1138 | if (z->ey <= scany) { 1139 | *step = z->next; // delete from list 1140 | // NSVG__assert(z->valid); 1141 | nsvg__freeActive(r, z); 1142 | } else { 1143 | z->x += z->dx; // advance to position for current scanline 1144 | step = &((*step)->next); // advance through list 1145 | } 1146 | } 1147 | 1148 | // resort the list if needed 1149 | for (;;) { 1150 | int changed = 0; 1151 | step = &active; 1152 | while (*step && (*step)->next) { 1153 | if ((*step)->x > (*step)->next->x) { 1154 | NSVGactiveEdge* t = *step; 1155 | NSVGactiveEdge* q = t->next; 1156 | t->next = q->next; 1157 | q->next = t; 1158 | *step = q; 1159 | changed = 1; 1160 | } 1161 | step = &(*step)->next; 1162 | } 1163 | if (!changed) break; 1164 | } 1165 | 1166 | // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline 1167 | while (e < r->nedges && r->edges[e].y0 <= scany) { 1168 | if (r->edges[e].y1 > scany) { 1169 | NSVGactiveEdge* z = nsvg__addActive(r, &r->edges[e], scany); 1170 | if (z == NULL) break; 1171 | // find insertion point 1172 | if (active == NULL) { 1173 | active = z; 1174 | } else if (z->x < active->x) { 1175 | // insert at front 1176 | z->next = active; 1177 | active = z; 1178 | } else { 1179 | // find thing to insert AFTER 1180 | NSVGactiveEdge* p = active; 1181 | while (p->next && p->next->x < z->x) 1182 | p = p->next; 1183 | // at this point, p->next->x is NOT < z->x 1184 | z->next = p->next; 1185 | p->next = z; 1186 | } 1187 | } 1188 | e++; 1189 | } 1190 | 1191 | // now process all active edges in non-zero fashion 1192 | if (active != NULL) 1193 | nsvg__fillActiveEdges(r->scanline, r->width, active, maxWeight, &xmin, &xmax, fillRule); 1194 | } 1195 | // Blit 1196 | if (xmin < 0) xmin = 0; 1197 | if (xmax > r->width-1) xmax = r->width-1; 1198 | if (xmin <= xmax) { 1199 | nsvg__scanlineSolid(&r->bitmap[y * r->stride] + xmin*4, xmax-xmin+1, &r->scanline[xmin], xmin, y, tx,ty, scale, cache); 1200 | } 1201 | } 1202 | 1203 | } 1204 | 1205 | static void nsvg__unpremultiplyAlpha(unsigned char* image, int w, int h, int stride) 1206 | { 1207 | int x,y; 1208 | 1209 | // Unpremultiply 1210 | for (y = 0; y < h; y++) { 1211 | unsigned char *row = &image[y*stride]; 1212 | for (x = 0; x < w; x++) { 1213 | int r = row[0], g = row[1], b = row[2], a = row[3]; 1214 | if (a != 0) { 1215 | row[0] = (unsigned char)(r*255/a); 1216 | row[1] = (unsigned char)(g*255/a); 1217 | row[2] = (unsigned char)(b*255/a); 1218 | } 1219 | row += 4; 1220 | } 1221 | } 1222 | 1223 | // Defringe 1224 | for (y = 0; y < h; y++) { 1225 | unsigned char *row = &image[y*stride]; 1226 | for (x = 0; x < w; x++) { 1227 | int r = 0, g = 0, b = 0, a = row[3], n = 0; 1228 | if (a == 0) { 1229 | if (x-1 > 0 && row[-1] != 0) { 1230 | r += row[-4]; 1231 | g += row[-3]; 1232 | b += row[-2]; 1233 | n++; 1234 | } 1235 | if (x+1 < w && row[7] != 0) { 1236 | r += row[4]; 1237 | g += row[5]; 1238 | b += row[6]; 1239 | n++; 1240 | } 1241 | if (y-1 > 0 && row[-stride+3] != 0) { 1242 | r += row[-stride]; 1243 | g += row[-stride+1]; 1244 | b += row[-stride+2]; 1245 | n++; 1246 | } 1247 | if (y+1 < h && row[stride+3] != 0) { 1248 | r += row[stride]; 1249 | g += row[stride+1]; 1250 | b += row[stride+2]; 1251 | n++; 1252 | } 1253 | if (n > 0) { 1254 | row[0] = (unsigned char)(r/n); 1255 | row[1] = (unsigned char)(g/n); 1256 | row[2] = (unsigned char)(b/n); 1257 | } 1258 | } 1259 | row += 4; 1260 | } 1261 | } 1262 | } 1263 | 1264 | 1265 | static void nsvg__initPaint(NSVGcachedPaint* cache, NSVGpaint* paint, float opacity) 1266 | { 1267 | int i, j; 1268 | NSVGgradient* grad; 1269 | 1270 | cache->type = paint->type; 1271 | 1272 | if (paint->type == NSVG_PAINT_COLOR) { 1273 | cache->colors[0] = nsvg__applyOpacity(paint->color, opacity); 1274 | return; 1275 | } 1276 | 1277 | grad = paint->gradient; 1278 | 1279 | cache->spread = grad->spread; 1280 | memcpy(cache->xform, grad->xform, sizeof(float)*6); 1281 | 1282 | if (grad->nstops == 0) { 1283 | for (i = 0; i < 256; i++) 1284 | cache->colors[i] = 0; 1285 | } if (grad->nstops == 1) { 1286 | for (i = 0; i < 256; i++) 1287 | cache->colors[i] = nsvg__applyOpacity(grad->stops[i].color, opacity); 1288 | } else { 1289 | unsigned int ca, cb = 0; 1290 | float ua, ub, du, u; 1291 | int ia, ib, count; 1292 | 1293 | ca = nsvg__applyOpacity(grad->stops[0].color, opacity); 1294 | ua = nsvg__clampf(grad->stops[0].offset, 0, 1); 1295 | ub = nsvg__clampf(grad->stops[grad->nstops-1].offset, ua, 1); 1296 | ia = (int)(ua * 255.0f); 1297 | ib = (int)(ub * 255.0f); 1298 | for (i = 0; i < ia; i++) { 1299 | cache->colors[i] = ca; 1300 | } 1301 | 1302 | for (i = 0; i < grad->nstops-1; i++) { 1303 | ca = nsvg__applyOpacity(grad->stops[i].color, opacity); 1304 | cb = nsvg__applyOpacity(grad->stops[i+1].color, opacity); 1305 | ua = nsvg__clampf(grad->stops[i].offset, 0, 1); 1306 | ub = nsvg__clampf(grad->stops[i+1].offset, 0, 1); 1307 | ia = (int)(ua * 255.0f); 1308 | ib = (int)(ub * 255.0f); 1309 | count = ib - ia; 1310 | if (count <= 0) continue; 1311 | u = 0; 1312 | du = 1.0f / (float)count; 1313 | for (j = 0; j < count; j++) { 1314 | cache->colors[ia+j] = nsvg__lerpRGBA(ca,cb,u); 1315 | u += du; 1316 | } 1317 | } 1318 | 1319 | for (i = ib; i < 256; i++) 1320 | cache->colors[i] = cb; 1321 | } 1322 | 1323 | } 1324 | 1325 | /* 1326 | static void dumpEdges(NSVGrasterizer* r, const char* name) 1327 | { 1328 | float xmin = 0, xmax = 0, ymin = 0, ymax = 0; 1329 | NSVGedge *e = NULL; 1330 | int i; 1331 | if (r->nedges == 0) return; 1332 | FILE* fp = fopen(name, "w"); 1333 | if (fp == NULL) return; 1334 | 1335 | xmin = xmax = r->edges[0].x0; 1336 | ymin = ymax = r->edges[0].y0; 1337 | for (i = 0; i < r->nedges; i++) { 1338 | e = &r->edges[i]; 1339 | xmin = nsvg__minf(xmin, e->x0); 1340 | xmin = nsvg__minf(xmin, e->x1); 1341 | xmax = nsvg__maxf(xmax, e->x0); 1342 | xmax = nsvg__maxf(xmax, e->x1); 1343 | ymin = nsvg__minf(ymin, e->y0); 1344 | ymin = nsvg__minf(ymin, e->y1); 1345 | ymax = nsvg__maxf(ymax, e->y0); 1346 | ymax = nsvg__maxf(ymax, e->y1); 1347 | } 1348 | 1349 | fprintf(fp, "", xmin, ymin, (xmax - xmin), (ymax - ymin)); 1350 | 1351 | for (i = 0; i < r->nedges; i++) { 1352 | e = &r->edges[i]; 1353 | fprintf(fp ,"", e->x0,e->y0, e->x1,e->y1); 1354 | } 1355 | 1356 | for (i = 0; i < r->npoints; i++) { 1357 | if (i+1 < r->npoints) 1358 | fprintf(fp ,"", r->points[i].x, r->points[i].y, r->points[i+1].x, r->points[i+1].y); 1359 | fprintf(fp ,"", r->points[i].x, r->points[i].y, r->points[i].flags == 0 ? "#f00" : "#0f0"); 1360 | } 1361 | 1362 | fprintf(fp, ""); 1363 | fclose(fp); 1364 | } 1365 | */ 1366 | 1367 | void nsvgRasterize(NSVGrasterizer* r, 1368 | NSVGimage* image, float tx, float ty, float scale, 1369 | unsigned char* dst, int w, int h, int stride) 1370 | { 1371 | NSVGshape *shape = NULL; 1372 | NSVGedge *e = NULL; 1373 | NSVGcachedPaint cache; 1374 | int i; 1375 | 1376 | r->bitmap = dst; 1377 | r->width = w; 1378 | r->height = h; 1379 | r->stride = stride; 1380 | 1381 | if (w > r->cscanline) { 1382 | r->cscanline = w; 1383 | r->scanline = (unsigned char*)realloc(r->scanline, w); 1384 | if (r->scanline == NULL) return; 1385 | } 1386 | 1387 | for (i = 0; i < h; i++) 1388 | memset(&dst[i*stride], 0, w*4); 1389 | 1390 | for (shape = image->shapes; shape != NULL; shape = shape->next) { 1391 | if (!(shape->flags & NSVG_FLAGS_VISIBLE)) 1392 | continue; 1393 | 1394 | if (shape->fill.type != NSVG_PAINT_NONE) { 1395 | nsvg__resetPool(r); 1396 | r->freelist = NULL; 1397 | r->nedges = 0; 1398 | 1399 | nsvg__flattenShape(r, shape, scale); 1400 | 1401 | // Scale and translate edges 1402 | for (i = 0; i < r->nedges; i++) { 1403 | e = &r->edges[i]; 1404 | e->x0 = tx + e->x0; 1405 | e->y0 = (ty + e->y0) * NSVG__SUBSAMPLES; 1406 | e->x1 = tx + e->x1; 1407 | e->y1 = (ty + e->y1) * NSVG__SUBSAMPLES; 1408 | } 1409 | 1410 | // Rasterize edges 1411 | if (r->nedges != 0) 1412 | qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); 1413 | 1414 | // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule 1415 | nsvg__initPaint(&cache, &shape->fill, shape->opacity); 1416 | 1417 | nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, shape->fillRule); 1418 | } 1419 | if (shape->stroke.type != NSVG_PAINT_NONE && (shape->strokeWidth * scale) > 0.01f) { 1420 | nsvg__resetPool(r); 1421 | r->freelist = NULL; 1422 | r->nedges = 0; 1423 | 1424 | nsvg__flattenShapeStroke(r, shape, scale); 1425 | 1426 | // dumpEdges(r, "edge.svg"); 1427 | 1428 | // Scale and translate edges 1429 | for (i = 0; i < r->nedges; i++) { 1430 | e = &r->edges[i]; 1431 | e->x0 = tx + e->x0; 1432 | e->y0 = (ty + e->y0) * NSVG__SUBSAMPLES; 1433 | e->x1 = tx + e->x1; 1434 | e->y1 = (ty + e->y1) * NSVG__SUBSAMPLES; 1435 | } 1436 | 1437 | // Rasterize edges 1438 | if (r->nedges != 0) 1439 | qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); 1440 | 1441 | // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule 1442 | nsvg__initPaint(&cache, &shape->stroke, shape->opacity); 1443 | 1444 | nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, NSVG_FILLRULE_NONZERO); 1445 | } 1446 | } 1447 | 1448 | nsvg__unpremultiplyAlpha(dst, w, h, stride); 1449 | 1450 | r->bitmap = NULL; 1451 | r->width = 0; 1452 | r->height = 0; 1453 | r->stride = 0; 1454 | } 1455 | 1456 | #endif // NANOSVGRAST_IMPLEMENTATION 1457 | 1458 | #endif // NANOSVGRAST_H 1459 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dmenu", 3 | "version": "1", 4 | "dependencies": [ 5 | "fast-cpp-csv-parser", 6 | "rsm-binary-io", 7 | "simpleini", 8 | "spdlog", 9 | "xbyak", 10 | "rapidcsv", 11 | "nlohmann-json", 12 | { 13 | "name": "imgui", 14 | "features": [ 15 | "dx11-binding", 16 | "win32-binding" 17 | ] 18 | } 19 | ] 20 | } 21 | --------------------------------------------------------------------------------