├── .gitignore ├── BUILDING.md ├── Example.png ├── LICENSE ├── README.md ├── arcdps_healing_stats ├── arcdps_healing_stats.vcxproj ├── arcdps_healing_stats.vcxproj.filters ├── arcdps_healing_stats.vcxproj.user └── dllmain.cpp ├── arcdps_personal_stats.rc ├── arcdps_personal_stats.sln ├── arcdps_personal_stats.vcxproj ├── arcdps_personal_stats.vcxproj.filters ├── arcdps_personal_stats.vcxproj.user ├── arcdps_personal_stats_build_all.proj ├── evtc_rpc_server ├── evtc_rpc_server.vcxproj ├── evtc_rpc_server.vcxproj.filters ├── evtc_rpc_server.vcxproj.user ├── linux_versions_auto.h └── main.cpp ├── get_dependency_versions.py ├── icons └── specs │ ├── 000.png │ ├── 001.png │ ├── 002.png │ ├── 003.png │ ├── 004.png │ ├── 005.png │ ├── 006.png │ ├── 007.png │ ├── 008.png │ ├── 009.png │ ├── e101.png │ ├── e102.png │ ├── e103.png │ ├── e104.png │ ├── e201.png │ ├── e202.png │ ├── e203.png │ ├── e204.png │ ├── e301.png │ ├── e302.png │ ├── e303.png │ ├── e304.png │ ├── e401.png │ ├── e402.png │ ├── e403.png │ ├── e404.png │ ├── e501.png │ ├── e502.png │ ├── e503.png │ ├── e504.png │ ├── e601.png │ ├── e602.png │ ├── e603.png │ ├── e604.png │ ├── e701.png │ ├── e702.png │ ├── e703.png │ ├── e704.png │ ├── e801.png │ ├── e802.png │ ├── e803.png │ ├── e804.png │ ├── e901.png │ ├── e902.png │ ├── e903.png │ └── e904.png ├── networking ├── Client.cpp ├── Client.h ├── Server.cpp ├── Server.h ├── ServerStatistics.cpp ├── ServerStatistics.h ├── build_proto.py ├── evtc_rpc.proto ├── evtc_rpc_messages.h ├── networking.vcxproj ├── networking.vcxproj.filters └── networking.vcxproj.user ├── release.py ├── resource.h ├── simpleini ├── LICENCE.txt └── SimpleIni.h ├── src ├── AddonVersion.h ├── AgentTable.cpp ├── AgentTable.h ├── AggregatedStats.cpp ├── AggregatedStats.h ├── AggregatedStatsCollection.cpp ├── AggregatedStatsCollection.h ├── Common.h ├── EventProcessor.cpp ├── EventProcessor.h ├── EventSequencer.cpp ├── EventSequencer.h ├── Exports.h ├── GUI.cpp ├── GUI.h ├── ImGuiEx.cpp ├── ImGuiEx.h ├── Log.cpp ├── Log.h ├── Options.cpp ├── Options.h ├── PlayerStats.cpp ├── PlayerStats.h ├── Skills.cpp ├── Skills.h ├── SpecializationData.h ├── State.h ├── UpdateGUI.cpp ├── UpdateGUI.h ├── Utilities.h └── dllmain.cpp ├── test ├── ConfigTest.cpp ├── EnvironmentTest.cpp ├── EventProcessorTest.cpp ├── GUITest.cpp ├── LocalStatsTest.cpp ├── NetworkTest.cpp ├── StressTest.cpp ├── UtilitiesTest.cpp ├── main.cpp ├── test.vcxproj ├── test.vcxproj.user └── xevtc_logs │ ├── berserker_solo.xevtc │ ├── druid_MO.xevtc │ ├── druid_solo.xevtc │ ├── null_names.xevtc │ └── renegade_solo.xevtc ├── vcpkg-configuration.json ├── vcpkg.json ├── vcpkg_install_dependencies ├── vcpkg_install_dependencies.vcxproj ├── vcpkg_install_dependencies.vcxproj.filters └── vcpkg_install_dependencies.vcxproj.user └── xmake.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | x64 3 | x86 4 | build 5 | .xmake 6 | ~AutoRecover* 7 | *.ini 8 | *.mock 9 | *.log 10 | *.aps 11 | *.txt 12 | *.user 13 | packages 14 | vcpkg_installed -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # Building the project 2 | ### Prerequisites: 3 | Visual Studio 2022, Version 17.6.3 was working for me at the time of writing 4 | vcpkg (installed with visual studio integration), Version 2023.04.15 was working for me at the time of writing 5 | Python, Version 3.9 was working for me at the time of writing 6 | 7 | ### Clone the project 8 | --recursive is important so that it clones submodules 9 | ``` 10 | git clone --recursive https://github.com/Krappa322/arcdps_healing_stats 11 | ``` 12 | 13 | ### Build the addon 14 | Open up arcdps_personal_stats.sln with Visual Studio. Choose mode (Release/Debug) and build the solution. You might have to rebuild only the "vcpkg_install_dependencies" project first before building the whole solution, to ensure that some of the tools needed for building (grpc, protobuf) are downloaded first. 15 | 16 | ### Running tests 17 | Set test.vcxproj as startup project, and run "Local Windows Debugger". You can also run test.exe from in the output directory 18 | -------------------------------------------------------------------------------- /Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/Example.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Kappa322 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArcDPS Healing Stats 2 | [![downloads](https://img.shields.io/github/downloads/Krappa322/arcdps_healing_stats/total)](https://github.com/Krappa322/arcdps_healing_stats/releases/latest) 3 | 4 | Show healing statistics based on your local stats (i.e. your own healing output). 5 | 6 | This includes outgoing healing per agent and per skill, as well as filtering to only include your own subgroup/squad or to exclude minions. Format of the window title and contents are fully configurable and windows can be configured to show different data (targets healed, skills used to heal, total healing). 7 | 8 | If live stats sharing is enabled, this addon also allows you to see other players in your squads healing stats (and them to see yours) 9 | 10 | Also logs healing to the arcdps evtc, allowing evtc parsers to show healing stats. 11 | 12 | ## Installation 13 | Requires [ArcDPS](https://www.deltaconnected.com/arcdps/). 14 | 15 | Download the latest [arcdps_healing_stats.dll](https://github.com/Krappa322/arcdps_healing_stats/releases/latest) from the Releases page. Drag and drop arcdps_healing_stats.dll into the same directory as arcdps (d3d11.dll) 16 | 17 | ## Usage 18 | Toggle any window under the "Heal Stats" menu in the arcdps main menu. Right-click the window to see more options and hover over options to see what they do. All configuration is done per window, and there can be up to 10 different windows. 19 | 20 | To enable live-sharing of healing stats, allowing you to see others healing stats (and them to see yours), go to "Heal Stats Options" and check the checkbox for "enable live stats sharing" 21 | 22 | ## Issues and requests 23 | Please report issues and requests using the github issue tracker 24 | 25 | ## Pictures 26 | ![Example](./Example.png) 27 | 28 | ## Technical information 29 | This addon uses the local stats provided by ArcDPS to count healing done. This information is only available for the local player, i.e. the server does not notify about healing done by other players to other players. As such it is not possible to extend the addon to include everyone's healing without every player in the instance having the addon installed. 30 | 31 | ## Planned features 32 | - Displaying healing done per time skill was cast 33 | - Track overhealing. This is kind of hard because it would require simulating healing events and how much they should be healing (which requires knowing all modifiers and such). Please open an issue with suggestions for how to do this if you know of any :) 34 | - Store history similar to that available in the vanilla ArcDPS dps window (i.e. show statistics for previous encounters) 35 | - Display a graph showing healing done over time (allowing visualisation of when healing pressure is high) 36 | - More statistics than just healing. Confusion comes to mind, since the 10 man log method mentioned above could in the case of confusion create a log that shows true dps done for bosses where self stats and area stats don't match (such as Soulless Horror) 37 | 38 | ## Copyright Notice 39 | This project is licensed under the MIT license (see the LICENSE file for more details). It makes use of the following third party libraries (they are all statically linked): 40 | ### arcdps-extension 41 | [arcdps-extension](https://github.com/knoxfighter/arcdps-extension), licensed under the MIT license and included in this project as a git submodule of [arcdps_mock](/arcdps_mock). 42 | ### Dear ImGui 43 | [Dear ImGui](https://github.com/ocornut/imgui), licensed under the MIT license and included in this project as a git submodule [imgui](/imgui). 44 | ### GoogleTest 45 | [GoogleTest](https://github.com/google/googletest), licensed under the BSD-3-Clause license and included in this project as a linking/header dependency (provided through vcpkg). 46 | ### gRPC 47 | [gRPC](https://github.com/grpc/grpc), licensed under the Apache-2.0 license and included in this project as a linking/header dependency (provided through vcpkg). 48 | ### JSON for Modern C++ 49 | [JSON for Modern C++](https://github.com/nlohmann/json), licensed under the MIT license and included in this project as a linking/header dependency (provided through vcpkg). 50 | ### Prometheus Client Library for Modern C++ 51 | [prometheus-cpp](https://github.com/jupp0r/prometheus-cpp), licensed under the MIT license and included in this project as a linking/header dependency (provided through vcpkg). 52 | ### Protocol Buffers 53 | [Protocol Buffers](https://github.com/protocolbuffers/protobuf), licensed under the BSD-3-Clause license and included in this project as a linking/header dependency (provided through vcpkg). 54 | ### simpleini 55 | [simpleini](https://github.com/brofield/simpleini), licensed under the MIT license and included in this project as a directory [simpleini](/simpleini). 56 | ### spdlog 57 | [spdlog](https://github.com/gabime/spdlog), licensed under the MIT license and included in this project as a linking/header dependency (provided through vcpkg). 58 | -------------------------------------------------------------------------------- /arcdps_healing_stats/arcdps_healing_stats.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | 23 | 24 | Header Files 25 | 26 | 27 | 28 | 29 | Resource Files 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Resource Files 38 | 39 | 40 | Resource Files 41 | 42 | 43 | Resource Files 44 | 45 | 46 | Resource Files 47 | 48 | 49 | Resource Files 50 | 51 | 52 | Resource Files 53 | 54 | 55 | Resource Files 56 | 57 | 58 | Resource Files 59 | 60 | 61 | Resource Files 62 | 63 | 64 | Resource Files 65 | 66 | 67 | Resource Files 68 | 69 | 70 | Resource Files 71 | 72 | 73 | Resource Files 74 | 75 | 76 | Resource Files 77 | 78 | 79 | Resource Files 80 | 81 | 82 | Resource Files 83 | 84 | 85 | Resource Files 86 | 87 | 88 | Resource Files 89 | 90 | 91 | Resource Files 92 | 93 | 94 | Resource Files 95 | 96 | 97 | Resource Files 98 | 99 | 100 | Resource Files 101 | 102 | 103 | Resource Files 104 | 105 | 106 | Resource Files 107 | 108 | 109 | Resource Files 110 | 111 | 112 | Resource Files 113 | 114 | 115 | Resource Files 116 | 117 | 118 | Resource Files 119 | 120 | 121 | Resource Files 122 | 123 | 124 | Resource Files 125 | 126 | 127 | Resource Files 128 | 129 | 130 | Resource Files 131 | 132 | 133 | Resource Files 134 | 135 | 136 | Resource Files 137 | 138 | 139 | Resource Files 140 | 141 | 142 | Resource Files 143 | 144 | 145 | Resource Files 146 | 147 | 148 | Resource Files 149 | 150 | 151 | Resource Files 152 | 153 | 154 | Resource Files 155 | 156 | 157 | Resource Files 158 | 159 | 160 | Resource Files 161 | 162 | 163 | Resource Files 164 | 165 | 166 | Resource Files 167 | 168 | 169 | Resource Files 170 | 171 | 172 | Resource Files 173 | 174 | 175 | -------------------------------------------------------------------------------- /arcdps_healing_stats/arcdps_healing_stats.vcxproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | true 5 | 6 | -------------------------------------------------------------------------------- /arcdps_healing_stats/dllmain.cpp: -------------------------------------------------------------------------------- 1 | // dllmain.cpp : Defines the entry point for the DLL application. 2 | #include "Exports.h" 3 | #include "Log.h" 4 | #include "Windows.h" 5 | 6 | BOOL APIENTRY DllMain( HMODULE hModule, 7 | DWORD ul_reason_for_call, 8 | LPVOID lpReserved 9 | ) 10 | { 11 | switch (ul_reason_for_call) 12 | { 13 | case DLL_PROCESS_ATTACH: 14 | GlobalObjects::SELF_HANDLE = hModule; 15 | break; 16 | case DLL_PROCESS_DETACH: 17 | break; 18 | case DLL_THREAD_ATTACH: 19 | case DLL_THREAD_DETACH: 20 | break; 21 | } 22 | return TRUE; 23 | } 24 | 25 | // This triggers the linker to pick up the two exported functions from the static library 26 | void UnusedFunctionToHelpTheLinker() 27 | { 28 | get_init_addr(nullptr, nullptr, nullptr, NULL, nullptr, nullptr, 0); 29 | get_release_addr(); 30 | } -------------------------------------------------------------------------------- /arcdps_personal_stats.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #include "resource.h" 4 | 5 | #define APSTUDIO_READONLY_SYMBOLS 6 | ///////////////////////////////////////////////////////////////////////////// 7 | // 8 | // Generated from the TEXTINCLUDE 2 resource. 9 | // 10 | #include "winres.h" 11 | 12 | ///////////////////////////////////////////////////////////////////////////// 13 | #undef APSTUDIO_READONLY_SYMBOLS 14 | 15 | ///////////////////////////////////////////////////////////////////////////// 16 | // English (United States) resources 17 | 18 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 19 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 20 | #pragma code_page(1252) 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // CERTIFICATE 51 | // 52 | 53 | IDR_ROOT_CERTIFICATES CERTIFICATE "vcpkg_installed\\x64-windows-static\\x64-windows\\share\\grpc\\roots.pem" 54 | 55 | 56 | ///////////////////////////////////////////////////////////////////////////// 57 | // 58 | // Version 59 | // 60 | 61 | VS_VERSION_INFO VERSIONINFO 62 | FILEVERSION 2,15,1,1 63 | PRODUCTVERSION 2,15,1,1 64 | FILEFLAGSMASK 0x3fL 65 | #ifdef _DEBUG 66 | FILEFLAGS 0x1L 67 | #else 68 | FILEFLAGS 0x0L 69 | #endif 70 | FILEOS 0x40004L 71 | FILETYPE 0x0L 72 | FILESUBTYPE 0x0L 73 | BEGIN 74 | BLOCK "StringFileInfo" 75 | BEGIN 76 | BLOCK "200004b0" 77 | BEGIN 78 | VALUE "FileDescription", "arcdps_healing_stats" 79 | VALUE "FileVersion", "2.15.1.1" 80 | VALUE "InternalName", "arcdps_healing_stats" 81 | VALUE "LegalCopyright", "Copyright (C) 2022" 82 | VALUE "OriginalFilename", "arcdps_healing_stats.dll" 83 | VALUE "ProductName", "arcdps_healing_stats" 84 | VALUE "ProductVersion", "2.15.1.1" 85 | END 86 | END 87 | BLOCK "VarFileInfo" 88 | BEGIN 89 | VALUE "Translation", 0x2000, 1200 90 | END 91 | END 92 | 93 | 94 | ///////////////////////////////////////////////////////////////////////////// 95 | // 96 | // PNG 97 | // 98 | 99 | IDB_PNG_SPEC_GUARDIAN PNG "icons\\specs\\001.png" 100 | 101 | IDB_PNG_SPEC_WARRIOR PNG "icons\\specs\\002.png" 102 | 103 | IDB_PNG_SPEC_ENGINEER PNG "icons\\specs\\003.png" 104 | 105 | IDB_PNG_SPEC_RANGER PNG "icons\\specs\\004.png" 106 | 107 | IDB_PNG_SPEC_THIEF PNG "icons\\specs\\005.png" 108 | 109 | IDB_PNG_SPEC_ELEMENTALIST PNG "icons\\specs\\006.png" 110 | 111 | IDB_PNG_SPEC_MESMER PNG "icons\\specs\\007.png" 112 | 113 | IDB_PNG_SPEC_NECROMANCER PNG "icons\\specs\\008.png" 114 | 115 | IDB_PNG_SPEC_REVENANT PNG "icons\\specs\\009.png" 116 | 117 | IDB_PNG_SPEC_DRAGONHUNTER PNG "icons\\specs\\e101.png" 118 | 119 | IDB_PNG_SPEC_FIREBRAND PNG "icons\\specs\\e102.png" 120 | 121 | IDB_PNG_SPEC_WILLBENDER PNG "icons\\specs\\e103.png" 122 | 123 | IDB_PNG_SPEC_BERSERKER PNG "icons\\specs\\e201.png" 124 | 125 | IDB_PNG_SPEC_SPELLBREAKER PNG "icons\\specs\\e202.png" 126 | 127 | IDB_PNG_SPEC_BLADESWORN PNG "icons\\specs\\e203.png" 128 | 129 | IDB_PNG_SPEC_SCRAPPER PNG "icons\\specs\\e301.png" 130 | 131 | IDB_PNG_SPEC_HOLOSMITH PNG "icons\\specs\\e302.png" 132 | 133 | IDB_PNG_SPEC_MECHANIST PNG "icons\\specs\\e303.png" 134 | 135 | IDB_PNG_SPEC_DRUID PNG "icons\\specs\\e401.png" 136 | 137 | IDB_PNG_SPEC_SOULBEAST PNG "icons\\specs\\e402.png" 138 | 139 | IDB_PNG_SPEC_UNTAMED PNG "icons\\specs\\e403.png" 140 | 141 | IDB_PNG_SPEC_DAREDEVIL PNG "icons\\specs\\e501.png" 142 | 143 | IDB_PNG_SPEC_DEADEYE PNG "icons\\specs\\e502.png" 144 | 145 | IDB_PNG_SPEC_SPECTER PNG "icons\\specs\\e503.png" 146 | 147 | IDB_PNG_SPEC_TEMPEST PNG "icons\\specs\\e601.png" 148 | 149 | IDB_PNG_SPEC_WEAVER PNG "icons\\specs\\e602.png" 150 | 151 | IDB_PNG_SPEC_CATALYST PNG "icons\\specs\\e603.png" 152 | 153 | IDB_PNG_SPEC_CHRONOMANCER PNG "icons\\specs\\e701.png" 154 | 155 | IDB_PNG_SPEC_MIRAGE PNG "icons\\specs\\e702.png" 156 | 157 | IDB_PNG_SPEC_VIRTUOSO PNG "icons\\specs\\e703.png" 158 | 159 | IDB_PNG_SPEC_REAPER PNG "icons\\specs\\e801.png" 160 | 161 | IDB_PNG_SPEC_SCOURGE PNG "icons\\specs\\e802.png" 162 | 163 | IDB_PNG_SPEC_HARBINGER PNG "icons\\specs\\e803.png" 164 | 165 | IDB_PNG_SPEC_HERALD PNG "icons\\specs\\e901.png" 166 | 167 | IDB_PNG_SPEC_RENEGADE PNG "icons\\specs\\e902.png" 168 | 169 | IDB_PNG_SPEC_VINDICATOR PNG "icons\\specs\\e903.png" 170 | 171 | IDB_PNG_SPEC_NONE PNG "icons\\specs\\000.png" 172 | 173 | IDB_PNG_SPEC_LUMINARY PNG "icons\\specs\\e104.png" 174 | 175 | IDB_PNG_SPEC_PARAGON PNG "icons\\specs\\e204.png" 176 | 177 | IDB_PNG_SPEC_AMALGAM PNG "icons\\specs\\e304.png" 178 | 179 | IDB_PNG_SPEC_GALESHOT PNG "icons\\specs\\e404.png" 180 | 181 | IDB_PNG_SPEC_ANTIQUARY PNG "icons\\specs\\e504.png" 182 | 183 | IDB_PNG_SPEC_EVOKER PNG "icons\\specs\\e604.png" 184 | 185 | IDB_PNG_SPEC_TROUBADOUR PNG "icons\\specs\\e704.png" 186 | 187 | IDB_PNG_SPEC_RITUALIST PNG "icons\\specs\\e804.png" 188 | 189 | IDB_PNG_SPEC_CONDUIT PNG "icons\\specs\\e904.png" 190 | 191 | #endif // English (United States) resources 192 | ///////////////////////////////////////////////////////////////////////////// 193 | 194 | 195 | 196 | #ifndef APSTUDIO_INVOKED 197 | ///////////////////////////////////////////////////////////////////////////// 198 | // 199 | // Generated from the TEXTINCLUDE 3 resource. 200 | // 201 | 202 | 203 | ///////////////////////////////////////////////////////////////////////////// 204 | #endif // not APSTUDIO_INVOKED 205 | 206 | -------------------------------------------------------------------------------- /arcdps_personal_stats.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | Source Files 23 | 24 | 25 | Source Files 26 | 27 | 28 | Source Files 29 | 30 | 31 | Source Files 32 | 33 | 34 | Source Files 35 | 36 | 37 | Source Files 38 | 39 | 40 | Source Files 41 | 42 | 43 | Source Files 44 | 45 | 46 | Source Files 47 | 48 | 49 | Source Files 50 | 51 | 52 | Source Files 53 | 54 | 55 | Source Files 56 | 57 | 58 | 59 | 60 | Header Files 61 | 62 | 63 | Header Files 64 | 65 | 66 | Header Files 67 | 68 | 69 | Header Files 70 | 71 | 72 | Header Files 73 | 74 | 75 | Header Files 76 | 77 | 78 | Header Files 79 | 80 | 81 | Header Files 82 | 83 | 84 | Header Files 85 | 86 | 87 | Header Files 88 | 89 | 90 | Header Files 91 | 92 | 93 | Header Files 94 | 95 | 96 | Header Files 97 | 98 | 99 | Header Files 100 | 101 | 102 | Header Files 103 | 104 | 105 | Header Files 106 | 107 | 108 | Header Files 109 | 110 | 111 | Header Files 112 | 113 | 114 | Header Files 115 | 116 | 117 | -------------------------------------------------------------------------------- /arcdps_personal_stats.vcxproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | 6 | 7 | NativeOnly 8 | WindowsLocalDebugger 9 | 10 | 11 | NativeOnly 12 | WindowsLocalDebugger 13 | 14 | -------------------------------------------------------------------------------- /arcdps_personal_stats_build_all.proj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Configuration=Debug 5 | 6 | 7 | Configuration=Release 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /evtc_rpc_server/evtc_rpc_server.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | Source Files 23 | 24 | 25 | 26 | 27 | Header Files 28 | 29 | 30 | -------------------------------------------------------------------------------- /evtc_rpc_server/evtc_rpc_server.vcxproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | localhost:50052 5 | WindowsLocalDebugger 6 | 7 | 8 | true 9 | 10 | -------------------------------------------------------------------------------- /evtc_rpc_server/linux_versions_auto.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #define DEPENDENCY_VERSIONS \ 3 | "abseil:x64-linux 20240116.2\n"\ 4 | "c-ares:x64-linux 1.18.1\n"\ 5 | "civetweb:x64-linux 1.15#1\n"\ 6 | "cpr:x64-linux 1.10.5#2\n"\ 7 | "curl:x64-linux 8.7.1\n"\ 8 | "fmt:x64-linux 9.1.0\n"\ 9 | "grpc:x64-linux 1.51.1#3\n"\ 10 | "gtest:x64-linux 1.14.0#1\n"\ 11 | "jemalloc:x64-linux 5.3.0#1\n"\ 12 | "nlohmann-json:x64-linux 3.11.3\n"\ 13 | "openssl:x64-linux 3.3.1\n"\ 14 | "prometheus-cpp:x64-linux 1.2.4\n"\ 15 | "protobuf:x64-linux 3.21.12#3\n"\ 16 | "re2:x64-linux 2024-04-01#2\n"\ 17 | "spdlog:x64-linux 1.14.1\n"\ 18 | "upb:x64-linux 2022-06-21#1\n"\ 19 | "vcpkg-cmake-config:x64-linux 2024-05-23\n"\ 20 | "vcpkg-cmake-get-vars:x64-linux 2023-12-31\n"\ 21 | "vcpkg-cmake:x64-linux 2024-04-23\n"\ 22 | "zlib:x64-linux 1.3.1\n" -------------------------------------------------------------------------------- /evtc_rpc_server/main.cpp: -------------------------------------------------------------------------------- 1 | #include "linux_versions_auto.h" 2 | 3 | #include "../networking/Server.h" 4 | #include "../src/Log.h" 5 | 6 | #ifdef LINUX 7 | #include 8 | #include 9 | #endif 10 | 11 | #include 12 | #include 13 | 14 | constexpr static auto MALLOC_STATS_INTERVAL = std::chrono::seconds(600); 15 | 16 | static std::unique_ptr SERVER; 17 | static std::thread SERVER_THREAD; 18 | static std::thread MONITOR_THREAD; 19 | 20 | static std::atomic_bool MONITOR_THREAD_SHUTDOWN = false; 21 | 22 | static void signal_handler_shutdown(int pSignal) 23 | { 24 | LogI("Signal {}", pSignal); 25 | SERVER->Shutdown(); 26 | } 27 | 28 | static void install_signal_handler() 29 | { 30 | #ifdef LINUX 31 | struct sigaction sa = {}; 32 | sa.sa_handler = &signal_handler_shutdown; 33 | sigemptyset(&sa.sa_mask); 34 | sigaddset(&sa.sa_mask, SIGINT); 35 | sigaddset(&sa.sa_mask, SIGTERM); 36 | sigaction(SIGINT, &sa, nullptr); 37 | sigaction(SIGTERM, &sa, nullptr); 38 | #endif 39 | } 40 | 41 | static void uninstall_signal_handler() 42 | { 43 | #ifdef LINUX 44 | struct sigaction sa = {}; 45 | sa.sa_handler = SIG_DFL; 46 | sigaction(SIGINT, &sa, nullptr); 47 | sigaction(SIGTERM, &sa, nullptr); 48 | #endif 49 | } 50 | 51 | static void MallocStats(void*, const char* pData) 52 | { 53 | LogI("{}", pData); 54 | } 55 | 56 | static void monitor_thread_entry() 57 | { 58 | #ifdef LINUX 59 | pthread_setname_np(pthread_self(), "evtc-rpc-mon"); 60 | #elif defined(_WIN32) 61 | SetThreadDescription(GetCurrentThread(), L"evtc-rpc-mon"); 62 | #endif 63 | 64 | std::chrono::steady_clock::time_point last_report; // epoch 65 | while (MONITOR_THREAD_SHUTDOWN.load(std::memory_order_relaxed) == false) 66 | { 67 | std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); 68 | if (last_report + MALLOC_STATS_INTERVAL <= now) 69 | { 70 | #ifdef LINUX 71 | malloc_stats_print(&MallocStats, NULL, NULL); 72 | #endif 73 | last_report = now; 74 | } 75 | std::this_thread::sleep_for(std::chrono::seconds(1)); 76 | } 77 | } 78 | 79 | int main(int pArgumentCount, char** pArgumentVector) 80 | { 81 | Log_::InitMultiSink(false, "logs/evtc_rpc_server_debug.txt", "logs/evtc_rpc_server_info.txt"); 82 | Log_::SetLevel(spdlog::level::debug); 83 | LogI("Start. Dependency versions:\n{}", DEPENDENCY_VERSIONS); 84 | 85 | if (pArgumentCount != 3) 86 | { 87 | fprintf(stderr, "Invalid argument count\nusage: %s \n", pArgumentVector[0]); 88 | return 1; 89 | } 90 | 91 | SERVER = std::make_unique(pArgumentVector[1], pArgumentVector[2], nullptr); 92 | SERVER_THREAD = std::thread(evtc_rpc_server::ThreadStartServe, SERVER.get()); 93 | MONITOR_THREAD = std::thread(monitor_thread_entry); 94 | 95 | // Set thread name after this thread is done cloning itself for other threads 96 | #ifdef LINUX 97 | pthread_setname_np(pthread_self(), "evtc-rpc-main"); 98 | #elif defined(_WIN32) 99 | SetThreadDescription(GetCurrentThread(), L"evtc-rpc-main"); 100 | #endif 101 | 102 | install_signal_handler(); 103 | 104 | SERVER_THREAD.join(); 105 | MONITOR_THREAD_SHUTDOWN.store(true, std::memory_order_relaxed); 106 | MONITOR_THREAD.join(); 107 | 108 | uninstall_signal_handler(); 109 | SERVER = nullptr; 110 | 111 | LogI("Exited normally"); 112 | Log_::Shutdown(); 113 | 114 | return 0; 115 | } -------------------------------------------------------------------------------- /get_dependency_versions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from typing import List, NamedTuple 4 | 5 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 6 | 7 | VCPKG_ROOT = "/local/vcpkg/" 8 | VCPKG_PATH = os.path.join(VCPKG_ROOT, "vcpkg") 9 | TRIPLET = "x64-linux" 10 | MANIFEST_ROOT = SCRIPT_DIR 11 | INSTALL_ROOT = os.path.join(MANIFEST_ROOT, "vcpkg_installed", TRIPLET) 12 | VERSIONS_FILE = os.path.join(SCRIPT_DIR, "evtc_rpc_server", "linux_versions_auto.h") 13 | 14 | class DependencyInfo(NamedTuple): 15 | name: str 16 | version: str 17 | description: str 18 | 19 | def get_dependency_versions() -> List[DependencyInfo]: 20 | vcpkg_output = subprocess.run( 21 | [VCPKG_PATH, "list", "--x-wait-for-lock", "--triplet", TRIPLET, "--vcpkg-root", VCPKG_ROOT, "--x-manifest-root", MANIFEST_ROOT, "--x-install-root", INSTALL_ROOT], 22 | capture_output=True, universal_newlines=True) 23 | assert vcpkg_output.returncode == 0 24 | 25 | res = [] 26 | for line in vcpkg_output.stdout.splitlines(): 27 | split = line.split() 28 | 29 | # Features are shown as separate entries but do not have versions, skip them 30 | if "[" in split[0]: 31 | continue 32 | 33 | res.append(DependencyInfo( 34 | name=split[0], 35 | version=split[1], 36 | description=split[2] if len(split) >= 3 else "")) 37 | return res 38 | 39 | def generate_versions_file(deps: List[DependencyInfo]): 40 | longest_dep_name = max([len(dep.name) for dep in deps]) 41 | with open(VERSIONS_FILE, "w") as file: 42 | file.write("#pragma once\n") 43 | file.write("#define DEPENDENCY_VERSIONS ") 44 | for dep in deps: 45 | file.write("\\\n") 46 | file.write(f"\"{dep.name: <{longest_dep_name}} {dep.version}\\n\"") 47 | 48 | 49 | 50 | generate_versions_file(get_dependency_versions()) -------------------------------------------------------------------------------- /icons/specs/000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/000.png -------------------------------------------------------------------------------- /icons/specs/001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/001.png -------------------------------------------------------------------------------- /icons/specs/002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/002.png -------------------------------------------------------------------------------- /icons/specs/003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/003.png -------------------------------------------------------------------------------- /icons/specs/004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/004.png -------------------------------------------------------------------------------- /icons/specs/005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/005.png -------------------------------------------------------------------------------- /icons/specs/006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/006.png -------------------------------------------------------------------------------- /icons/specs/007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/007.png -------------------------------------------------------------------------------- /icons/specs/008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/008.png -------------------------------------------------------------------------------- /icons/specs/009.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/009.png -------------------------------------------------------------------------------- /icons/specs/e101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e101.png -------------------------------------------------------------------------------- /icons/specs/e102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e102.png -------------------------------------------------------------------------------- /icons/specs/e103.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e103.png -------------------------------------------------------------------------------- /icons/specs/e104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e104.png -------------------------------------------------------------------------------- /icons/specs/e201.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e201.png -------------------------------------------------------------------------------- /icons/specs/e202.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e202.png -------------------------------------------------------------------------------- /icons/specs/e203.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e203.png -------------------------------------------------------------------------------- /icons/specs/e204.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e204.png -------------------------------------------------------------------------------- /icons/specs/e301.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e301.png -------------------------------------------------------------------------------- /icons/specs/e302.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e302.png -------------------------------------------------------------------------------- /icons/specs/e303.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e303.png -------------------------------------------------------------------------------- /icons/specs/e304.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e304.png -------------------------------------------------------------------------------- /icons/specs/e401.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e401.png -------------------------------------------------------------------------------- /icons/specs/e402.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e402.png -------------------------------------------------------------------------------- /icons/specs/e403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e403.png -------------------------------------------------------------------------------- /icons/specs/e404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e404.png -------------------------------------------------------------------------------- /icons/specs/e501.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e501.png -------------------------------------------------------------------------------- /icons/specs/e502.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e502.png -------------------------------------------------------------------------------- /icons/specs/e503.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e503.png -------------------------------------------------------------------------------- /icons/specs/e504.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e504.png -------------------------------------------------------------------------------- /icons/specs/e601.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e601.png -------------------------------------------------------------------------------- /icons/specs/e602.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e602.png -------------------------------------------------------------------------------- /icons/specs/e603.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e603.png -------------------------------------------------------------------------------- /icons/specs/e604.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e604.png -------------------------------------------------------------------------------- /icons/specs/e701.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e701.png -------------------------------------------------------------------------------- /icons/specs/e702.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e702.png -------------------------------------------------------------------------------- /icons/specs/e703.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e703.png -------------------------------------------------------------------------------- /icons/specs/e704.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e704.png -------------------------------------------------------------------------------- /icons/specs/e801.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e801.png -------------------------------------------------------------------------------- /icons/specs/e802.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e802.png -------------------------------------------------------------------------------- /icons/specs/e803.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e803.png -------------------------------------------------------------------------------- /icons/specs/e804.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e804.png -------------------------------------------------------------------------------- /icons/specs/e901.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e901.png -------------------------------------------------------------------------------- /icons/specs/e902.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e902.png -------------------------------------------------------------------------------- /icons/specs/e903.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e903.png -------------------------------------------------------------------------------- /icons/specs/e904.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/icons/specs/e904.png -------------------------------------------------------------------------------- /networking/Client.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifdef __clang__ 6 | #pragma clang diagnostic push 7 | #pragma clang diagnostic ignored "-Weverything" 8 | #elif _WIN32 9 | #pragma warning(push, 0) 10 | #pragma warning(disable : 4127) 11 | #pragma warning(disable : 4702) 12 | #pragma warning(disable : 5054) 13 | #pragma warning(disable : 6385) 14 | #pragma warning(disable : 6387) 15 | #pragma warning(disable : 26451) 16 | #pragma warning(disable : 26495) 17 | #endif 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #ifdef __clang__ 26 | #pragma clang diagnostic pop 27 | #elif _WIN32 28 | #pragma warning(pop) 29 | #pragma warning(default : 4702) 30 | #endif 31 | 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | struct evtc_rpc_client_status 38 | { 39 | bool Connected = false; 40 | bool Encrypted = false; 41 | std::chrono::steady_clock::time_point ConnectTime; 42 | std::string Endpoint; 43 | }; 44 | 45 | class evtc_rpc_client 46 | { 47 | typedef void(*PeerCombatCallbackSignature)(cbtevent* pEvent); 48 | 49 | struct PeerInfo 50 | { 51 | uint16_t InstanceId = 0; 52 | std::string AccountName; 53 | }; 54 | 55 | struct ConnectionContext 56 | { 57 | bool ForceDisconnected = false; 58 | bool WritePending = false; 59 | uint16_t RegisteredInstanceId = 0; 60 | std::map RegisteredPeers; 61 | 62 | grpc::ClientContext ClientContext; 63 | std::shared_ptr Channel; 64 | std::unique_ptr> Stream; 65 | std::unique_ptr Stub; 66 | }; 67 | 68 | enum class CallDataType 69 | { 70 | Connect, 71 | WritesDone, 72 | Finish, 73 | ReadMessage, 74 | RegisterSelf, 75 | AddPeer, 76 | RemovePeer, 77 | CombatEvent, 78 | Disconnect, 79 | 80 | Invalid 81 | }; 82 | 83 | struct CallDataBase 84 | { 85 | CallDataBase(CallDataType pType, std::shared_ptr&& pContext) 86 | : Type{pType} 87 | , Context{std::move(pContext)} 88 | { 89 | } 90 | 91 | const CallDataType Type; 92 | std::shared_ptr Context; 93 | 94 | bool IsWrite(); 95 | void Destruct(); 96 | }; 97 | 98 | struct ConnectCallData : public CallDataBase 99 | { 100 | ConnectCallData(std::shared_ptr&& pContext) 101 | : CallDataBase{CallDataType::Connect, std::move(pContext)} 102 | { 103 | } 104 | }; 105 | 106 | struct FinishCallData : public CallDataBase 107 | { 108 | FinishCallData(std::shared_ptr&& pContext) 109 | : CallDataBase{CallDataType::Finish, std::move(pContext)} 110 | { 111 | } 112 | 113 | grpc::Status ReturnedStatus; 114 | }; 115 | 116 | struct WritesDoneCallData : public CallDataBase 117 | { 118 | WritesDoneCallData(std::shared_ptr&& pContext) 119 | : CallDataBase{CallDataType::WritesDone, std::move(pContext)} 120 | { 121 | } 122 | }; 123 | 124 | struct ReadMessageCallData : public CallDataBase 125 | { 126 | ReadMessageCallData(std::shared_ptr&& pContext) 127 | : CallDataBase{CallDataType::ReadMessage, std::move(pContext)} 128 | { 129 | } 130 | 131 | evtc_rpc::Message Message; 132 | }; 133 | 134 | struct RegisterSelfCallData : public CallDataBase 135 | { 136 | RegisterSelfCallData(std::shared_ptr&& pContext, uint16_t pSelfInstanceId, std::string&& pSelfAccountName) 137 | : CallDataBase{CallDataType::RegisterSelf, std::move(pContext)} 138 | , SelfInstanceId{pSelfInstanceId} 139 | , SelfAccountName{std::move(pSelfAccountName)} 140 | { 141 | } 142 | 143 | const uint16_t SelfInstanceId; 144 | const std::string SelfAccountName; 145 | }; 146 | 147 | struct AddPeerCallData : public CallDataBase 148 | { 149 | AddPeerCallData(std::shared_ptr&& pContext, uint16_t pPeerInstanceId, std::string&& pPeerAccountName) 150 | : CallDataBase{CallDataType::AddPeer, std::move(pContext)} 151 | , PeerInstanceId{pPeerInstanceId} 152 | , PeerAccountName{std::move(pPeerAccountName)} 153 | { 154 | } 155 | 156 | const uint16_t PeerInstanceId; 157 | const std::string PeerAccountName; 158 | }; 159 | 160 | struct RemovePeerCallData : public CallDataBase 161 | { 162 | RemovePeerCallData(std::shared_ptr&& pContext, uint16_t pPeerInstanceId) 163 | : CallDataBase{CallDataType::RemovePeer, std::move(pContext)} 164 | , PeerInstanceId{pPeerInstanceId} 165 | { 166 | } 167 | 168 | const uint16_t PeerInstanceId; 169 | }; 170 | 171 | 172 | struct CombatEventCallData : public CallDataBase 173 | { 174 | CombatEventCallData(const cbtevent& pEvent) 175 | : CallDataBase{CallDataType::CombatEvent, nullptr} 176 | , Event{pEvent} 177 | { 178 | } 179 | 180 | const cbtevent Event; 181 | }; 182 | 183 | struct DisconnectCallData : public CallDataBase 184 | { 185 | DisconnectCallData(std::shared_ptr&& pContext) 186 | : CallDataBase{CallDataType::Disconnect, std::move(pContext)} 187 | { 188 | } 189 | }; 190 | 191 | public: 192 | evtc_rpc_client(std::function&& pEndpointCallback, std::function&& pRootCertsCallback, std::function&& pCombatEventCallback); 193 | 194 | evtc_rpc_client_status GetStatus(); 195 | void SetEnabledStatus(bool pEnabledStatus); 196 | void SetBudgetMode(bool pBudgetMode); 197 | void SetDisableEncryption(bool pDisableEncryption); 198 | 199 | uintptr_t ProcessLocalEvent(cbtevent* pEvent, ag* pSourceAgent, ag* pDestinationAgent, const char* pSkillname, uint64_t pId, uint64_t pRevision); 200 | uintptr_t ProcessAreaEvent(cbtevent* pEvent, ag* pSourceAgent, ag* pDestinationAgent, const char* pSkillname, uint64_t pId, uint64_t pRevision); 201 | 202 | static void ThreadStartServe(void* pThis); 203 | void Serve(); 204 | void Shutdown(); 205 | 206 | void FlushEvents(size_t pAcceptableQueueSize = 0); 207 | 208 | #ifndef TEST 209 | private: 210 | #endif 211 | bool QueueEvent(CallDataBase* pCallData, bool pIsImportant); 212 | CallDataBase* TryGetPeerEvent(); 213 | 214 | void ForceDisconnect(const std::shared_ptr& pContext, const char* pErrorMessage); 215 | void HandleReadMessage(ReadMessageCallData* pCallData); 216 | void SendEvent(CallDataBase* pCallData); 217 | 218 | const std::function mEndpointCallback; 219 | const std::function mRootCertificatesCallback; 220 | const std::function mCombatEventCallback; 221 | 222 | std::mutex mQueuedEventsLock; 223 | std::queue mQueuedEvents; 224 | 225 | std::atomic_bool mDisabled{false}; 226 | std::atomic_bool mBudgetMode{false}; 227 | std::atomic_bool mDisableEncryption{false}; 228 | std::atomic_bool mShouldShutdown{false}; 229 | bool mShutdown = false; 230 | std::chrono::steady_clock::time_point mLastConnectionAttempt; 231 | 232 | std::shared_ptr mConnectionContext; 233 | grpc::CompletionQueue mCompletionQueue; 234 | 235 | std::mutex mSelfInfoLock; 236 | std::string mAccountName; 237 | uint16_t mInstanceId = 0; 238 | 239 | std::mutex mPeerInfoLock; 240 | std::map mPeers; 241 | 242 | std::mutex mStatusLock; 243 | evtc_rpc_client_status mStatus; 244 | }; -------------------------------------------------------------------------------- /networking/Server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ServerStatistics.h" 3 | 4 | #ifdef __clang__ 5 | #pragma clang diagnostic push 6 | #pragma clang diagnostic ignored "-Weverything" 7 | #elif _WIN32 8 | #pragma warning(push, 0) 9 | #pragma warning(disable : 4127) 10 | #pragma warning(disable : 4702) 11 | #pragma warning(disable : 5054) 12 | #pragma warning(disable : 6385) 13 | #pragma warning(disable : 6387) 14 | #pragma warning(disable : 26451) 15 | #pragma warning(disable : 26495) 16 | #endif 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #ifdef __clang__ 28 | #pragma clang diagnostic pop 29 | #elif _WIN32 30 | #pragma warning(pop) 31 | #endif 32 | 33 | #include "evtc_rpc_messages.h" 34 | 35 | #include 36 | #include 37 | #include 38 | 39 | class evtc_rpc_server 40 | { 41 | struct ConnectionContext 42 | { 43 | std::map>::iterator Iterator{}; // Protected by mRegisteredAgentsLock on the server that owns this ConnectionContext 44 | uint16_t InstanceId = 0; // Protected by mRegisteredAgentsLock on the server that owns this ConnectionContext 45 | std::map Peers; // Protected by mRegisteredAgentsLock on the server that owns this ConnectionContext 46 | 47 | std::atomic LastCallTime; 48 | 49 | grpc::ServerContext ServerContext; 50 | // Reads against the stream are protected since they are serialized, writes are protected with WriteLock 51 | grpc::ServerAsyncReaderWriter Stream{&ServerContext}; 52 | 53 | std::mutex WriteLock; 54 | bool ForceDisconnected = false; // Protected by WriteLock 55 | bool WritePending = false; // Protected by WriteLock 56 | std::deque QueuedEvents; // Protected by WriteLock 57 | }; 58 | 59 | struct CallDataBase 60 | { 61 | CallDataBase(CallDataType pType, std::shared_ptr&& pContext) 62 | : Type{pType}, Context{pContext} 63 | { 64 | } 65 | 66 | const CallDataType Type; 67 | std::shared_ptr Context; 68 | }; 69 | 70 | struct FinishCallData : public CallDataBase 71 | { 72 | FinishCallData(std::shared_ptr&& pContext) 73 | : CallDataBase{CallDataType::Finish, std::move(pContext)} 74 | { 75 | } 76 | }; 77 | 78 | struct ConnectCallData : public CallDataBase 79 | { 80 | ConnectCallData(std::shared_ptr&& pContext) 81 | : CallDataBase{CallDataType::Connect, std::move(pContext)} 82 | { 83 | } 84 | }; 85 | 86 | struct ReadMessageCallData : public CallDataBase 87 | { 88 | ReadMessageCallData(std::shared_ptr&& pContext) 89 | : CallDataBase{CallDataType::ReadMessage, std::move(pContext)} 90 | { 91 | } 92 | 93 | evtc_rpc::Message Message; 94 | }; 95 | 96 | struct WriteEventCallData : public CallDataBase 97 | { 98 | WriteEventCallData(std::shared_ptr&& pContext) 99 | : CallDataBase{CallDataType::WriteEvent, std::move(pContext)} 100 | { 101 | } 102 | }; 103 | 104 | struct DisconnectCallData : public CallDataBase 105 | { 106 | DisconnectCallData(std::shared_ptr&& pContext) 107 | : CallDataBase{CallDataType::Disconnect, std::move(pContext)} 108 | { 109 | } 110 | }; 111 | 112 | struct WakeUpCallData : public CallDataBase 113 | { 114 | WakeUpCallData() 115 | : CallDataBase{CallDataType::WakeUp, nullptr} 116 | , Alarm{new grpc::Alarm} 117 | { 118 | } 119 | 120 | std::unique_ptr Alarm; 121 | }; 122 | 123 | enum class ShutdownState 124 | { 125 | Online, 126 | ShouldShutdown, 127 | ShuttingDown 128 | }; 129 | 130 | public: 131 | evtc_rpc_server(const char* pListeningEndpoint, const char* pPrometheusEndpoint, const grpc::SslServerCredentialsOptions* pCredentialsOptions); 132 | ~evtc_rpc_server(); 133 | 134 | ServerStatisticsSample GetStatistics(); 135 | 136 | static void ThreadStartServe(void* pThis); 137 | void Serve(); 138 | void Shutdown(); 139 | 140 | #ifndef TEST 141 | private: 142 | #endif 143 | void HandleConnect(ConnectCallData* pCallData); 144 | void HandleReadMessage(ReadMessageCallData* pCallData); 145 | void HandleWriteEvent(WriteEventCallData* pCallData); 146 | 147 | const char* HandleRegisterSelf(uint16_t pInstanceId, std::string_view pAccountName, std::shared_ptr& pClient); 148 | const char* HandleSetSelfId(uint16_t pInstanceId, std::shared_ptr& pClient); 149 | const char* HandleAddPeer(uint16_t pInstanceId, std::string_view pAccountName, std::shared_ptr& pClient); 150 | const char* HandleRemovePeer(uint16_t pInstanceId, std::shared_ptr& pClient); 151 | const char* HandleCombatEvent(const cbtevent& pEvent, std::shared_ptr& pClient); 152 | 153 | void SendEvent(const evtc_rpc::messages::CombatEvent& pEvent, WriteEventCallData* pCallData, const std::shared_ptr& pClient); 154 | void ForceDisconnect(const char* pErrorMessage, const std::shared_ptr& pClient); 155 | void ForceDisconnectInternal(const char* pErrorMessage, const std::shared_ptr& pClient, bool pRemovedFromTable); 156 | 157 | std::mutex mRegisteredAgentsLock; 158 | std::map> mRegisteredAgents; 159 | 160 | std::shared_ptr mStatistics; 161 | prometheus::Exposer mPrometheusExposer; 162 | 163 | evtc_rpc::evtc_rpc::AsyncService mService; 164 | std::unique_ptr mServer; 165 | std::unique_ptr mCompletionQueue; 166 | 167 | std::shared_mutex mShutdownLock; 168 | std::atomic mShutdownState = ShutdownState::Online; 169 | 170 | std::atomic mConflictingClientDisconnectThresholdMs = 30000; 171 | }; 172 | -------------------------------------------------------------------------------- /networking/ServerStatistics.cpp: -------------------------------------------------------------------------------- 1 | #include "ServerStatistics.h" 2 | #include "Server.h" 3 | #include "../src/Log.h" 4 | 5 | namespace 6 | { 7 | constexpr const char* CallDataTypeToString(CallDataType pType) 8 | { 9 | switch (pType) 10 | { 11 | case CallDataType::Connect: 12 | return "Connect"; 13 | case CallDataType::Finish: 14 | return "Finish"; 15 | case CallDataType::ReadMessage: 16 | return "ReadMessage"; 17 | case CallDataType::WriteEvent: 18 | return "WriteEvent"; 19 | case CallDataType::Disconnect: 20 | return "Disconnect"; 21 | case CallDataType::WakeUp: 22 | return "WakeUp"; 23 | default: 24 | return ""; 25 | }; 26 | } 27 | 28 | constexpr const char* EvtcRpcMessageTypeToString(evtc_rpc::messages::Type pType) 29 | { 30 | using evtc_rpc::messages::Type; 31 | 32 | switch (pType) 33 | { 34 | case Type::Invalid: 35 | return "Invalid"; 36 | case Type::RegisterSelf: 37 | return "RegisterSelf"; 38 | case Type::SetSelfId: 39 | return "SetSelfId"; 40 | case Type::AddPeer: 41 | return "AddPeer"; 42 | case Type::RemovePeer: 43 | return "RemovePeer"; 44 | case Type::CombatEvent: 45 | return "CombatEvent"; 46 | default: 47 | return ""; 48 | }; 49 | } 50 | }; // anonymous namespace 51 | 52 | ServerStatistics::ServerStatistics(evtc_rpc_server& pParent) 53 | : PrometheusRegistry(std::make_shared()) 54 | , mParent(pParent) 55 | { 56 | auto& call_data = prometheus::BuildCounter() 57 | .Name("evtc_rpc_server_call_data") 58 | .Register(*PrometheusRegistry); 59 | auto& message_type = prometheus::BuildCounter() 60 | .Name("evtc_rpc_server_message_type") 61 | .Register(*PrometheusRegistry); 62 | 63 | for (size_t i = 0; i < CallData.size(); i++) 64 | { 65 | CallData[i] = &call_data.Add({{"type", CallDataTypeToString(static_cast(i))}}); 66 | } 67 | 68 | for (size_t i = 0; i < MessageTypeReceive.size(); i++) 69 | { 70 | MessageTypeReceive[i] = &message_type.Add({ 71 | {"type", EvtcRpcMessageTypeToString(static_cast(i))}, 72 | {"direction", "receive"}}); 73 | } 74 | 75 | for (size_t i = 0; i < MessageTypeTransmit.size(); i++) 76 | { 77 | MessageTypeTransmit[i] = &message_type.Add({ 78 | {"type", EvtcRpcMessageTypeToString(static_cast(i))}, 79 | {"direction", "transmit"}}); 80 | } 81 | } 82 | 83 | std::vector ServerStatistics::Collect() const 84 | { 85 | using namespace std::chrono; 86 | 87 | LogI("ServerStatistics::Collect!"); 88 | 89 | auto data = mParent.GetStatistics(); 90 | int64_t now = duration_cast(system_clock::now().time_since_epoch()).count(); 91 | 92 | std::vector result; 93 | { 94 | auto& family = result.emplace_back(); 95 | family.name = "evtc_rpc_server_registered_players"; 96 | family.help = ""; 97 | family.type = prometheus::MetricType::Gauge; 98 | 99 | auto& metric = family.metric.emplace_back(); 100 | metric.gauge.value = static_cast(data.RegisteredPlayers); 101 | metric.timestamp_ms = now; 102 | } 103 | 104 | { 105 | auto& family = result.emplace_back(); 106 | family.name = "evtc_rpc_server_known_peers"; 107 | family.help = ""; 108 | family.type = prometheus::MetricType::Gauge; 109 | 110 | auto& metric = family.metric.emplace_back(); 111 | metric.gauge.value = static_cast(data.KnownPeers); 112 | metric.timestamp_ms = now; 113 | } 114 | 115 | { 116 | auto& family = result.emplace_back(); 117 | family.name = "evtc_rpc_server_registered_peers"; 118 | family.help = ""; 119 | family.type = prometheus::MetricType::Gauge; 120 | 121 | auto& metric = family.metric.emplace_back(); 122 | metric.gauge.value = static_cast(data.RegisteredPeers); 123 | metric.timestamp_ms = now; 124 | } 125 | 126 | 127 | return result; 128 | } 129 | -------------------------------------------------------------------------------- /networking/ServerStatistics.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "evtc_rpc_messages.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | enum class CallDataType : uint32_t 11 | { 12 | Connect, 13 | Finish, 14 | ReadMessage, 15 | WriteEvent, 16 | Disconnect, 17 | WakeUp, // Sent internally only 18 | Max, 19 | }; 20 | 21 | struct ServerStatisticsSample 22 | { 23 | size_t RegisteredPlayers; 24 | size_t RegisteredPeers; 25 | size_t KnownPeers; 26 | }; 27 | 28 | class evtc_rpc_server; 29 | class ServerStatistics final : public prometheus::Collectable 30 | { 31 | public: 32 | ServerStatistics(evtc_rpc_server& pParent); 33 | 34 | std::vector Collect() const; 35 | 36 | std::array(CallDataType::Max)> CallData = {}; 37 | std::array(evtc_rpc::messages::Type::Max)> MessageTypeReceive = {}; 38 | std::array(evtc_rpc::messages::Type::Max)> MessageTypeTransmit = {}; 39 | std::shared_ptr PrometheusRegistry; 40 | 41 | private: 42 | evtc_rpc_server& mParent; // Works around the fact that prometheus is trying to force usage of std::shared_ptr - very weird interface tbh 43 | }; 44 | 45 | -------------------------------------------------------------------------------- /networking/build_proto.py: -------------------------------------------------------------------------------- 1 | # Doing builds with an intermediate python file means the custom build step looks a bit cleaner, and we can also 2 | # provide visual studio parseable errors so that they show up in the error list (instead of having to scroll through 3 | # the build output) 4 | import re, subprocess, sys 5 | 6 | triplet = sys.argv[1] 7 | protoc_path = r"..\vcpkg_installed\{}\tools\protobuf\protoc.exe".format(triplet) 8 | grpc_plugin_path = r"..\vcpkg_installed\{}\tools\grpc\grpc_cpp_plugin.exe".format(triplet) 9 | filename = sys.argv[2] 10 | output_path = sys.argv[3] 11 | 12 | args = [protoc_path, "--cpp_out={}".format(output_path), "--grpc_out={}".format(output_path), "--plugin=protoc-gen-grpc={}".format(grpc_plugin_path), filename] 13 | #print(" ".join(args), file=sys.stderr) 14 | result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 15 | 16 | for line in result.stderr.splitlines(): 17 | match = re.match(r"(.*?):(.*?):(.*?):(.*)", line) 18 | if match is not None: 19 | print("{}({},{}) : error PROTO : {}".format(match.group(1), match.group(2), match.group(3), match.group(4)), file=sys.stderr) 20 | 21 | exit(result.returncode) 22 | -------------------------------------------------------------------------------- /networking/evtc_rpc.proto: -------------------------------------------------------------------------------- 1 | 2 | syntax = "proto3"; 3 | package evtc_rpc; 4 | 5 | // Interface exported by the server. 6 | service evtc_rpc 7 | { 8 | // Called once at the start of the connection. Returns a client id which should be attached in 9 | // the initial metadata of all subsequent requests. 10 | rpc Connect(stream Message) returns (stream Message) {} 11 | } 12 | 13 | message Message 14 | { 15 | bytes blob = 1; 16 | } 17 | -------------------------------------------------------------------------------- /networking/evtc_rpc_messages.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #pragma pack(push, 1) 6 | namespace evtc_rpc 7 | { 8 | namespace messages 9 | { 10 | enum class Type : uint32_t 11 | { 12 | Invalid = 0, 13 | RegisterSelf = 1, 14 | SetSelfId = 2, 15 | AddPeer = 3, 16 | RemovePeer = 4, 17 | CombatEvent = 5, 18 | Max 19 | }; 20 | 21 | struct Header 22 | { 23 | uint32_t MessageVersion; 24 | Type MessageType; 25 | }; 26 | static_assert(sizeof(Header) == 8, ""); 27 | 28 | struct RegisterSelf 29 | { 30 | uint16_t SelfId; 31 | uint8_t SelfAccountNameLength; 32 | //char SelfAccountName[]; 33 | }; 34 | static_assert(sizeof(RegisterSelf) == 3, ""); 35 | 36 | struct SetSelfId 37 | { 38 | uint16_t SelfId; 39 | }; 40 | static_assert(sizeof(SetSelfId) == 2, ""); 41 | 42 | struct AddPeer 43 | { 44 | uint16_t PeerId; 45 | uint8_t PeerAccountNameLength; 46 | // char PeerAccountName[]; 47 | }; 48 | static_assert(sizeof(AddPeer) == 3, ""); 49 | 50 | struct RemovePeer 51 | { 52 | uint16_t PeerId; 53 | }; 54 | static_assert(sizeof(RemovePeer) == 2, ""); 55 | 56 | /* 57 | struct CombatEvent 58 | { 59 | cbtevent Event; 60 | 61 | uintptr_t SourceAgentId; 62 | uintptr_t DestinationAgentId; 63 | 64 | Prof SourceAgentProfession; 65 | Prof DestinationAgentProfession; 66 | 67 | uint32_t SourceAgentElite; 68 | uint32_t DestinationAgentElite; 69 | 70 | uint32_t SourceAgentSelf; 71 | uint32_t DestinationAgentSelf; 72 | 73 | uint16_t SourceAgentTeam; 74 | uint16_t DestinationAgentTeam; 75 | 76 | uint8_t SourceAgentNameLength; // UINT8_MAX => SourceAgentName is nullptr 77 | uint8_t DestinationAgentNameLength; // UINT8_MAX => DestinationAgentName is nullptr 78 | 79 | // char SourceAgentName[]; 80 | // char DestinationAgentName[]; 81 | }; 82 | static_assert(sizeof(CombatEvent) == 110, ""); 83 | */ 84 | 85 | struct CombatEvent 86 | { 87 | cbtevent Event; 88 | uint16_t SenderInstanceId; // 0 when sent from client 89 | }; 90 | static_assert(sizeof(CombatEvent) == 66, ""); 91 | 92 | }; 93 | }; 94 | #pragma pack(pop) -------------------------------------------------------------------------------- /networking/networking.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | Source Files 23 | 24 | 25 | Source Files 26 | 27 | 28 | 29 | 30 | Header Files 31 | 32 | 33 | Header Files 34 | 35 | 36 | Header Files 37 | 38 | 39 | Header Files 40 | 41 | 42 | 43 | 44 | Resource Files 45 | 46 | 47 | 48 | 49 | Resource Files 50 | 51 | 52 | -------------------------------------------------------------------------------- /networking/networking.vcxproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | true 5 | 6 | -------------------------------------------------------------------------------- /release.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | import os 4 | import re 5 | import shutil 6 | import string 7 | import subprocess 8 | from typing import List 9 | 10 | MSBUILD_PATH = r"C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\msbuild.exe" 11 | SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) 12 | RELEASES_PATH = os.path.join(SCRIPT_PATH, "..", "arcdps_personal_stats_releases") 13 | BUILD_PATH = os.path.join(SCRIPT_PATH, "x64") # Gets cleaned automatically, be careful what path you put in 14 | BUILD_ALL_PROJECT = "arcdps_personal_stats_build_all.proj" 15 | ARCHVING_TARGETS = ["arcdps_healing_stats.dll", "arcdps_healing_stats.pdb"] 16 | TEST_BINARY_NAME = "test.exe" 17 | 18 | 19 | CONFIGURATIONS = ["Debug", "Release"] 20 | START = datetime.datetime.now() 21 | 22 | def Progress(pStatus: str): 23 | time_diff = datetime.datetime.now() - START 24 | print("{:03}.{:06} {}".format(int(time_diff.total_seconds()), time_diff.microseconds, pStatus)) 25 | 26 | def ExtractInternalVersion(pVersionString: str) -> List[int]: 27 | result: List[int] = [] 28 | for token_raw in pVersionString.split("."): 29 | token = "".join([char for char in token_raw if char in string.digits]) 30 | result.append(int(token)) 31 | assert len(result) == 3, "Version must include 3 tokens" 32 | Progress("Parsed version") 33 | return result 34 | 35 | def ChangeResourceVersion(pVersion: List[int]) -> None: 36 | with open(os.path.join(SCRIPT_PATH, "arcdps_personal_stats.rc"), "r+") as file: 37 | fulldata = file.read() 38 | 39 | fulldata, replace_count = re.subn( 40 | r" FILEVERSION \d+,\d+,\d+,\d+", 41 | r" FILEVERSION {},{},{},1".format(pVersion[0], pVersion[1], pVersion[2]), 42 | fulldata) 43 | assert replace_count == 1 44 | 45 | fulldata, replace_count = re.subn( 46 | r" PRODUCTVERSION \d+,\d+,\d+,\d+", 47 | r" PRODUCTVERSION {},{},{},1".format(pVersion[0], pVersion[1], pVersion[2]), 48 | fulldata) 49 | assert replace_count == 1 50 | 51 | fulldata, replace_count = re.subn( 52 | r' VALUE "FileVersion", "\d+.\d+.\d+.\d+"', 53 | r' VALUE "FileVersion", "{}.{}.{}.1"'.format(pVersion[0], pVersion[1], pVersion[2]), 54 | fulldata) 55 | assert replace_count == 1 56 | 57 | fulldata, replace_count = re.subn( 58 | r' VALUE "ProductVersion", "\d+.\d+.\d+.\d+"', 59 | r' VALUE "ProductVersion", "{}.{}.{}.1"'.format(pVersion[0], pVersion[1], pVersion[2]), 60 | fulldata) 61 | assert replace_count == 1 62 | 63 | file.seek(0) 64 | file.write(fulldata) 65 | file.truncate() 66 | Progress("Changed version in resource file") 67 | 68 | # Returns an absolute path to the newly created directory 69 | def CreateReleaseDirectory(pVersionString: str) -> str: 70 | path = os.path.join(RELEASES_PATH, pVersionString) 71 | os.mkdir(path) 72 | return path 73 | 74 | def Build(pReleaseDirectory: str, pRebuild: bool): 75 | if pRebuild == True: 76 | Progress("Cleaning build") 77 | try: 78 | shutil.rmtree(BUILD_PATH) 79 | except FileNotFoundError: 80 | pass 81 | 82 | Progress("Starting build") 83 | build_output = subprocess.run( 84 | [MSBUILD_PATH, os.path.join(SCRIPT_PATH, BUILD_ALL_PROJECT), "-maxCpuCount", "-verbosity:diagnostic", "-consoleLoggerParameters:PerformanceSummary"], 85 | capture_output=True) 86 | Progress("Finished build") 87 | 88 | os.makedirs(os.path.join(pReleaseDirectory, "BuildLogs"), exist_ok=True) 89 | with open(os.path.join(pReleaseDirectory, "BuildLogs", "release_build_stdout.log"), "wb") as file: 90 | file.write(build_output.stdout) 91 | with open(os.path.join(pReleaseDirectory, "BuildLogs", "release_build_stderr.log"), "wb") as file: 92 | file.write(build_output.stderr) 93 | 94 | if (len(build_output.stderr) > 0): 95 | print(build_output.stderr.decode("utf8")) 96 | assert build_output.returncode == 0, "Build failed" 97 | 98 | Progress("Finished saving build logs") 99 | 100 | def SetupTestDirectory(pConfiguration: str): 101 | shutil.copytree( 102 | os.path.join(SCRIPT_PATH, "test", "xevtc_logs"), 103 | os.path.join(BUILD_PATH, pConfiguration, "xevtc_logs"), 104 | dirs_exist_ok=True 105 | ) 106 | Progress("Setup test directory for {}".format(pConfiguration)) 107 | 108 | async def _RunTests(pReleaseDirectory: str, pConfiguration: str): 109 | process = await asyncio.create_subprocess_exec( 110 | os.path.join(BUILD_PATH, pConfiguration, TEST_BINARY_NAME), 111 | cwd=os.path.join(BUILD_PATH, pConfiguration), 112 | stdout=asyncio.subprocess.PIPE, 113 | stderr=asyncio.subprocess.PIPE) 114 | Progress("Started tests for {}".format(pConfiguration)) 115 | stdout, stderr = await process.communicate() 116 | Progress("Finished tests for {}".format(pConfiguration)) 117 | 118 | with open(os.path.join(pReleaseDirectory, "TestLogs", "{}_tests_stdout.log".format(pConfiguration)), "wb") as file: 119 | file.write(stdout) 120 | with open(os.path.join(pReleaseDirectory, "TestLogs", "{}_tests_stderr.log".format(pConfiguration)), "wb") as file: 121 | file.write(stderr) 122 | 123 | Progress("Finished saving test logs for {}".format(pConfiguration)) 124 | assert process.returncode == 0, "Tests failed for {}".format(pConfiguration) 125 | 126 | async def Test(pReleaseDirectory: str): 127 | os.makedirs(os.path.join(pReleaseDirectory, "TestLogs"), exist_ok=True) 128 | 129 | for configuration in CONFIGURATIONS: 130 | SetupTestDirectory(configuration) 131 | await _RunTests(pReleaseDirectory, configuration) 132 | 133 | # Can't run in parallel since the tests bind to ports and thus interfere with eachother 134 | #await asyncio.gather(*[_RunTests(pReleaseDirectory, configuration) for configuration in CONFIGURATIONS]) 135 | 136 | def Archive(pReleaseDirectory: str): 137 | Progress("Archiving binaries") 138 | for configuration in CONFIGURATIONS: 139 | os.mkdir(os.path.join(pReleaseDirectory, configuration)) 140 | for target in ARCHVING_TARGETS: 141 | shutil.copy2( 142 | os.path.join(BUILD_PATH, configuration, target), 143 | os.path.join(pReleaseDirectory, configuration, target)) 144 | 145 | Progress("Archiving complete") 146 | 147 | def Do_Release(pVersionString: str): 148 | version_internal = ExtractInternalVersion(pVersionString) 149 | release_directory = CreateReleaseDirectory(pVersionString) 150 | ChangeResourceVersion(version_internal) 151 | Build(release_directory, True) 152 | asyncio.run(Test(release_directory)) 153 | Archive(release_directory) 154 | Progress("Release done") 155 | 156 | def Do_Test(): 157 | Build(BUILD_PATH, False) 158 | asyncio.run(Test(BUILD_PATH)) 159 | Progress("Do_Test done") 160 | 161 | #Do_Test() 162 | Do_Release("v2.15.rc1") 163 | -------------------------------------------------------------------------------- /resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by arcdps_personal_stats.rc 4 | // 5 | #define IDR_ROOT_CERTIFICATES 101 6 | #define IDB_PNG_SPEC_GUARDIAN 102 7 | #define IDB_PNG_SPEC_WARRIOR 103 8 | #define IDB_PNG_SPEC_ENGINEER 104 9 | #define IDB_PNG_SPEC_RANGER 105 10 | #define IDB_PNG_SPEC_THIEF 106 11 | #define IDB_PNG_SPEC_ELEMENTALIST 107 12 | #define IDB_PNG_SPEC_MESMER 108 13 | #define IDB_PNG_SPEC_NECROMANCER 109 14 | #define IDB_PNG_SPEC_REVENANT 110 15 | #define IDB_PNG_SPEC_DRAGONHUNTER 111 16 | #define IDB_PNG_SPEC_FIREBRAND 112 17 | #define IDB_PNG_SPEC_WILLBENDER 113 18 | #define IDB_PNG_SPEC_BERSERKER 114 19 | #define IDB_PNG_SPEC_SPELLBREAKER 115 20 | #define IDB_PNG_SPEC_BLADESWORN 116 21 | #define IDB_PNG_SPEC_SCRAPPER 117 22 | #define IDB_PNG_SPEC_HOLOSMITH 118 23 | #define IDB_PNG_SPEC_MECHANIST 119 24 | #define IDB_PNG_SPEC_DRUID 120 25 | #define IDB_PNG_SPEC_SOULBEAST 121 26 | #define IDB_PNG_SPEC_UNTAMED 122 27 | #define IDB_PNG_SPEC_DAREDEVIL 123 28 | #define IDB_PNG_SPEC_DEADEYE 124 29 | #define IDB_PNG_SPEC_SPECTER 125 30 | #define IDB_PNG_SPEC_TEMPEST 126 31 | #define IDB_PNG_SPEC_WEAVER 127 32 | #define IDB_PNG_SPEC_CATALYST 128 33 | #define IDB_PNG_SPEC_CHRONOMANCER 129 34 | #define IDB_PNG_SPEC_MIRAGE 130 35 | #define IDB_PNG_SPEC_VIRTUOSO 131 36 | #define IDB_PNG_SPEC_REAPER 132 37 | #define IDB_PNG_SPEC_SCOURGE 133 38 | #define IDB_PNG_SPEC_HARBINGER 134 39 | #define IDB_PNG_SPEC_HERALD 135 40 | #define IDB_PNG_SPEC_RENEGADE 136 41 | #define IDB_PNG_SPEC_VINDICATOR 137 42 | #define IDB_PNG_SPEC_NONE 138 43 | #define IDB_PNG_SPEC_LUMINARY 139 44 | #define IDB_PNG_SPEC_PARAGON 140 45 | #define IDB_PNG_SPEC_AMALGAM 141 46 | #define IDB_PNG_SPEC_GALESHOT 142 47 | #define IDB_PNG_SPEC_ANTIQUARY 143 48 | #define IDB_PNG_SPEC_EVOKER 144 49 | #define IDB_PNG_SPEC_TROUBADOUR 145 50 | #define IDB_PNG_SPEC_RITUALIST 146 51 | #define IDB_PNG_SPEC_CONDUIT 147 52 | 53 | // Next default values for new objects 54 | // 55 | #ifdef APSTUDIO_INVOKED 56 | #ifndef APSTUDIO_READONLY_SYMBOLS 57 | #define _APS_NEXT_RESOURCE_VALUE 148 58 | #define _APS_NEXT_COMMAND_VALUE 40001 59 | #define _APS_NEXT_CONTROL_VALUE 1001 60 | #define _APS_NEXT_SYMED_VALUE 101 61 | #endif 62 | #endif 63 | -------------------------------------------------------------------------------- /simpleini/LICENCE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2006-2013 Brodie Thiesfield 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/AddonVersion.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define HEALING_STATS_ADDON_SIGNATURE 0x9c9b3c99U 6 | #define HEALING_STATS_EVTC_REVISION 2U 7 | #define LEGACY_INI_CONFIG_PATH "addons\\arcdps\\arcdps_healing_stats.ini" 8 | #define JSON_CONFIG_PATH "addons\\arcdps\\arcdps_healing_stats.json" 9 | 10 | #define VERSION_EVENT_SIGNATURE 0x00000000U 11 | struct EvtcVersionHeader 12 | { 13 | uint32_t Signature; 14 | uint32_t EvtcRevision : 24; 15 | uint32_t VersionStringLength : 8; 16 | }; 17 | 18 | enum HealingEventFlags : uint8_t 19 | { 20 | // The target of the event is downed. Only gets set for buff damage/healing events since arcdps overrides pad61, for 21 | // direct damage/healing, look at the lower bits of is_offcycle instead (as documented in arcdps evtc documentation) 22 | HealingEventFlags_TargetIsDowned = 1 << 5, 23 | // Signifies that the agent identified by dst_agent/dst_instid generated the event. This lets an event parser figure 24 | // out which agents are sending events, and as a result also for which players the healing data is complete 25 | HealingEventFlags_EventCameFromDestination = 1 << 6, 26 | // Signifies that the agent identified by src_agent/src_instid generated the event. This lets an event parser figure 27 | // out which agents are sending events, and as a result also for which players the healing data is complete 28 | HealingEventFlags_EventCameFromSource = 1 << 7 29 | }; 30 | -------------------------------------------------------------------------------- /src/AgentTable.cpp: -------------------------------------------------------------------------------- 1 | #include "AgentTable.h" 2 | #include "Log.h" 3 | 4 | #include 5 | 6 | HealedAgent::HealedAgent(uint16_t pInstanceId, const char* pAgentName, const char* pAccountName, uint16_t pSubgroup, bool pIsMinion, bool pIsPlayer, Prof pProfession, uint32_t pElite) 7 | : InstanceId{pInstanceId} 8 | , Name{pAgentName} 9 | , AccountName{pAccountName != nullptr ? pAccountName : ""} 10 | , Subgroup{pSubgroup} 11 | , IsMinion{pIsMinion} 12 | , IsPlayer{pIsPlayer} 13 | , Profession{pProfession} 14 | , Elite{pElite} 15 | { 16 | } 17 | 18 | HealedAgent::HealedAgent(const char* pAgentName) 19 | : InstanceId{0} 20 | , Name{pAgentName} 21 | , AccountName{} 22 | , Subgroup{0} 23 | , IsMinion{false} 24 | , IsPlayer{false} 25 | , Profession{Prof::PROF_UNKNOWN} 26 | , Elite{0xFFFFFFFF} 27 | { 28 | } 29 | 30 | HealedAgent::HealedAgent(std::string&& pAgentName) 31 | : InstanceId{0} 32 | , Name{std::move(pAgentName)} 33 | , AccountName{} 34 | , Subgroup{0} 35 | , IsMinion{false} 36 | , IsPlayer{false} 37 | , Profession{Prof::PROF_UNKNOWN} 38 | , Elite{0xFFFFFFFF} 39 | { 40 | } 41 | 42 | bool HealedAgent::operator==(const HealedAgent& pRight) const 43 | { 44 | return std::tie(InstanceId, Name, AccountName, Subgroup, IsMinion, IsPlayer, Profession, Elite) == std::tie(pRight.InstanceId, pRight.Name, pRight.AccountName, pRight.Subgroup, pRight.IsMinion, pRight.IsPlayer, pRight.Profession, pRight.Elite); 45 | } 46 | 47 | bool HealedAgent::operator!=(const HealedAgent& pRight) const 48 | { 49 | return (*this == pRight) == false; 50 | } 51 | 52 | void AgentTable::AddAgent(uintptr_t pUniqueId, uint16_t pInstanceId, const char* pAgentName, const char* pAccountName, std::optional pSubgroup, std::optional pIsMinion, std::optional pIsPlayer, Prof pProfession, uint32_t pElite) 53 | { 54 | assert(pAgentName != nullptr); 55 | LOG("Inserting new agent %llu %hu %s %hu %s %s", pUniqueId, pInstanceId, pAgentName, pSubgroup.value_or(0), BOOL_STR(pIsMinion.value_or(false)), BOOL_STR(pIsPlayer.value_or(false))); 56 | 57 | std::lock_guard lock(mLock); 58 | 59 | auto [agent, agentInserted] = mAgents.try_emplace(pUniqueId, pInstanceId, pAgentName, pAccountName, pSubgroup.value_or(0), pIsMinion.value_or(false), pIsPlayer.value_or(false), pProfession, pElite); 60 | if (agentInserted == false) 61 | { 62 | if ((strcmp(agent->second.Name.c_str(), pAgentName) != 0) 63 | || (pAccountName != nullptr && strcmp(agent->second.AccountName.c_str(), pAccountName) != 0) 64 | || (agent->second.InstanceId != pInstanceId) 65 | || (pSubgroup.has_value() && agent->second.Subgroup != *pSubgroup) 66 | || (pIsMinion.has_value() && agent->second.IsMinion != *pIsMinion) 67 | || (pIsPlayer.has_value() && agent->second.IsPlayer != *pIsPlayer) 68 | || (agent->second.Profession != pProfession) 69 | || (agent->second.Elite != pElite)) 70 | { 71 | LOG("Unique id %llu already exists - replacing existing entry %hu %s %hu %s %s", 72 | pUniqueId, agent->second.InstanceId, agent->second.Name.c_str(), agent->second.Subgroup, BOOL_STR(agent->second.IsMinion), BOOL_STR(agent->second.IsPlayer)); 73 | 74 | agent->second = HealedAgent{ 75 | pInstanceId, 76 | pAgentName, 77 | pAccountName != nullptr ? pAccountName : agent->second.AccountName.c_str(), 78 | pSubgroup.value_or(agent->second.Subgroup), 79 | pIsMinion.value_or(agent->second.IsMinion), 80 | pIsPlayer.value_or(agent->second.IsPlayer), 81 | pProfession, 82 | pElite}; 83 | } 84 | } 85 | 86 | auto [iter, instidInserted] = mInstanceIds.try_emplace(pInstanceId, agent); 87 | if (instidInserted == false && iter->second != agent) 88 | { 89 | LOG("Instance id %hu already exists - replacing existing entry %llu %s %hu %s %s", 90 | pInstanceId, iter->second->first, iter->second->second.Name.c_str(), iter->second->second.Subgroup, BOOL_STR(iter->second->second.IsMinion), BOOL_STR(iter->second->second.IsPlayer)); 91 | iter->second = agent; 92 | } 93 | } 94 | 95 | std::optional AgentTable::GetUniqueId(uint16_t pInstanceId, bool pAllowNonPlayer) 96 | { 97 | std::lock_guard lock(mLock); 98 | 99 | auto iter = mInstanceIds.find(pInstanceId); 100 | if (iter == mInstanceIds.end()) 101 | { 102 | LOG("Couldn't find instance id %hu", pInstanceId); 103 | return std::nullopt; 104 | } 105 | 106 | if (pAllowNonPlayer == false && iter->second->second.IsPlayer == false) 107 | { 108 | LOG("Mapped %hu to %llu but it is not a player (%hu %s %hu %s %s)", pInstanceId, iter->second->first, 109 | iter->second->second.InstanceId, iter->second->second.Name.c_str(), iter->second->second.Subgroup, BOOL_STR(iter->second->second.IsMinion), BOOL_STR(iter->second->second.IsPlayer)); 110 | return std::nullopt; 111 | } 112 | 113 | LOG("Mapping %hu to %llu", pInstanceId, iter->second->first); 114 | return iter->second->first; 115 | } 116 | 117 | std::optional AgentTable::GetName(uintptr_t pUniqueId) 118 | { 119 | std::lock_guard lock(mLock); 120 | 121 | auto iter = mAgents.find(pUniqueId); 122 | if (iter == mAgents.end()) 123 | { 124 | LOG("Couldn't find unique id %llu", pUniqueId); 125 | return std::nullopt; 126 | } 127 | 128 | DEBUGLOG("Mapping %llu to %s", pUniqueId, iter->second.Name.c_str()); 129 | return iter->second.Name; 130 | } 131 | 132 | std::optional AgentTable::GetAgentData(uintptr_t pUniqueId) 133 | { 134 | std::lock_guard lock(mLock); 135 | 136 | auto iter = mAgents.find(pUniqueId); 137 | if (iter == mAgents.end()) 138 | { 139 | LogD("Couldn't find unique id {}", pUniqueId); 140 | return std::nullopt; 141 | } 142 | 143 | LogT("Mapping {} to {}", pUniqueId, iter->second.Name.c_str()); 144 | return iter->second; 145 | } 146 | 147 | std::map AgentTable::GetState() 148 | { 149 | std::map result; 150 | { 151 | std::lock_guard lock(mLock); 152 | result = mAgents; 153 | } 154 | 155 | return result; 156 | } 157 | -------------------------------------------------------------------------------- /src/AgentTable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | struct HealedAgent 11 | { 12 | uint16_t InstanceId; 13 | std::string Name; // Agent Name (UTF8) 14 | std::string AccountName; 15 | uint16_t Subgroup; 16 | bool IsMinion; 17 | bool IsPlayer; 18 | Prof Profession = Prof::PROF_UNKNOWN; 19 | uint32_t Elite = 0xFFFFFFFF; 20 | 21 | HealedAgent() = default; 22 | HealedAgent(const char* pAgentName); 23 | HealedAgent(std::string&& pAgentName); 24 | HealedAgent(uint16_t pInstanceId, const char* pAgentName, const char* pAccountName, uint16_t pSubgroup, bool pIsMinion, bool pIsPlayer, Prof pProfession, uint32_t pElite); 25 | 26 | bool operator==(const HealedAgent& other) const; 27 | bool operator!=(const HealedAgent& pRight) const; 28 | }; 29 | 30 | class AgentTable 31 | { 32 | public: 33 | void AddAgent(uintptr_t pUniqueId, uint16_t pInstanceId, const char* pAgentName, const char* pAccountName, std::optional pSubgroup, std::optional pIsMinion, std::optional pIsPlayer, Prof pProfession, uint32_t pElite); 34 | 35 | std::optional GetUniqueId(uint16_t pInstanceId, bool pAllowNonPlayer); 36 | std::optional GetName(uintptr_t pUniqueId); 37 | std::optional GetAgentData(uintptr_t pUniqueId); 38 | 39 | std::map GetState(); 40 | 41 | private: 42 | std::mutex mLock; 43 | std::map mAgents; // 44 | std::map::iterator> mInstanceIds; // 45 | }; -------------------------------------------------------------------------------- /src/AggregatedStats.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "State.h" 4 | #include "EventProcessor.h" 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | enum class GroupFilter 15 | { 16 | Group = 0, 17 | Squad = 1, 18 | AllExcludingMinions = 2, 19 | All = 3, 20 | Max 21 | }; 22 | 23 | struct AggregatedStatsEntry 24 | { 25 | uint64_t Id; 26 | HealedAgent Agent; 27 | float TimeInCombat; 28 | 29 | uint64_t Healing; 30 | uint64_t Hits; 31 | std::optional Casts; 32 | uint64_t BarrierGeneration; 33 | 34 | AggregatedStatsEntry(uint64_t pId, HealedAgent&& pAgent, float pTimeInCombat, uint64_t pHealing, uint64_t pHits, std::optional pCasts, uint64_t pBarrierGeneration); 35 | 36 | auto GetTie() const 37 | { 38 | return std::tie(Id, Agent, TimeInCombat, Healing, Hits, Casts, BarrierGeneration); 39 | } 40 | }; 41 | 42 | struct AggregatedVector 43 | { 44 | std::vector Entries; 45 | uint64_t HighestHealing{0}; 46 | 47 | void Add(uint64_t pId, HealedAgent&& pAgent, float pTimeInCombat, uint64_t pHealing, uint64_t pHits, std::optional pCasts, uint64_t pBarrierGeneration); 48 | }; 49 | 50 | 51 | using TotalHealingStats = std::array(GroupFilter::Max)>; 52 | 53 | constexpr static uint32_t IndirectHealingSkillId = 0; 54 | 55 | class AggregatedStatsCollection; 56 | class AggregatedStats 57 | { 58 | friend AggregatedStatsCollection; 59 | public: 60 | AggregatedStats(HealingStats&& pSourceData, const HealWindowOptions& pOptions, bool pDebugMode); 61 | 62 | const AggregatedStatsEntry& GetTotal(); 63 | const AggregatedVector& GetStats(DataSource pDataSource); 64 | const AggregatedVector& GetDetails(DataSource pDataSource, uint64_t pId); 65 | 66 | const AggregatedVector& GetGroupFilterTotals(); 67 | 68 | float GetCombatTime(); 69 | 70 | private: 71 | uint64_t GetCombatEnd(); 72 | 73 | const AggregatedVector& GetAgents(std::optional pSkillId); 74 | const AggregatedVector& GetSkills(std::optional pAgentId); 75 | 76 | static void Sort(std::vector& pVector, SortOrder pSortOrder); 77 | 78 | bool Filter(uintptr_t pAgentId) const; // Returns true if agent should be filtered out 79 | bool Filter(std::map::const_iterator& pAgent) const; // Returns true if agent should be filtered out 80 | bool FilterInternal(std::map::const_iterator& pAgent, const HealWindowOptions& pFilter) const; // Returns true if agent should be filtered out 81 | 82 | HealingStats mySourceData; 83 | 84 | HealWindowOptions myOptions; 85 | bool myDebugMode; 86 | 87 | float myCombatTime = NAN; 88 | std::unique_ptr myTotal; 89 | std::unique_ptr myFilteredAgents; 90 | std::unique_ptr mySkills; 91 | std::unique_ptr myGroupFilterTotals; 92 | 93 | std::map myAgentsDetailed; // uintptr_t => agent id 94 | std::map mySkillsDetailed; // uint32_t => skill id 95 | }; -------------------------------------------------------------------------------- /src/AggregatedStatsCollection.cpp: -------------------------------------------------------------------------------- 1 | #include "AggregatedStatsCollection.h" 2 | 3 | #include 4 | 5 | const static AggregatedVector EMPTY_STATS; 6 | 7 | AggregatedStatsCollection::Player::Player(HealedAgent&& pAgent, HealingStats&& pStats, const HealWindowOptions& pOptions, bool pDebugMode) 8 | : Agent{std::move(pAgent)} 9 | , Stats{std::move(pStats), pOptions, pDebugMode} 10 | { 11 | } 12 | 13 | AggregatedStatsCollection::AggregatedStatsCollection(std::map>&& pPeerStates, uintptr_t pLocalUniqueId, const HealWindowOptions& pOptions, bool pDebugMode) 14 | : mOptions{pOptions} 15 | , mDebugMode{pDebugMode} 16 | { 17 | mLocalState = mSourceData.end(); 18 | for (auto& [id, state] : pPeerStates) 19 | { 20 | auto [iter, inserted] = mSourceData.try_emplace(id, std::move(state.first), std::move(state.second), pOptions, pDebugMode); 21 | assert(inserted == true); 22 | if (id == pLocalUniqueId) 23 | { 24 | mLocalState = iter; 25 | } 26 | } 27 | 28 | assert(mLocalState != mSourceData.end()); 29 | } 30 | 31 | const AggregatedStatsEntry& AggregatedStatsCollection::GetTotal(DataSource pDataSource) 32 | { 33 | if (pDataSource != DataSource::PeersOutgoing) 34 | { 35 | return mLocalState->second.Stats.GetTotal(); 36 | } 37 | 38 | if (mPeersOutgoingTotal != nullptr) 39 | { 40 | return *mPeersOutgoingTotal; 41 | } 42 | 43 | uint64_t healing = 0; 44 | uint64_t hits = 0; 45 | uint64_t barrierGeneration = 0; 46 | const AggregatedVector& sourceStats = GetStats(pDataSource); 47 | 48 | for (const AggregatedStatsEntry& entry : sourceStats.Entries) 49 | { 50 | healing += entry.Healing; 51 | hits += entry.Hits; 52 | barrierGeneration += entry.BarrierGeneration; 53 | } 54 | 55 | mPeersOutgoingTotal = std::make_unique(0, HealedAgent{ "__SUPERTOTAL__" }, mLocalState->second.Stats.GetCombatTime(), healing, hits, std::nullopt, barrierGeneration); 56 | return *mPeersOutgoingTotal; 57 | } 58 | 59 | const AggregatedVector& AggregatedStatsCollection::GetStats(DataSource pDataSource) 60 | { 61 | if (pDataSource != DataSource::PeersOutgoing) 62 | { 63 | return mLocalState->second.Stats.GetStats(pDataSource); 64 | } 65 | 66 | if (mPeersOutgoingStats != nullptr) 67 | { 68 | return *mPeersOutgoingStats; 69 | } 70 | 71 | mPeersOutgoingStats = std::make_unique(); 72 | 73 | for (auto& [id, source] : mSourceData) 74 | { 75 | const AggregatedStatsEntry& entry = source.Stats.GetTotal(); 76 | mPeersOutgoingStats->Add(id, HealedAgent{source.Agent}, source.Stats.GetCombatTime(), entry.Healing, entry.Hits, entry.Casts, entry.BarrierGeneration); 77 | } 78 | 79 | AggregatedStats::Sort(mPeersOutgoingStats->Entries, mOptions.SortOrderChoice); 80 | return *mPeersOutgoingStats; 81 | } 82 | 83 | const AggregatedVector& AggregatedStatsCollection::GetDetails(DataSource pDataSource, uint64_t pId) 84 | { 85 | if (pDataSource != DataSource::PeersOutgoing) 86 | { 87 | return mLocalState->second.Stats.GetDetails(pDataSource, pId); 88 | } 89 | 90 | auto iter = mSourceData.find(pId); 91 | if (iter == mSourceData.end()) 92 | { 93 | return EMPTY_STATS; 94 | } 95 | 96 | return iter->second.Stats.GetStats(DataSource::Skills); 97 | } 98 | 99 | const AggregatedVector& AggregatedStatsCollection::GetGroupFilterTotals() 100 | { 101 | return mLocalState->second.Stats.GetGroupFilterTotals(); 102 | } 103 | 104 | float AggregatedStatsCollection::GetCombatTime() 105 | { 106 | return mLocalState->second.Stats.GetCombatTime(); 107 | } 108 | -------------------------------------------------------------------------------- /src/AggregatedStatsCollection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "AggregatedStats.h" 3 | 4 | class AggregatedStatsCollection 5 | { 6 | struct Player 7 | { 8 | Player(HealedAgent&& pAgent, HealingStats&& pStats, const HealWindowOptions& pOptions, bool pDebugMode); 9 | 10 | AggregatedStats Stats; 11 | HealedAgent Agent; 12 | }; 13 | 14 | public: 15 | AggregatedStatsCollection(std::map>&& pPeerStates, uintptr_t pLocalUniqueId, const HealWindowOptions& pOptions, bool pDebugMode); 16 | 17 | const AggregatedStatsEntry& GetTotal(DataSource pDataSource); 18 | const AggregatedVector& GetStats(DataSource pDataSource); 19 | const AggregatedVector& GetDetails(DataSource pDataSource, uint64_t pId); 20 | 21 | const AggregatedVector& GetGroupFilterTotals(); 22 | 23 | float GetCombatTime(); 24 | 25 | private: 26 | std::unique_ptr mPeersOutgoingStats; 27 | std::unique_ptr mPeersOutgoingTotal; 28 | 29 | std::map::iterator mLocalState; 30 | std::map mSourceData; 31 | const HealWindowOptions mOptions; 32 | const bool mDebugMode; 33 | }; -------------------------------------------------------------------------------- /src/Common.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | enum class EventType 6 | { 7 | Damage, 8 | Healing, 9 | SemiDamaging, // non-damaging events that still reset combat time 10 | Other, 11 | }; 12 | 13 | static inline EventType GetEventType(const cbtevent* pEvent, bool pIsLocal) 14 | { 15 | if (pEvent->is_statechange != 0 || pEvent->is_activation != 0 || pEvent->is_buffremove != 0) 16 | { 17 | return EventType::Other; 18 | } 19 | 20 | if (pEvent->buff == 0) 21 | { 22 | switch (pEvent->result) 23 | { 24 | case CBTR_NORMAL: 25 | case CBTR_CRIT: 26 | case CBTR_GLANCE: 27 | break; 28 | case CBTR_ACTIVATION: 29 | return EventType::Other; 30 | default: 31 | return EventType::SemiDamaging; // Breakbar / misc. 32 | } 33 | 34 | if ((pIsLocal && pEvent->value <= 0) || (!pIsLocal && pEvent->value >= 0)) 35 | { 36 | return EventType::Damage; // Direct damage 37 | } 38 | else 39 | { 40 | return EventType::Healing; // Direct healing 41 | } 42 | } 43 | else 44 | { 45 | if (pEvent->buff_dmg == 0) 46 | { 47 | return EventType::SemiDamaging; // Buff apply 48 | } 49 | else if ((pIsLocal && pEvent->buff_dmg <= 0) || (!pIsLocal && pEvent->buff_dmg >= 0)) 50 | { 51 | return EventType::Damage; // Buff damage 52 | } 53 | else 54 | { 55 | return EventType::Healing; // Buff healing (e.g. Regeneration) 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/EventProcessor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "AgentTable.h" 3 | #include "PlayerStats.h" 4 | #include "Skills.h" 5 | 6 | #include 7 | 8 | #include 9 | 10 | struct HealingStats : HealingStatsSlim 11 | { 12 | uint64_t CollectionTime = 0; 13 | 14 | std::map Agents; // 15 | std::shared_ptr Skills; // 16 | }; 17 | 18 | class EventProcessor 19 | { 20 | public: 21 | EventProcessor(); 22 | 23 | void SetEvtcLoggingEnabled(bool pEnabled); 24 | 25 | void AreaCombat(cbtevent* pEvent, ag* pSourceAgent, ag* pDestinationAgent, const char* pSkillname, uint64_t pId, uint64_t pRevision); 26 | void LocalCombat(cbtevent* pEvent, ag* pSourceAgent, ag* pDestinationAgent, const char* pSkillname, uint64_t pId, uint64_t pRevision, std::optional* pModifiedEvent = nullptr); 27 | void PeerCombat(cbtevent* pEvent, uint16_t pPeerInstanceId); 28 | 29 | // Returns > 30 | // pSelfUniqueId is only specified in testing 31 | std::pair>> GetState(uintptr_t pSelfUniqueId = 0); 32 | 33 | #ifndef TEST 34 | private: 35 | #endif 36 | void PreProcessEvent(cbtevent* pEvent, bool pIsLocal); 37 | 38 | PlayerStats mLocalState; 39 | std::atomic mSelfInstanceId = UINT32_MAX; 40 | std::atomic mSelfUniqueId = UINT64_MAX; 41 | 42 | AgentTable mAgentTable; 43 | std::shared_ptr mSkillTable; 44 | 45 | std::mutex mPeerStatesLock; 46 | std::map> mPeerStates; 47 | 48 | std::atomic_bool mEvtcLoggingEnabled = false; 49 | }; -------------------------------------------------------------------------------- /src/EventSequencer.cpp: -------------------------------------------------------------------------------- 1 | #include "EventSequencer.h" 2 | #include "Log.h" 3 | 4 | #include 5 | 6 | EventSequencer::EventSequencer(const CombatCallbackSignature pCallback) 7 | : mCallback(pCallback) 8 | { 9 | for (uint32_t i = 0; i < MAX_QUEUED_EVENTS; i++) 10 | { 11 | mQueuedEvents[i].id = 0; 12 | } 13 | } 14 | 15 | uintptr_t EventSequencer::ProcessEvent(cbtevent* pEvent, ag* pSourceAgent, ag* pDestinationAgent, const char* pSkillname, uint64_t pId, uint64_t pRevision) 16 | { 17 | if (pId == 0) // id 0 can occur multiple times and is unordered 18 | { 19 | LogT("Id0 event"); 20 | 21 | mCallback(pEvent, pSourceAgent, pDestinationAgent, pSkillname, pId, pRevision); 22 | return 0; 23 | } 24 | 25 | uint64_t current = mHighestId.load(std::memory_order_acquire); 26 | while (true) 27 | { 28 | LogT(">> {}", pId); 29 | if (current == pId) 30 | { 31 | //assert(false); 32 | 33 | LogW("Received event {} twice!", pId); 34 | 35 | mCallback(pEvent, pSourceAgent, pDestinationAgent, pSkillname, pId, pRevision); 36 | return 0; 37 | } 38 | 39 | if (current == UINT64_MAX) 40 | { 41 | bool result = mHighestId.compare_exchange_weak(current, pId - 1, std::memory_order_acq_rel); 42 | DBG_UNREFERENCED_LOCAL_VARIABLE(result); 43 | 44 | LogD("Registered first event ({}) - result {}", pId, BOOL_STR(result)); 45 | 46 | current = mHighestId.load(std::memory_order_acquire); 47 | continue; 48 | } 49 | else if (current > (pId - 1)) // Race condition after registering first event 50 | { 51 | LogD("Got event lower than current highest seen ({} vs {})", pId, current); 52 | 53 | mCallback(pEvent, pSourceAgent, pDestinationAgent, pSkillname, pId, pRevision); 54 | return 0; 55 | } 56 | else if (current == (pId - 1)) // Fast path (most common) 57 | { 58 | mCallback(pEvent, pSourceAgent, pDestinationAgent, pSkillname, pId, pRevision); 59 | 60 | if (mHighestId.compare_exchange_strong(current, pId, std::memory_order_acq_rel) == false) 61 | { 62 | assert(false); 63 | } 64 | 65 | TryFlushEvents(); 66 | 67 | return 0; 68 | } 69 | else 70 | { 71 | std::shared_lock lock(mLock); 72 | 73 | // change if current changed since waiting for lock could potentially take quite long 74 | uint64_t current2 = mHighestId.load(std::memory_order_acquire); 75 | if (current != current2) 76 | { 77 | LogD("Race1 current {} current2 {}", current, current2); 78 | 79 | current = current2; 80 | continue; 81 | } 82 | 83 | // Add first so that another racing thread is sure to read mQueuedLocalEventCount != 0 after changing 84 | // the value (or it is not able to change the value). 85 | uint32_t index = mQueuedEventCount.fetch_add(1, std::memory_order_acq_rel); 86 | if (index >= MAX_QUEUED_EVENTS) 87 | { 88 | LogD("More than max events queued - {} {} {} {}", index, MAX_QUEUED_EVENTS, pId, current); 89 | // Subtracting here has the same issue as described below in Race2 90 | 91 | assert(false); 92 | 93 | mCallback(pEvent, pSourceAgent, pDestinationAgent, pSkillname, pId, pRevision); 94 | return 0; 95 | } 96 | 97 | current2 = mHighestId.load(std::memory_order_acquire); 98 | if (current != current2) 99 | { 100 | // We can't decrement because of the race condition 101 | // Thread2: fetch_add (0) 102 | // Thread3: fetch_add (1) 103 | // Thread3: read mHighestId 104 | // Thread1: change mHighestId 105 | // Thread2: read mHighestId 106 | // 107 | // If we were to decrement at that point, then index 1 would be given out twice 108 | // So instead, we just add the event as completely empty. 109 | mQueuedEvents[index].id = 0; 110 | 111 | LogD("Race2 current {} current2 {} index {}", current, current2, index); 112 | 113 | current = current2; 114 | continue; 115 | } 116 | 117 | if (pEvent != nullptr) 118 | { 119 | *static_cast(&mQueuedEvents[index].ev) = *pEvent; 120 | mQueuedEvents[index].ev.present = true; 121 | } 122 | else 123 | { 124 | mQueuedEvents[index].ev.present = false; 125 | } 126 | 127 | if (pSourceAgent != nullptr) 128 | { 129 | mQueuedEvents[index].source_ag.id = pSourceAgent->id; 130 | mQueuedEvents[index].source_ag.prof = pSourceAgent->prof; 131 | mQueuedEvents[index].source_ag.elite = pSourceAgent->elite; 132 | mQueuedEvents[index].source_ag.self = pSourceAgent->self; 133 | mQueuedEvents[index].source_ag.team = pSourceAgent->team; 134 | 135 | if (pSourceAgent->name != nullptr) 136 | { 137 | mQueuedEvents[index].source_ag.name_storage = pSourceAgent->name; 138 | mQueuedEvents[index].source_ag.name = mQueuedEvents[index].source_ag.name_storage.c_str(); 139 | } 140 | else 141 | { 142 | mQueuedEvents[index].source_ag.name = nullptr; 143 | } 144 | 145 | mQueuedEvents[index].source_ag.present = true; 146 | } 147 | else 148 | { 149 | mQueuedEvents[index].source_ag.present = false; 150 | } 151 | 152 | if (pDestinationAgent != nullptr) 153 | { 154 | mQueuedEvents[index].destination_ag.id = pDestinationAgent->id; 155 | mQueuedEvents[index].destination_ag.prof = pDestinationAgent->prof; 156 | mQueuedEvents[index].destination_ag.elite = pDestinationAgent->elite; 157 | mQueuedEvents[index].destination_ag.self = pDestinationAgent->self; 158 | mQueuedEvents[index].destination_ag.team = pDestinationAgent->team; 159 | 160 | if (pDestinationAgent->name != nullptr) 161 | { 162 | mQueuedEvents[index].destination_ag.name_storage = pDestinationAgent->name; 163 | mQueuedEvents[index].destination_ag.name = mQueuedEvents[index].destination_ag.name_storage.c_str(); 164 | } 165 | else 166 | { 167 | mQueuedEvents[index].destination_ag.name = nullptr; 168 | } 169 | 170 | mQueuedEvents[index].destination_ag.present = true; 171 | } 172 | else 173 | { 174 | mQueuedEvents[index].destination_ag.present = false; 175 | } 176 | 177 | mQueuedEvents[index].skillname = pSkillname; 178 | mQueuedEvents[index].id = pId; 179 | mQueuedEvents[index].revision = pRevision; 180 | 181 | LogT("Queued {} at index {}, current {}", pId, index, current); 182 | return 0; 183 | } 184 | } 185 | } 186 | 187 | bool EventSequencer::QueueIsEmpty() 188 | { 189 | bool isEmpty = (mQueuedEventCount.load(std::memory_order_acquire) == 0); 190 | 191 | LogD("isEmpty={}, highestQueueSize={} highestId={} queuedEventCount={}", 192 | BOOL_STR(isEmpty), 193 | mHighestQueueSize.load(std::memory_order_relaxed), 194 | mHighestId.load(std::memory_order_relaxed), 195 | mQueuedEventCount.load(std::memory_order_relaxed)); 196 | return isEmpty; 197 | } 198 | 199 | void EventSequencer::TryFlushEvents() 200 | { 201 | if (mQueuedEventCount.load(std::memory_order_acquire) == 0) 202 | { 203 | return; 204 | } 205 | 206 | std::unique_lock lock(mLock); 207 | 208 | uint32_t eventCount = mQueuedEventCount.load(std::memory_order_acquire); 209 | if (eventCount > MAX_QUEUED_EVENTS) 210 | { 211 | // This is possible because of Race1 above 212 | LogD("Truncating event count {} {}", eventCount, MAX_QUEUED_EVENTS); 213 | eventCount = MAX_QUEUED_EVENTS; 214 | } 215 | 216 | bool removed = false; 217 | size_t nonZeroEvents = 0; 218 | do 219 | { 220 | removed = false; 221 | nonZeroEvents = 0; 222 | 223 | for (uint32_t i = 0; i < eventCount; i++) 224 | { 225 | if (mQueuedEvents[i].id == 0) 226 | { 227 | continue; 228 | } 229 | 230 | uint64_t current = mHighestId.load(std::memory_order_acquire); 231 | if (current != (mQueuedEvents[i].id - 1)) 232 | { 233 | nonZeroEvents++; 234 | LogT("Skipping {}", mQueuedEvents[i].id); 235 | continue; 236 | } 237 | 238 | LogT(">> Delayed {}", mQueuedEvents[i].id); 239 | 240 | ag source; 241 | ag destination; 242 | 243 | ag* source_arg = nullptr; 244 | ag* destination_arg = nullptr; 245 | cbtevent* ev_arg = nullptr; 246 | if (mQueuedEvents[i].source_ag.present == true) 247 | { 248 | source_arg = &source; 249 | source = *static_cast(&mQueuedEvents[i].source_ag); 250 | } 251 | 252 | if (mQueuedEvents[i].destination_ag.present == true) 253 | { 254 | destination_arg = &destination; 255 | destination = *static_cast(&mQueuedEvents[i].destination_ag); 256 | } 257 | 258 | if (mQueuedEvents[i].ev.present == true) 259 | { 260 | ev_arg = &mQueuedEvents[i].ev; 261 | } 262 | 263 | mCallback(ev_arg, source_arg, destination_arg, mQueuedEvents[i].skillname, mQueuedEvents[i].id, mQueuedEvents[i].revision); 264 | 265 | if (mHighestId.compare_exchange_strong(current, mQueuedEvents[i].id, std::memory_order_acq_rel) == false) 266 | { 267 | assert(false); 268 | } 269 | 270 | removed = true; 271 | mQueuedEvents[i].id = 0; 272 | } 273 | } while (removed == true); 274 | 275 | if (nonZeroEvents == 0) 276 | { 277 | for (uint32_t i = 0; i < MAX_QUEUED_EVENTS; i++) 278 | { 279 | assert(mQueuedEvents[i].id == 0); 280 | } 281 | 282 | uint32_t oldSize = mQueuedEventCount.exchange(0, std::memory_order_acq_rel); 283 | 284 | // No need to worry about races here, this is the only place it's written to and that's done under lock 285 | if (mHighestQueueSize.load(std::memory_order_relaxed) < oldSize) 286 | { 287 | mHighestQueueSize.store(oldSize, std::memory_order_relaxed); 288 | } 289 | 290 | LogT("Clearing size {}", oldSize); 291 | } 292 | } 293 | 294 | -------------------------------------------------------------------------------- /src/EventSequencer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | class EventSequencer 10 | { 11 | private: 12 | const static uint32_t MAX_QUEUED_EVENTS = 256; 13 | 14 | struct Event 15 | { 16 | struct : cbtevent 17 | { 18 | bool present; 19 | } ev; 20 | 21 | struct : ag 22 | { 23 | std::string name_storage; 24 | bool present; 25 | } source_ag; 26 | 27 | struct : ag 28 | { 29 | std::string name_storage; 30 | bool present; 31 | } destination_ag; 32 | 33 | const char* skillname; // Skill names are guaranteed to be valid for the lifetime of the process so copying pointer is fine 34 | uint64_t id; 35 | uint64_t revision; 36 | }; 37 | public: 38 | EventSequencer(const CombatCallbackSignature pCallback); 39 | 40 | uintptr_t ProcessEvent(cbtevent* pEvent, ag* pSourceAgent, ag* pDestinationAgent, const char* pSkillname, uint64_t pId, uint64_t pRevision); 41 | bool QueueIsEmpty(); 42 | 43 | private: 44 | void TryFlushEvents(); 45 | 46 | const CombatCallbackSignature mCallback = nullptr; 47 | 48 | std::shared_mutex mLock; 49 | std::atomic_uint64_t mHighestId = UINT64_MAX; 50 | std::atomic_uint32_t mQueuedEventCount = 0; 51 | std::atomic_uint32_t mHighestQueueSize = 0; 52 | Event mQueuedEvents[MAX_QUEUED_EVENTS]; 53 | }; -------------------------------------------------------------------------------- /src/Exports.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "EventProcessor.h" 4 | #include "EventSequencer.h" 5 | #include "UpdateGUI.h" 6 | #include "../networking/Client.h" 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | typedef void (*E3Signature)(const char* pString); 15 | typedef void (*E5Signature)(ImVec4** pColors); 16 | typedef uint64_t(*E7Signature)(); 17 | typedef void (*E9Signature)(cbtevent* pEvent, uint32_t pSignature); 18 | 19 | class GlobalObjects 20 | { 21 | public: 22 | static inline bool IS_UNIT_TEST = false; 23 | 24 | static inline HMODULE SELF_HANDLE = NULL; 25 | static inline E3Signature ARC_E3 = nullptr; 26 | static inline E5Signature ARC_E5 = nullptr; 27 | static inline E7Signature ARC_E7 = nullptr; 28 | static inline E9Signature ARC_E9 = nullptr; 29 | static inline E9Signature ARC_E10 = nullptr; 30 | static inline std::unique_ptr EVENT_SEQUENCER = nullptr; 31 | static inline std::unique_ptr EVENT_PROCESSOR = nullptr; 32 | static inline std::unique_ptr EVTC_RPC_CLIENT = nullptr; 33 | static inline std::unique_ptr EVTC_RPC_CLIENT_THREAD = nullptr; 34 | 35 | static inline UpdateChecker::Version VERSION = {}; 36 | static inline char VERSION_STRING_FRIENDLY[128] = {}; 37 | static inline std::unique_ptr UPDATE_CHECKER = nullptr; 38 | static inline std::unique_ptr UPDATE_STATE = nullptr; 39 | 40 | static inline std::string ROOT_CERTIFICATES = ""; 41 | 42 | static inline ImVec4* COLORS[5] = {}; 43 | 44 | static inline std::shared_mutex SHUTDOWN_LOCK; 45 | static inline bool IS_SHUTDOWN = true; 46 | }; 47 | 48 | typedef void* (*MallocSignature)(size_t); 49 | typedef void (*FreeSignature)(void*); 50 | extern "C" __declspec(dllexport) ModInitSignature get_init_addr(const char* pArcdpsVersionString, void* pImguiContext, void* pID3DPtr, HMODULE pArcModule, MallocSignature pArcdpsMalloc, FreeSignature pArcdpsFree, uint32_t pD3DVersion); 51 | extern "C" __declspec(dllexport) ModReleaseSignature get_release_addr(); -------------------------------------------------------------------------------- /src/GUI.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Options.h" 3 | #include "UpdateGUI.h" 4 | 5 | #include 6 | 7 | void SetContext(void* pImGuiContext); 8 | void FindAndResolveCyclicDependencies(HealTableOptions& pHealingOptions, size_t pStartIndex); 9 | void LoadIcons(HMODULE pArcModule, void* pID3DPtr, uint32_t pD3DVersion); 10 | 11 | void Display_GUI(HealTableOptions& pHealingOptions); 12 | void Display_AddonOptions(HealTableOptions& pHealingOptions); 13 | void Display_PostNewFrame(ImGuiContext* pImguiContext, HealTableOptions& pHealingOptions); 14 | void Display_PreEndFrame(ImGuiContext* pImguiContext, HealTableOptions& pHealingOptions); 15 | void ImGui_ProcessKeyEvent(HWND pWindowHandle, UINT pMessage, WPARAM pAdditionalW, LPARAM pAdditionalL); -------------------------------------------------------------------------------- /src/ImGuiEx.cpp: -------------------------------------------------------------------------------- 1 | #include "ImGuiEx.h" 2 | 3 | float ImGuiEx::CalcWindowHeight(size_t pLineCount, float pExtraHeight, ImGuiWindow* pWindow) 4 | { 5 | if (pWindow == nullptr) 6 | { 7 | pWindow = ImGui::GetCurrentWindowRead(); 8 | } 9 | 10 | //return pWindow->TitleBarHeight() + ImGui::GetStyle().WindowPadding.y * 2 + pLineCount * ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.y; 11 | 12 | float decorationsSize = pWindow->TitleBarHeight() + pWindow->MenuBarHeight(); 13 | float padding = pWindow->WindowPadding.y * 2.0f; 14 | float contentSize = 0; 15 | if (pLineCount > 0) 16 | { 17 | contentSize = pLineCount * ImGui::GetTextLineHeight() + (pLineCount - 1) * ImGui::GetStyle().ItemSpacing.y; 18 | } 19 | return decorationsSize + padding + contentSize + pExtraHeight; 20 | } 21 | 22 | bool ImGuiEx::SmallCheckBox(const char* pLabel, bool* pIsPressed) 23 | { 24 | RemoveFramePadding pad; 25 | bool result = ImGui::Checkbox(pLabel, pIsPressed); 26 | return result; 27 | } 28 | 29 | bool ImGuiEx::SmallInputFloat(const char* pLabel, float* pFloat) 30 | { 31 | RemoveFramePadding pad; 32 | bool result = ImGui::InputFloat(pLabel, pFloat, 0.0f, 0.0f, "%.1f"); 33 | return result; 34 | } 35 | 36 | bool ImGuiEx::SmallInputText(const char* pLabel, char* pBuffer, size_t pBufferSize) 37 | { 38 | RemoveFramePadding pad; 39 | bool result = ImGui::InputText(pLabel, pBuffer, pBufferSize); 40 | return result; 41 | } 42 | 43 | bool ImGuiEx::SmallRadioButton(const char* pLabel, bool pIsPressed) 44 | { 45 | RemoveFramePadding pad; 46 | bool result = ImGui::RadioButton(pLabel, pIsPressed); 47 | return result; 48 | } 49 | 50 | void ImGuiEx::SmallIndent() 51 | { 52 | ImGui::Indent(ImGui::GetCurrentContext()->FontSize); 53 | } 54 | 55 | void ImGuiEx::SmallUnindent() 56 | { 57 | ImGui::Unindent(ImGui::GetCurrentContext()->FontSize); 58 | } 59 | 60 | float ImGuiEx::StatsEntry(std::string_view pLeftText, std::string_view pRightText, std::optional pFillRatio, std::optional pBarrierGenerationRatio, std::optional pIndexNumber, std::optional pProfessionText, void* pProfessionIcon, std::optional pLeftTextColour, std::optional pHealColour, std::optional pBarrierGenerationColour, bool pSelf) 61 | { 62 | ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(204, 204, 212, 255)); 63 | ImGui::BeginGroup(); 64 | 65 | //ImGui::PushID(pUniqueId); 66 | //float startX = ImGui::GetCursorPosX(); 67 | //ImGui::Selectable("", false, ImGuiSelectableFlags_SpanAllColumns); 68 | //ImGui::PopID(); 69 | 70 | ImVec2 leftTextSize = ImGui::CalcTextSize(pLeftText.data(), pLeftText.data() + pLeftText.size()); 71 | ImVec2 rightTextSize = ImGui::CalcTextSize(pRightText.data(), pRightText.data() + pRightText.size()); 72 | ImVec2 indexNumberSize = ImVec2(); 73 | ImVec2 professionTextSize = ImVec2(); 74 | ImVec2 professionIconSize = ImVec2(); 75 | 76 | if (pFillRatio.has_value() == true) 77 | { 78 | ImVec2 pos = ImGui::GetCursorScreenPos(); 79 | 80 | float healingRatio = *pFillRatio; 81 | 82 | ImU32 healingColor = pHealColour.has_value() ? ImGui::ColorConvertFloat4ToU32(*pHealColour) : IM_COL32(102, 178, 102, 128); 83 | ImU32 barrierColor = pBarrierGenerationColour.has_value() ? ImGui::ColorConvertFloat4ToU32(*pBarrierGenerationColour) : IM_COL32(255, 225, 0, 128); 84 | 85 | if (pBarrierGenerationRatio.has_value() == true) 86 | { 87 | float barrierGenerationRatio = *pBarrierGenerationRatio; 88 | healingRatio -= barrierGenerationRatio; 89 | 90 | ImVec2 barrierStart = ImVec2(pos.x + ImGui::GetContentRegionAvailWidth() * healingRatio, pos.y); 91 | ImVec2 barrierEnd = ImVec2(pos.x + ImGui::GetContentRegionAvailWidth() * (healingRatio + barrierGenerationRatio), pos.y + ImGui::GetTextLineHeight()); 92 | 93 | ImGui::GetWindowDrawList()->AddRectFilled(barrierStart, barrierEnd, barrierColor); 94 | } 95 | 96 | ImVec2 healingStart = pos; 97 | ImVec2 healingEnd = ImVec2(pos.x + ImGui::GetContentRegionAvailWidth() * healingRatio, pos.y + ImGui::GetTextLineHeight()); 98 | 99 | ImGui::GetWindowDrawList()->AddRectFilled(healingStart, healingEnd, healingColor); 100 | } 101 | 102 | // Add ItemInnerSpacing even if no box is being drawn, that way it looks consistent with and without progress bars 103 | ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetStyle().ItemInnerSpacing.x); 104 | if (pIndexNumber.has_value() == true) 105 | { 106 | indexNumberSize = ImGui::CalcTextSize(pIndexNumber->data(), pIndexNumber->data() + pIndexNumber->size()) + ImGui::GetStyle().ItemSpacing; 107 | 108 | TextColoredUnformatted(std::optional(IM_COL32(255, 255, 97, 255)), *pIndexNumber); 109 | ImGui::SameLine(); 110 | } 111 | 112 | /* pProfessionIcon can be nullptr when: 113 | * 1 - "profession icons" option is disabled in Display settings 114 | * 2 - There is no icon for the profession and elite specialization pair 115 | * 3 - IconLoader has not finished loading the icon yet 116 | */ 117 | if (pProfessionIcon != nullptr) 118 | { 119 | professionTextSize = ImVec2(ImGui::GetFontSize(), ImGui::GetFontSize()) + ImGui::GetStyle().ItemSpacing; 120 | ImGui::Image(pProfessionIcon, ImVec2(ImGui::GetFontSize(), ImGui::GetFontSize())); 121 | ImGui::SameLine(); 122 | } 123 | 124 | if (pProfessionText.has_value() == true) 125 | { 126 | professionTextSize = ImGui::CalcTextSize(pProfessionText->data(), pProfessionText->data() + pProfessionText->size()) + ImGui::GetStyle().ItemSpacing; 127 | ImGui::TextUnformatted(pProfessionText->data(), pProfessionText->data() + pProfessionText->size()); 128 | ImGui::SameLine(); 129 | } 130 | 131 | auto leftTextColour = pLeftTextColour.has_value() ? ImGui::ColorConvertFloat4ToU32(*pLeftTextColour) : pSelf ? std::optional(IM_COL32(255, 255, 97, 255)) : std::nullopt; 132 | TextColoredUnformatted(leftTextColour, pLeftText); 133 | 134 | ImGui::SameLine(); 135 | ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - rightTextSize.x - ImGui::GetStyle().ItemInnerSpacing.x); // Sending x in SameLine messes with alignment when inside of a group 136 | ImGui::TextUnformatted(pRightText.data(), pRightText.data() + pRightText.size()); 137 | 138 | ImGui::EndGroup(); 139 | ImGui::PopStyleColor(); 140 | 141 | // one window padding and inner spacing on each edge of the window, two item spacings between left and right text. The other item spacings are accounted for into their respective size calculations already. 142 | return indexNumberSize.x + leftTextSize.x + rightTextSize.x + professionTextSize.x + professionIconSize.x + ImGui::GetStyle().ItemSpacing.x * 2.0f + ImGui::GetStyle().ItemInnerSpacing.x * 2.0f + ImGui::GetCurrentWindowRead()->WindowPadding.x * 2.0f; 143 | } 144 | 145 | void ImGuiEx::TextColoredUnformatted(std::optional pColor, std::string_view pText) 146 | { 147 | if (pColor.has_value()) 148 | { 149 | ImGui::PushStyleColor(ImGuiCol_Text, *pColor); 150 | ImGui::TextUnformatted(pText.data(), pText.data() + pText.size()); 151 | ImGui::PopStyleColor(); 152 | } 153 | else 154 | { 155 | ImGui::TextUnformatted(pText.data(), pText.data() + pText.size()); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/ImGuiEx.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Utilities.h" 3 | 4 | #define IMGUI_DEFINE_MATH_OPERATORS 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #ifdef __clang__ 13 | #pragma clang diagnostic push 14 | #pragma clang diagnostic ignored "-Wformat-security" 15 | #endif 16 | // Helpers for ImGui functions 17 | namespace ImGuiEx 18 | { 19 | class RemoveFramePadding 20 | { 21 | public: 22 | RemoveFramePadding() 23 | { 24 | ImVec2 padding = ImGui::GetStyle().FramePadding; 25 | padding.y = 1; 26 | ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, padding); 27 | } 28 | 29 | ~RemoveFramePadding() 30 | { 31 | ImGui::PopStyleVar(); 32 | } 33 | }; 34 | 35 | class ScopedUninteractable 36 | { 37 | public: 38 | ScopedUninteractable(bool pIf) 39 | : mEnabled(pIf) 40 | { 41 | if (mEnabled == true) 42 | { 43 | ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); 44 | ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(128, 128, 128, 255)); 45 | } 46 | } 47 | 48 | ~ScopedUninteractable() 49 | { 50 | if (mEnabled == true) 51 | { 52 | ImGui::PopItemFlag(); 53 | ImGui::PopStyleColor(); 54 | } 55 | } 56 | 57 | private: 58 | const bool mEnabled; 59 | }; 60 | 61 | 62 | float CalcWindowHeight(size_t pLineCount, float pExtraHeight, ImGuiWindow* pWindow = nullptr); 63 | 64 | bool SmallCheckBox(const char* pLabel, bool* pIsPressed); 65 | bool SmallInputFloat(const char* pLabel, float* pFloat); 66 | bool SmallInputText(const char* pLabel, char* pBuffer, size_t pBufferSize); 67 | bool SmallRadioButton(const char* pLabel, bool pIsPressed); 68 | 69 | void SmallIndent(); 70 | void SmallUnindent(); 71 | 72 | // returns minimum size needed to display the entry 73 | float StatsEntry(std::string_view pLeftText, std::string_view pRightText, std::optional pFillRatio, std::optional pBarrierGenerationRatio, std::optional pIndexNumber, std::optional pProfessionText, void* pProfessionIcon, std::optional pLeftTextColour, std::optional pHealColour, std::optional pBarrierGenerationColour, bool pSelf); 74 | 75 | template>> 76 | bool SmallInputInt(const char* pLabel, T* pInt) 77 | { 78 | ImGuiDataType dataType; 79 | if constexpr (std::is_same_v) 80 | { 81 | dataType = ImGuiDataType_S32; 82 | } 83 | else if constexpr (std::is_same_v) 84 | { 85 | dataType = ImGuiDataType_U64; 86 | } 87 | else if constexpr (std::is_same_v) 88 | { 89 | dataType = ImGuiDataType_S64; 90 | } 91 | else 92 | { 93 | assert(false); 94 | //static_assert(false, "unhandled type"); 95 | } 96 | 97 | RemoveFramePadding pad; 98 | return ImGui::InputScalar(pLabel, dataType, pInt); 99 | } 100 | 101 | template 102 | float TextRightAlignedSameLine(const char* pFormatString, Args... pArgs) 103 | { 104 | char buffer[1024]; 105 | 106 | snprintf(buffer, sizeof(buffer), pFormatString, pArgs...); 107 | float textSize = ImGui::CalcTextSize(buffer).x; 108 | 109 | ImGui::SameLine(); 110 | ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - textSize); // Sending x in SameLine messes with alignment when inside of a group 111 | ImGui::Text("%s", buffer); 112 | 113 | return textSize; 114 | } 115 | 116 | template 117 | float DetailsSummaryEntry(const char* pCategoryName, const char* pFormatString, Args... pArgs) 118 | { 119 | float leftTextSize = ImGui::CalcTextSize(pCategoryName).x; 120 | ImGui::Text(pCategoryName); 121 | 122 | float rightTextSize = TextRightAlignedSameLine(pFormatString, pArgs...); 123 | // Two item spacings between the left and right text, and WindowPadding on each side of the left and right text. 124 | return leftTextSize + ImGui::GetStyle().ItemSpacing.x * 2.0f + ImGui::GetCurrentWindowRead()->WindowPadding.x * 2.0f + rightTextSize; 125 | } 126 | 127 | template 128 | void TextColoredCentered(ImColor pColor, const char* pFormatString, Args... pArgs) 129 | { 130 | char buffer[1024]; 131 | 132 | snprintf(buffer, sizeof(buffer), pFormatString, pArgs...); 133 | ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x * 0.5f - ImGui::CalcTextSize(buffer).x * 0.5f); 134 | ImGui::TextColored(pColor, "%s", buffer); 135 | } 136 | 137 | void TextColoredUnformatted(std::optional pColor, std::string_view pText); 138 | 139 | template 140 | void BottomText(const char* pFormatString, Args... pArgs) 141 | { 142 | char buffer[1024]; 143 | 144 | snprintf(buffer, sizeof(buffer), pFormatString, pArgs...); 145 | ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetContentRegionAvail().y - ImGui::CalcTextSize(buffer).y); 146 | ImGui::Text("%s", buffer); 147 | } 148 | 149 | template 150 | void AddTooltipToLastItem(const char* pFormatString, Args... pArgs) 151 | { 152 | if (ImGui::IsItemHovered() == true) 153 | { 154 | ImGui::SetTooltip(pFormatString, pArgs...); 155 | } 156 | } 157 | 158 | template (EnumType::Max)> 159 | bool ComboMenu(const char* pLabel, EnumType& pCurrentItem, const EnumStringArray& pItems) 160 | { 161 | static_assert(std::is_enum_v, "Accidental loss of type safety?"); 162 | 163 | ImVec2 size{0, 0}; 164 | for (const char* item : pItems) 165 | { 166 | ImVec2 itemSize = ImGui::CalcTextSize(item); 167 | size.x = (std::max)(size.x, itemSize.x); 168 | size.y += itemSize.y; 169 | } 170 | size.y += ImGui::GetStyle().ItemSpacing.y * (pItems.size() - 1); 171 | 172 | bool value_changed = false; 173 | ImGui::SetNextWindowContentSize(size); 174 | if (ImGui::BeginMenu(pLabel) == true) 175 | { 176 | for (size_t i = 0; i < pItems.size(); i++) 177 | { 178 | ImGui::PushID(static_cast(i)); 179 | 180 | bool selected = (pCurrentItem == static_cast(i)); 181 | if (ImGui::Selectable(pItems[i], selected, ImGuiSelectableFlags_DontClosePopups) == true) 182 | { 183 | value_changed = true; 184 | pCurrentItem = static_cast(i); 185 | } 186 | 187 | ImGui::PopID(); 188 | } 189 | ImGui::EndMenu(); 190 | } 191 | 192 | return value_changed; 193 | } 194 | 195 | template (EnumType::Max)> 196 | void SmallEnumRadioButton(const char* pInvisibleLabel, EnumType& pCurrentItem, const EnumStringArray& pItems) 197 | { 198 | ImGui::PushID(pInvisibleLabel); 199 | for (size_t i = 0; i < pItems.size(); i++) 200 | { 201 | ImGui::PushID(static_cast(i)); 202 | 203 | bool selected = (pCurrentItem == static_cast(i)); 204 | if (SmallRadioButton(pItems[i], selected) == true) 205 | { 206 | pCurrentItem = static_cast(i); 207 | } 208 | 209 | ImGui::PopID(); 210 | } 211 | ImGui::PopID(); 212 | } 213 | 214 | template 215 | void SmallEnumCheckBox(const char* pLabel, EnumType* pSavedLocation, EnumType pStyleFlag, bool pCheckBoxIsInverseOfFlag) 216 | { 217 | static_assert(std::is_enum::value == true, "Accidental loss of type safety?"); 218 | 219 | bool checked = (*pSavedLocation & pStyleFlag) == pStyleFlag; 220 | if (pCheckBoxIsInverseOfFlag == true) 221 | { 222 | checked = !checked; 223 | } 224 | 225 | if (ImGuiEx::SmallCheckBox(pLabel, &checked) == true) 226 | { 227 | *pSavedLocation = static_cast(*pSavedLocation ^ pStyleFlag); 228 | } 229 | } 230 | } 231 | 232 | namespace DrawListEx 233 | { 234 | static inline ImVec2 CalcCenteredPosition(const ImVec2& pBoundsPosition, const ImVec2& pBoundsSize, const ImVec2& pItemSize) 235 | { 236 | return ImVec2{ 237 | pBoundsPosition.x + (std::max)((pBoundsSize.x - pItemSize.x) * 0.5f, 0.0f), 238 | pBoundsPosition.y + (std::max)((pBoundsSize.y - pItemSize.y) * 0.5f, 0.0f) 239 | }; 240 | } 241 | } 242 | #ifdef __clang__ 243 | #pragma clang diagnostic pop 244 | #endif -------------------------------------------------------------------------------- /src/Log.cpp: -------------------------------------------------------------------------------- 1 | #include "Log.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #ifdef LINUX 16 | #include 17 | #endif 18 | #ifdef _WIN32 19 | #include 20 | #endif 21 | 22 | #include 23 | #include 24 | 25 | class AbslLogRedirector final : public absl::LogSink 26 | { 27 | public: 28 | AbslLogRedirector() {} 29 | ~AbslLogRedirector() {} 30 | 31 | void Send(const absl::LogEntry& entry) override 32 | { 33 | spdlog::level::level_enum level = spdlog::level::info; 34 | switch (entry.log_severity()) 35 | { 36 | case absl::LogSeverity::kInfo: 37 | level = spdlog::level::info; 38 | break; 39 | case absl::LogSeverity::kWarning: 40 | level = spdlog::level::warn; 41 | break; 42 | case absl::LogSeverity::kError: 43 | level = spdlog::level::err; 44 | break; 45 | case absl::LogSeverity::kFatal: 46 | level = spdlog::level::critical; 47 | break; 48 | }; 49 | 50 | return Log_::LOGGER->log(level, FMT_STRING("ABSL|{}:{}|{}"), 51 | static_cast(entry.source_basename()), entry.source_line(), static_cast(entry.text_message())); 52 | } 53 | }; 54 | 55 | namespace 56 | { 57 | void SetThreadNameLogThread() 58 | { 59 | #ifdef LINUX 60 | pthread_setname_np(pthread_self(), "spdlog-worker"); 61 | #elif defined(_WIN32) 62 | SetThreadDescription(GetCurrentThread(), L"spdlog-worker"); 63 | #endif 64 | } 65 | std::shared_ptr ABSL_LOGGER; 66 | }; 67 | 68 | void Log_::LogImplementation_(const char* pComponentName, const char* pFunctionName, const char* pFormatString, ...) 69 | { 70 | char buffer[1024]; 71 | 72 | va_list args; 73 | va_start(args, pFormatString); 74 | vsnprintf(buffer, sizeof(buffer), pFormatString, args); 75 | va_end(args); 76 | 77 | Log_::LOGGER->debug("{}|{}|{}", pComponentName, pFunctionName, buffer); 78 | } 79 | 80 | void Log_::FlushLogFile() 81 | { 82 | Log_::LOGGER->flush(); 83 | } 84 | 85 | void Log_::Init(bool pRotateOnOpen, const char* pLogPath) 86 | { 87 | if (Log_::LOGGER != nullptr) 88 | { 89 | LogW("Skipping logger initialization since logger is not nullptr"); 90 | return; 91 | } 92 | 93 | Log_::LOGGER = spdlog::rotating_logger_mt("arcdps_healing_stats", pLogPath, 128*1024*1024, 8, pRotateOnOpen); 94 | Log_::LOGGER->set_pattern("%b %d %H:%M:%S.%f %t %L %v"); 95 | Log_::LOGGER->flush_on(spdlog::level::err); 96 | spdlog::flush_every(std::chrono::seconds(5)); 97 | 98 | ABSL_LOGGER = std::make_shared(); 99 | absl::AddLogSink(ABSL_LOGGER.get()); 100 | } 101 | 102 | // SetLevel can still be used after calling this, the sink levels and logger levels are different things - e.g if logger 103 | // level is debug and sink level is trace then trace lines will not be shown 104 | void Log_::InitMultiSink(bool pRotateOnOpen, const char* pLogPathTrace, const char* pLogPathInfo) 105 | { 106 | if (Log_::LOGGER != nullptr) 107 | { 108 | LogW("Skipping logger initialization since logger is not nullptr"); 109 | return; 110 | } 111 | 112 | spdlog::init_thread_pool(8192, 1, &SetThreadNameLogThread); 113 | 114 | auto debug_sink = std::make_shared(pLogPathTrace, 128*1024*1024, 8, pRotateOnOpen); 115 | debug_sink->set_level(spdlog::level::trace); 116 | auto info_sink = std::make_shared(pLogPathInfo, 128*1024*1024, 8, pRotateOnOpen); 117 | info_sink->set_level(spdlog::level::info); 118 | 119 | std::vector sinks{debug_sink, info_sink}; 120 | Log_::LOGGER = std::make_shared("arcdps_healing_stats", sinks.begin(), sinks.end(), spdlog::thread_pool(), spdlog::async_overflow_policy::block); 121 | Log_::LOGGER->set_pattern("%b %d %H:%M:%S.%f %t %L %v"); 122 | Log_::LOGGER->flush_on(spdlog::level::err); 123 | spdlog::register_logger(Log_::LOGGER); 124 | 125 | spdlog::flush_every(std::chrono::seconds(5)); 126 | 127 | ABSL_LOGGER = std::make_shared(); 128 | absl::AddLogSink(ABSL_LOGGER.get()); 129 | } 130 | 131 | void Log_::Shutdown() 132 | { 133 | Log_::LOGGER = nullptr; 134 | spdlog::shutdown(); 135 | } 136 | 137 | static std::atomic_bool LoggerLocked = false; 138 | void Log_::SetLevel(spdlog::level::level_enum pLevel) 139 | { 140 | if (pLevel < 0 || pLevel >= spdlog::level::n_levels) 141 | { 142 | LogW("Not setting level to {} since level is out of range", static_cast(pLevel)); 143 | return; 144 | } 145 | if (LoggerLocked == true) 146 | { 147 | LogW("Not setting level to {} since logger is locked", static_cast(pLevel)); 148 | return; 149 | } 150 | 151 | Log_::LOGGER->set_level(pLevel); 152 | LogI("Changed level to {}", static_cast(pLevel)); 153 | } 154 | 155 | void Log_::LockLogger() 156 | { 157 | LoggerLocked = true; 158 | LogI("Locked logger"); 159 | } 160 | -------------------------------------------------------------------------------- /src/Log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #define SPDLOG_COMPILED_LIB 3 | #define SPDLOG_FMT_EXTERNAL 4 | 5 | #pragma warning(push, 0) 6 | #pragma warning(disable : 4189) 7 | #pragma warning(disable : 6285) 8 | #include 9 | #include 10 | #pragma warning(pop) 11 | 12 | #include 13 | 14 | struct SimpleFormatter 15 | { 16 | constexpr auto parse(fmt::format_parse_context& pContext) -> decltype(pContext.begin()) 17 | { 18 | // [ctx.begin(), ctx.end()) is a character range that contains a part of 19 | // the format string starting from the format specifications to be parsed, 20 | // e.g. in 21 | // 22 | // fmt::format("{:f} - point of interest", point{1, 2}); 23 | // 24 | // the range will contain "f} - point of interest". The formatter should 25 | // parse specifiers until '}' or the end of the range. In this example 26 | // the formatter should parse the 'f' specifier and return an iterator 27 | // pointing to '}'. 28 | 29 | // Parse the presentation format and store it in the formatter: 30 | auto it = pContext.begin(), end = pContext.end(); 31 | 32 | // Check if reached the end of the range: 33 | if (it != end && *it != '}') 34 | { 35 | throw fmt::format_error("invalid format"); 36 | } 37 | 38 | // Return an iterator past the end of the parsed range: 39 | return it; 40 | } 41 | }; 42 | 43 | namespace Log_ 44 | { 45 | static constexpr const char* StripPath(const char* pPath) 46 | { 47 | const char* lastname = pPath; 48 | 49 | for (const char* p = pPath; *p != '\0'; p++) 50 | { 51 | if ((*p == '/' || *p == '\\') && (*(p + 1) != '\0')) 52 | { 53 | lastname = p + 1; 54 | } 55 | } 56 | 57 | return lastname; 58 | } 59 | 60 | static constexpr const char* FindEnd(const char* pPath) 61 | { 62 | const char* lastpoint = pPath; 63 | 64 | while (*lastpoint != '\0') 65 | { 66 | lastpoint++; 67 | if (*lastpoint == '.') 68 | { 69 | break; 70 | } 71 | } 72 | 73 | return lastpoint; 74 | } 75 | 76 | struct FileNameStruct 77 | { 78 | char name[1024]; 79 | 80 | constexpr operator char* () 81 | { 82 | return name; 83 | } 84 | }; 85 | 86 | static constexpr FileNameStruct GetFileName(const char* pFilePath) 87 | { 88 | FileNameStruct fileName = {}; 89 | const char* strippedPath = StripPath(pFilePath); 90 | const char* pointPosition = FindEnd(strippedPath); 91 | 92 | for (unsigned int i = 0; i < pointPosition - strippedPath; i++) 93 | { 94 | *(fileName + i) = *(strippedPath + i); 95 | } 96 | 97 | fileName[pointPosition - strippedPath] = '\0'; 98 | return fileName; 99 | } 100 | 101 | void LogImplementation_(const char* pComponentName, const char* pFunctionName, const char* pFormatString, ...); 102 | 103 | void FlushLogFile(); 104 | void Init(bool pRotateOnOpen, const char* pLogPath); 105 | void InitMultiSink(bool pRotateOnOpen, const char* pLogPathTrace, const char* pLogPathInfo); 106 | void Shutdown(); 107 | void SetLevel(spdlog::level::level_enum pLevel); 108 | void LockLogger(); 109 | 110 | inline std::shared_ptr LOGGER; 111 | } 112 | 113 | #define LogT(pFormatString, ...) Log_::LOGGER->trace(FMT_STRING("{}|{}|" pFormatString), Log_::GetFileName(__FILE__).name, __func__, ##__VA_ARGS__) 114 | #define LogD(pFormatString, ...) Log_::LOGGER->debug(FMT_STRING("{}|{}|" pFormatString), Log_::GetFileName(__FILE__).name, __func__, ##__VA_ARGS__) 115 | #define LogI(pFormatString, ...) Log_::LOGGER->info(FMT_STRING("{}|{}|" pFormatString), Log_::GetFileName(__FILE__).name, __func__, ##__VA_ARGS__) 116 | #define LogW(pFormatString, ...) Log_::LOGGER->warn(FMT_STRING("{}|{}|" pFormatString), Log_::GetFileName(__FILE__).name, __func__, ##__VA_ARGS__) 117 | #define LogE(pFormatString, ...) Log_::LOGGER->error(FMT_STRING("{}|{}|" pFormatString), Log_::GetFileName(__FILE__).name, __func__, ##__VA_ARGS__) 118 | #define LogC(pFormatString, ...) Log_::LOGGER->critical(FMT_STRING("{}|{}|" pFormatString), Log_::GetFileName(__FILE__).name, __func__, ##__VA_ARGS__) 119 | 120 | #define LOG(pFormatString, ...) Log_::LogImplementation_(Log_::GetFileName(__FILE__), __func__, pFormatString, ##__VA_ARGS__); if (false) { printf(pFormatString, ##__VA_ARGS__); } 121 | 122 | #define DEBUGLOG(pFormatString, ...) if (false) { printf(pFormatString, ##__VA_ARGS__); } 123 | 124 | #define BOOL_STR(pBool) pBool == true ? "true" : "false" 125 | -------------------------------------------------------------------------------- /src/Options.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "AggregatedStatsCollection.h" 4 | #include "Log.h" 5 | #include "State.h" 6 | 7 | #pragma warning(push, 0) 8 | #pragma warning(disable : 26495) 9 | #pragma warning(disable : 26819) 10 | #pragma warning(disable : 28183) 11 | #include 12 | #pragma warning(pop) 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | enum class AutoUpdateSettingEnum : uint8_t 24 | { 25 | Off = 0, 26 | On = 1, 27 | PreReleases = 2, 28 | Max = 3 29 | }; 30 | 31 | struct DetailsWindowState : AggregatedStatsEntry 32 | { 33 | bool IsOpen = false; 34 | 35 | float LastFrameLeftSideMinWidth = 0.0f; // In-Memory only 36 | float LastFrameRightSideMinWidth = 0.0f; // In-Memory only 37 | 38 | explicit DetailsWindowState(const AggregatedStatsEntry& pEntry); 39 | }; 40 | 41 | struct HealWindowContext : HealWindowOptions 42 | { 43 | std::unique_ptr CurrentAggregatedStats; // In-Memory only 44 | time_t LastAggregatedTime = 0; // In-Memory only 45 | uintptr_t SelfUniqueId = 0; // In-Memory only 46 | 47 | std::vector OpenSkillWindows; // In-Memory only 48 | std::vector OpenAgentWindows; // In-Memory only 49 | std::vector OpenPeersOutgoingWindows; // In-Memory only 50 | 51 | ImGuiID WindowId = 0; // In-Memory only 52 | 53 | float LastFrameMinWidth = 0.0f; // In-Memory only 54 | size_t CurrentFrameLineCount = 0; // In-Memory only 55 | float CurrentFrameExtraHeight = 0.0f; // In-Memory only 56 | }; 57 | 58 | struct HealTableOptions 59 | { 60 | AutoUpdateSettingEnum AutoUpdateSetting = AutoUpdateSettingEnum::On; 61 | bool DebugMode = false; 62 | bool GrpcDnsResolverCAres = false; 63 | spdlog::level::level_enum LogLevel = spdlog::level::off; 64 | 65 | bool EvtcLoggingEnabled = true; 66 | 67 | char EvtcRpcEndpoint[128] = "evtc-rpc.kappa322.com"; 68 | bool EvtcRpcEnabled = false; 69 | bool EvtcRpcBudgetMode = false; 70 | bool EvtcRpcDisableEncryption = false; 71 | int EvtcRpcEnabledHotkey = 0; 72 | 73 | std::array Windows; 74 | 75 | std::vector AnchoringHighlightedWindows; // In-Memory only 76 | 77 | HealTableOptions(); 78 | ~HealTableOptions() = default; 79 | 80 | void Load(const char* pConfigPath); 81 | bool Save(const char* pConfigPath) const; 82 | 83 | void FromJson(const nlohmann::json& pJsonObject); 84 | void ToJson(nlohmann::json& pJsonObject) const; 85 | 86 | void Reset(); 87 | }; 88 | static_assert(std::is_same::type, int>::value == true, "HealTableOptions::LogLevel size changed"); 89 | 90 | template<> 91 | struct fmt::formatter : SimpleFormatter 92 | { 93 | // Formats the point p using the parsed format specification (presentation) 94 | // stored in this formatter. 95 | template 96 | auto format(const HealTableOptions& pObject, FormatContext& pContext) const 97 | { 98 | nlohmann::json jsonObject; 99 | pObject.ToJson(jsonObject); 100 | 101 | return fmt::format_to( 102 | pContext.out(), 103 | "{}", 104 | jsonObject.dump()); 105 | } 106 | }; 107 | -------------------------------------------------------------------------------- /src/PlayerStats.cpp: -------------------------------------------------------------------------------- 1 | #include "PlayerStats.h" 2 | 3 | #include "Log.h" 4 | #include "Utilities.h" 5 | 6 | #include 7 | #include 8 | 9 | HealEvent::HealEvent(uint64_t pTime, uint64_t pSize, uintptr_t pAgentId, uint32_t pSkillId, bool pIsBarrierGeneration) 10 | : Time{pTime} 11 | , Size{pSize} 12 | , AgentId{pAgentId} 13 | , SkillId{pSkillId} 14 | , IsBarrierGeneration{pIsBarrierGeneration} 15 | { 16 | } 17 | 18 | bool HealEvent::operator==(const HealEvent& pRight) const 19 | { 20 | return std::tie(Time, Size, AgentId, SkillId, IsBarrierGeneration) == std::tie(pRight.Time, pRight.Size, pRight.AgentId, pRight.SkillId, pRight.IsBarrierGeneration); 21 | } 22 | 23 | bool HealEvent::operator!=(const HealEvent& pRight) const 24 | { 25 | return (*this == pRight) == false; 26 | } 27 | 28 | bool HealingStatsSlim::IsOutOfCombat() 29 | { 30 | return EnteredCombatTime == 0 || ExitedCombatTime != 0; 31 | } 32 | 33 | void PlayerStats::EnteredCombat(uint64_t pTime, uint16_t pSubGroup) 34 | { 35 | std::lock_guard lock(myLock); 36 | 37 | if (myStats.IsOutOfCombat() == true) 38 | { 39 | myStats.EnteredCombatTime = pTime; 40 | myStats.ExitedCombatTime = 0; 41 | myStats.LastDamageEvent = 0; 42 | 43 | myStats.Events.clear(); 44 | myStats.SubGroup = pSubGroup; 45 | 46 | LOG("Entered combat, time is %llu, subgroup is %hu", pTime, pSubGroup); 47 | } 48 | else 49 | { 50 | LOG("Tried to enter combat when already in combat (old time %llu, current time %llu)", myStats.EnteredCombatTime, pTime); 51 | } 52 | } 53 | 54 | uint64_t PlayerStats::ExitedCombat(uint64_t pTime, uint64_t pLastDamageEventTime) 55 | { 56 | std::lock_guard lock(myLock); 57 | 58 | if (myStats.IsOutOfCombat() == true) 59 | { 60 | LogD("Tried to exit combat when not in combat (current time {})", pTime); 61 | return 0; 62 | } 63 | 64 | if (pTime <= myStats.EnteredCombatTime) 65 | { 66 | LogD("Tried to exit combat with timestamp earlier than combat start (current time {}, combat start {})", pTime, myStats.EnteredCombatTime); 67 | return 0; 68 | } 69 | 70 | myStats.ExitedCombatTime = pTime; 71 | myStats.LastDamageEvent = std::max(myStats.LastDamageEvent, pLastDamageEventTime); 72 | 73 | LogI("EnteredCombatTime={} ExitedCombatTime={} LastDamageEvent={} EventCount={} pLastDamageEventTime={}", 74 | myStats.EnteredCombatTime, myStats.ExitedCombatTime, myStats.LastDamageEvent, myStats.Events.size(), pLastDamageEventTime); 75 | 76 | return myStats.LastDamageEvent; 77 | } 78 | 79 | bool PlayerStats::ResetIfNotInCombat() 80 | { 81 | std::lock_guard lock(myLock); 82 | 83 | if (myStats.IsOutOfCombat() == true) 84 | { 85 | // Reset everything except the subgroup 86 | myStats.EnteredCombatTime = 0; 87 | myStats.ExitedCombatTime = 0; 88 | myStats.LastDamageEvent = 0; 89 | myStats.Events.clear(); 90 | 91 | return true; 92 | } 93 | 94 | return false; 95 | } 96 | 97 | void PlayerStats::DamageEvent(uint64_t pTime) 98 | { 99 | { 100 | std::lock_guard lock(myLock); 101 | 102 | if (myStats.IsOutOfCombat() == true) 103 | { 104 | return; 105 | } 106 | 107 | myStats.LastDamageEvent = std::max(myStats.LastDamageEvent, pTime); 108 | } 109 | } 110 | 111 | void PlayerStats::HealingEvent(cbtevent* pEvent, uintptr_t pDestinationAgentId) 112 | { 113 | uint32_t healedAmount = pEvent->value; 114 | if (healedAmount == 0) 115 | { 116 | healedAmount = pEvent->buff_dmg; 117 | assert(healedAmount != 0); 118 | } 119 | 120 | { 121 | std::lock_guard lock(myLock); 122 | 123 | if (myStats.IsOutOfCombat() == true) 124 | { 125 | LOG("Event before combat enter %llu", pEvent->time); 126 | return; 127 | } 128 | 129 | myStats.Events.emplace_back(pEvent->time, healedAmount, pDestinationAgentId, pEvent->skillid, false); 130 | } 131 | } 132 | 133 | void PlayerStats::BarrierGenerationEvent(cbtevent* pEvent, uintptr_t pDestinationAgentId) 134 | { 135 | uint32_t barrierGenerationAmount = pEvent->value; 136 | if (barrierGenerationAmount == 0) 137 | { 138 | barrierGenerationAmount = pEvent->buff_dmg; 139 | assert(barrierGenerationAmount != 0); 140 | } 141 | 142 | { 143 | std::lock_guard lock(myLock); 144 | 145 | if (myStats.IsOutOfCombat() == true) 146 | { 147 | LOG("Event before combat enter %llu", pEvent->time); 148 | return; 149 | } 150 | 151 | myStats.Events.emplace_back(pEvent->time, barrierGenerationAmount, pDestinationAgentId, pEvent->skillid, true); 152 | } 153 | } 154 | 155 | HealingStatsSlim PlayerStats::GetState() 156 | { 157 | std::lock_guard lock(myLock); 158 | 159 | HealingStatsSlim result{myStats}; 160 | return result; 161 | } 162 | -------------------------------------------------------------------------------- /src/PlayerStats.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | struct HealEvent 13 | { 14 | const uint64_t Time = 0; 15 | const uint64_t Size = 0; 16 | const uintptr_t AgentId = 0; 17 | const uint32_t SkillId = 0; 18 | const bool IsBarrierGeneration = false; 19 | 20 | HealEvent(uint64_t pTime, uint64_t pSize, uintptr_t pAgentId, uint32_t pSkillId, bool pIsBarrierGeneration); 21 | 22 | bool operator==(const HealEvent& pRight) const; 23 | bool operator!=(const HealEvent& pRight) const; 24 | }; 25 | 26 | struct HealingStatsSlim 27 | { 28 | uint64_t EnteredCombatTime = 0; 29 | uint64_t ExitedCombatTime = 0; 30 | uint64_t LastDamageEvent = 0; 31 | uint16_t SubGroup = 0; 32 | 33 | std::vector Events; // Really this should be some segmented vector, will probably write one if it becomes a pain point 34 | 35 | bool IsOutOfCombat(); 36 | }; 37 | 38 | class PlayerStats 39 | { 40 | public: 41 | PlayerStats() = default; 42 | 43 | void EnteredCombat(uint64_t pTime, uint16_t pSubGroup); 44 | 45 | // Returns last damage event time (or 0 if combat wasn't really exited) 46 | uint64_t ExitedCombat(uint64_t pTime, uint64_t pLastDamageEventTime = 0); 47 | bool ResetIfNotInCombat(); // Returns true if peer was reset 48 | 49 | void DamageEvent(uint64_t pTime); 50 | void HealingEvent(cbtevent* pEvent, uintptr_t pDestinationAgentId); 51 | void BarrierGenerationEvent(cbtevent* pEvent, uintptr_t pDestinationAgentId); 52 | 53 | HealingStatsSlim GetState(); 54 | 55 | private: 56 | std::mutex myLock; 57 | 58 | HealingStatsSlim myStats; 59 | }; -------------------------------------------------------------------------------- /src/Skills.cpp: -------------------------------------------------------------------------------- 1 | #include "Skills.h" 2 | 3 | #include "Log.h" 4 | 5 | #include 6 | #include 7 | 8 | SkillTable::SkillTable() 9 | : myDamagingSkills{} 10 | , myHybridSkills{} 11 | { 12 | #define ENTRY(pId) myHybridSkills[pId / 64] |= (1ULL << (pId % 64)) 13 | ENTRY(2654); // Crashing Waves 14 | ENTRY(5510); // Water Trident 15 | ENTRY(5549); // Water Blast (Elementalist) 16 | ENTRY(5570); // Signet of Water 17 | ENTRY(5595); // Water Arrow 18 | ENTRY(9080); // Leap of Faith 19 | ENTRY(9090); // Symbol of Punishment (Writ of Persistence) 20 | ENTRY(9095); // Symbol of Judgement (Writ of Persistence) 21 | ENTRY(9097); // Symbol of Blades (Writ of Persistence) 22 | ENTRY(9108); // Holy Strike 23 | ENTRY(9111); // Symbol of Faith (Writ of Persistence) 24 | ENTRY(9140); // Faithful Strike 25 | ENTRY(9143); // Symbol of Swiftness (Writ of Persistence) 26 | ENTRY(9146); // Symbol of Wrath (Writ of Persistence) 27 | ENTRY(9161); // Symbol of Protection (Writ of Persistence) 28 | ENTRY(9192); // Symbol of Spears (Writ of Persistence) 29 | ENTRY(9208); // Symbol of Light (Writ of Persistence) 30 | ENTRY(9950); // Nourishment (Blueberry Pie AND Slice of Rainbow Cake) 31 | ENTRY(9952); // Nourishment (Strawberry Pie AND Cupcake) 32 | ENTRY(9954); // Nourishment (Cherry Pie) 33 | ENTRY(9955); // Nourishment (Blackberry Pie) 34 | ENTRY(9956); // Nourishment (Mixed Berry Pie) 35 | ENTRY(9957); // Nourishment (Omnomberry Pie AND Slice of Candied Dragon Roll) 36 | ENTRY(10190); // Cry of Frustration (Restorative Illusions) 37 | ENTRY(10191); // Mind Wrack (Restorative Illusions) 38 | ENTRY(10560); // Life Leech 39 | ENTRY(10563); // Life Siphon 40 | ENTRY(10594); // Life Transfer (Transfusion) 41 | ENTRY(10619); // Deadly Feast 42 | ENTRY(10643); // Gathering Plague (Transfusion) 43 | ENTRY(12424); // Blood Frenzy 44 | ENTRY(13684); // Lesser Symbol of Protection (Writ of Persistence) 45 | ENTRY(15259); // Nourishment (Omnomberry Ghost) 46 | ENTRY(21656); // Arcane Brilliance 47 | ENTRY(24800); // Nourishment (Prickly Pear Pie AND Bowl of Cactus Fruit Salad) 48 | ENTRY(26557); // Vengeful Hammers 49 | ENTRY(26646); // Battle Scars 50 | ENTRY(29145); // Mender's Rebuke 51 | ENTRY(29789); // Symbol of Energy (Writ of Persistence) 52 | ENTRY(29856); // Well of Recall (All's Well That Ends Well) 53 | ENTRY(30359); // Gravity Well (All's Well That Ends Well) 54 | ENTRY(30285); // Vampiric Aura 55 | ENTRY(30488); // "Your Soul is Mine!" 56 | ENTRY(30504); // Soul Spiral (Transfusion) 57 | ENTRY(30525); // Well of Calamity (All's Well That Ends Well) 58 | ENTRY(30783); // Wings of Resolve (Soaring Devastation) 59 | ENTRY(30814); // Well of Action (All's Well That Ends Well) 60 | ENTRY(30864); // Tidal Surge 61 | ENTRY(33792); // Slice of Allspice Cake 62 | ENTRY(34207); // Nourishment (Scoop of Mintberry Swirl Ice Cream) 63 | ENTRY(37475); // Nourishment (Winterberry Pie) 64 | ENTRY(40624); // Symbol of Vengeance (Writ of Persistence) 65 | ENTRY(41052); // Sieche 66 | ENTRY(43199); // Breaking Wave 67 | ENTRY(44405); // Riptide 68 | ENTRY(44428); // Garish Pillar (Transfusion) 69 | ENTRY(45026); // Soulcleave's Summit 70 | ENTRY(45983); // Claptosis 71 | ENTRY(51646); // Transmute Frost 72 | ENTRY(51692); // Facet of Nature - Assassin 73 | ENTRY(56928); // Rewinder (Restorative Illusions) 74 | ENTRY(56930); // Split Second (Restorative Illusions) 75 | ENTRY(57117); // Nourishment (Salsa Eggs Benedict) 76 | ENTRY(57239); // Nourishment (Strawberry Cilantro Cheesecake) - Apparently this one has a separate id from the damage event 77 | ENTRY(57244); // Nourishment (Cilantro Lime Sous-Vide Steak) 78 | ENTRY(57253); // Nourishment (Coq Au Vin with Salsa) 79 | ENTRY(57267); // Nourishment (Mango Cilantro Creme Brulee) 80 | ENTRY(57269); // Nourishment (Salsa-Topped Veggie Flatbread) 81 | ENTRY(57295); // Nourishment (Clear Truffle and Cilantro Ravioli) 82 | ENTRY(57341); // Nourishment (Poultry Aspic with Salsa Garnish) 83 | ENTRY(57356); // Nourishment (Spherified Cilantro Oyster Soup) 84 | ENTRY(57401); // Nourishment (Fruit Salad with Cilantro Garnish) 85 | ENTRY(57409); // Nourishment (Cilantro and Cured Meat Flatbread) 86 | ENTRY(62667); // Elixir of Promise 87 | ENTRY(63160); // Eternal Night 88 | ENTRY(63167); // Grasping Shadow 89 | ENTRY(63220); // Dawn's Repose 90 | ENTRY(63249); // Mind Shock 91 | ENTRY(63362); // Haunt Shot 92 | ENTRY(69302); // LifeSiphon 93 | ENTRY(71799); // PathOfGluttony 94 | ENTRY(71800); // Effervescence 95 | ENTRY(71813); // HungeringMaelstrom 96 | ENTRY(71850); // EnervationEcho 97 | ENTRY(71871); // Gorge 98 | ENTRY(71875); // RampartSplitter 99 | ENTRY(71882); // EssenceOfLivingShadows 100 | ENTRY(71892); // FriendlyFire 101 | ENTRY(71897); // Journey 102 | ENTRY(71901); // LineBreakerHeal 103 | ENTRY(71950); // PathToVictory 104 | ENTRY(71970); // FriendlyFireIllu 105 | ENTRY(71986); // EnervationBlade 106 | ENTRY(71999); // Flourish 107 | ENTRY(72002); // ValiantLeap 108 | ENTRY(72005); // InspiringImagery 109 | ENTRY(72028); // FrigidFlurry 110 | ENTRY(72033); // SoothingSplash 111 | ENTRY(72051); // DeathlyEnervation 112 | ENTRY(72062); // EchoingErosion 113 | #undef ENTRY 114 | 115 | std::lock_guard lock(mLock); 116 | 117 | // Fixing names 118 | mSkillNames.emplace(1066, "Revive"); // Pressing "f" on a downed person 119 | mSkillNames.emplace(13594, "Selfless Daring"); // The game maps this name incorrectly to "Selflessness Daring" 120 | mSkillNames.emplace(14024, "Natural Healing"); // The game does not map this one at all 121 | mSkillNames.emplace(26558, "Energy Expulsion"); 122 | mSkillNames.emplace(29863, "Live Vicariously"); // The game maps this name incorrectly to "Vigorous Recovery" 123 | mSkillNames.emplace(30313, "Escapist's Fortitude"); // The game maps this to the wrong skill 124 | 125 | // Clarifying names that exist on more than one skill 126 | mSkillNames.emplace(21750, "Signet of the Ether (Active)"); 127 | mSkillNames.emplace(21775, "Aqua Surge (Self)"); 128 | mSkillNames.emplace(21776, "Aqua Surge (Area)"); 129 | mSkillNames.emplace(26937, "Enchanted Daggers (Initial)"); 130 | mSkillNames.emplace(28313, "Enchanted Daggers (Siphon)"); 131 | mSkillNames.emplace(45686, "Breakrazor's Bastion (Self)"); 132 | mSkillNames.emplace(46232, "Breakrazor's Bastion (Area)"); 133 | mSkillNames.emplace(49103, "Signet of the Ether (Passive)"); 134 | 135 | // Instant cast skills that might otherwise not be mapped in peer stats 136 | // 13594 is on this list as well, but we already override it above 137 | mSkillNames.emplace(40787, "Chapter 1: Desert Bloom"); 138 | mSkillNames.emplace(41714, "Mantra of Solace"); 139 | } 140 | 141 | 142 | void SkillTable::RegisterDamagingSkill(uint32_t pSkillId, const char* pSkillName) 143 | { 144 | UNREFERENCED_PARAMETER(pSkillName); 145 | if (pSkillId > MAX_SKILL_ID) 146 | { 147 | LOG("Too high skill id %u %s!", pSkillId, pSkillName); 148 | assert(false); 149 | return; 150 | } 151 | 152 | uint64_t prevVal = myDamagingSkills[pSkillId / 64].fetch_or(1ULL << (pSkillId % 64), std::memory_order_relaxed); 153 | if ((prevVal & (1ULL << (pSkillId % 64))) == 0) 154 | { 155 | LOG("Registered %u %s as damaging", pSkillId, pSkillName); 156 | } 157 | } 158 | 159 | void SkillTable::RegisterSkillName(uint32_t pSkillId, const char* pSkillName) 160 | { 161 | std::lock_guard lock(mLock); 162 | 163 | auto [iter, inserted] = mSkillNames.try_emplace(pSkillId, pSkillName); 164 | if (inserted == true) 165 | { 166 | LOG("Registered skillname %u %s", pSkillId, pSkillName); 167 | } 168 | } 169 | 170 | bool SkillTable::IsSkillIndirectHealing(uint32_t pSkillId, const char* pSkillName) 171 | { 172 | UNREFERENCED_PARAMETER(pSkillName); 173 | if (pSkillId > MAX_SKILL_ID) 174 | { 175 | LOG("Too high skill id %u %s!", pSkillId, pSkillName); 176 | assert(false); 177 | return false; 178 | } 179 | 180 | // Check if the skill both heals and does damage. If so then treat it as a healing skill (even though other 181 | // traits/skills could've caused it to heal more) 182 | uint64_t val1 = myHybridSkills[pSkillId / 64]; 183 | if ((val1 & (1ULL << (pSkillId % 64))) != 0) 184 | { 185 | return false; 186 | } 187 | 188 | // Otherwise if the skill only does damage normally then the healing is indirectly caused by another skill or trait 189 | uint64_t val2 = myDamagingSkills[pSkillId / 64].load(std::memory_order_relaxed); 190 | if ((val2 & (1ULL << (pSkillId % 64))) != 0) 191 | { 192 | return true; 193 | } 194 | 195 | // If the skill doesn't do any damage, then it's a pure healing skill 196 | return false; 197 | } 198 | 199 | const char* SkillTable::GetSkillName(uint32_t pSkillId) 200 | { 201 | std::lock_guard lock(mLock); 202 | 203 | auto iter = mSkillNames.find(pSkillId); 204 | if (iter != mSkillNames.end()) 205 | { 206 | return iter->second; 207 | } 208 | 209 | return nullptr; 210 | } 211 | 212 | std::map SkillTable::GetState() 213 | { 214 | std::map result; 215 | { 216 | std::lock_guard lock(mLock); 217 | result = mSkillNames; 218 | } 219 | 220 | return result; 221 | } 222 | -------------------------------------------------------------------------------- /src/Skills.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | /* 10 | * The reason these functions exist is to distinguish between direct healing and indirect healing. Some skills and 11 | * traits that do "percentage of damage dealt" based healing, for example Blood Reckoning and Invigorating Precision, 12 | * have their heal attributed to the skill that did the damage and not to the trait itself. We deal with this by 13 | * aggregating damage done by skills which are not normally supposed to heal under a pseudo-skill "Healing by Damage 14 | * Dealt". 15 | * 16 | * This has the edge case of lifestealing skills as well as skills which do both damage and heal, which are registered 17 | * on a per skill basis as "hybrid skills" inside of Skills.cpp. Non-registered skills of this category will 18 | * (incorrectly) show up as "From Damage Dealt" even though there is no such effect active. There is no way to 19 | * distinguish between healing from hybrid skills and indirect healing, and as such the healing statistics for hybrid 20 | * skills can be shown as too high (and "From Damage Dealt" shown as too low) when there are both hybrid skills 21 | * and indirect healing skills present. 22 | * 23 | * RegisterDamagingSkill is called to signify that a skill does damage (called for every damage tick in area combat) 24 | * IsSkillIndirectHealing is called to check if a skill only does damage normally (and its presence is thus caused by 25 | * indirect healing) 26 | */ 27 | class SkillTable 28 | { 29 | public: 30 | SkillTable(); 31 | 32 | void RegisterDamagingSkill(uint32_t pSkillId, const char* pSkillName); 33 | void RegisterSkillName(uint32_t pSkillId, const char* pSkillName); 34 | bool IsSkillIndirectHealing(uint32_t pSkillId, const char* pSkillName); 35 | 36 | // Returns the name for a given skill. The vast majority of skills will use the default skill name provided by 37 | // ArcDPS; a select few skills we override the name for, either because it's not mapped, it's incorrect, or because 38 | // the name is shared with another skill id (for example, Signet of Ether has two components to it - active and 39 | // passive). 40 | const char* GetSkillName(uint32_t pSkillId); 41 | 42 | std::map GetState(); 43 | 44 | private: 45 | std::mutex mLock; 46 | std::map mSkillNames; 47 | 48 | constexpr static uint32_t MAX_SKILL_ID = 131071; 49 | static_assert((MAX_SKILL_ID + 1) % 64 == 0, ""); 50 | 51 | std::atomic myDamagingSkills[(MAX_SKILL_ID + 1) / 64]; 52 | uint64_t myHybridSkills[(MAX_SKILL_ID + 1) / 64]; 53 | }; -------------------------------------------------------------------------------- /src/SpecializationData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | struct SpecializationData 8 | { 9 | std::string Abbreviation = ""; 10 | UINT IconResourceId = 0; 11 | std::string IconName = ""; 12 | size_t IconTextureId = 0; 13 | void* IconTextureData = nullptr; 14 | }; 15 | -------------------------------------------------------------------------------- /src/State.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | constexpr static uint32_t MAX_HEAL_WINDOW_NAME = 31; 11 | constexpr static uint32_t MAX_HEAL_WINDOW_TITLE = 127; 12 | constexpr static uint32_t MAX_HEAL_WINDOW_ENTRY = 127; 13 | constexpr static uint32_t HEAL_WINDOW_COUNT = 10; 14 | 15 | enum class DataSource 16 | { 17 | Agents = 0, 18 | Skills = 1, 19 | Totals = 2, 20 | Combined = 3, 21 | PeersOutgoing = 4, 22 | Max 23 | }; 24 | 25 | enum class SortOrder 26 | { 27 | AscendingAlphabetical = 0, 28 | DescendingAlphabetical = 1, 29 | AscendingTotalOutgoingSize = 2, 30 | DescendingTotalOutgoingSize = 3, 31 | AscendingHealSize = 4, 32 | DescendingHealSize = 5, 33 | AscendingBarrierGenerationSize = 6, 34 | DescendingBarrierGenerationSize = 7, 35 | Max 36 | }; 37 | 38 | enum class CombatEndCondition 39 | { 40 | CombatExit = 0, 41 | LastDamageEvent = 1, 42 | LastHealEvent = 2, 43 | LastDamageOrHealEvent = 3, 44 | Max 45 | }; 46 | 47 | struct HealWindowOptions 48 | { 49 | bool Shown = false; 50 | 51 | DataSource DataSourceChoice = DataSource::Agents; 52 | SortOrder SortOrderChoice = SortOrder::DescendingTotalOutgoingSize; 53 | CombatEndCondition CombatEndConditionChoice = CombatEndCondition::LastDamageEvent; 54 | 55 | bool ExcludeGroup = false; 56 | bool ExcludeOffGroup = false; 57 | bool ExcludeOffSquad = false; 58 | bool ExcludeMinions = true; 59 | bool ExcludeUnmapped = true; 60 | bool ExcludeHealing = false; 61 | bool ExcludeBarrierGeneration = true; 62 | 63 | bool ShowProgressBars = true; 64 | bool UseSubgroupForBarColour = false; 65 | bool UseProfessionForBarColour = false; 66 | bool IndexNumbers = false; 67 | bool ProfessionText = false; 68 | bool ProfessionIcons = false; 69 | bool ReplacePlayerWithAccountName = false; 70 | bool UseProfessionForNameColour = false; 71 | bool UseSubgroupForNameColour = false; 72 | bool SelfOnTop = false; 73 | bool HideSelfFromList = false; 74 | bool SelfOnly = false; 75 | bool AnonymousMode = false; 76 | char Name[MAX_HEAL_WINDOW_NAME + 1] = {}; 77 | char TitleFormat[MAX_HEAL_WINDOW_TITLE + 1] = "{1} ({4}/s, {7}s in combat)"; 78 | char EntryFormat[MAX_HEAL_WINDOW_ENTRY + 1] = "{1} ({4}/s, {7}%)"; 79 | char DetailsEntryFormat[MAX_HEAL_WINDOW_ENTRY + 1] = "{1} ({4}/s, {7}%)"; 80 | 81 | ImGuiWindowFlags_ WindowFlags = ImGuiWindowFlags_None; 82 | 83 | int Hotkey = 0; 84 | 85 | Position PositionRule = Position::Manual; 86 | CornerPosition RelativeScreenCorner = CornerPosition::TopLeft; 87 | CornerPosition RelativeSelfCorner = CornerPosition::TopLeft; 88 | CornerPosition RelativeAnchorWindowCorner = CornerPosition::TopLeft; 89 | int64_t RelativeX = 0; 90 | int64_t RelativeY = 0; 91 | ImGuiID AnchorWindowId = 0; 92 | 93 | bool AutoResize = false; 94 | int64_t MaxNameLength = 0; 95 | size_t MinLinesDisplayed = 0; 96 | size_t MaxLinesDisplayed = 10; 97 | size_t FixedWindowWidth = 400; 98 | 99 | void FromJson(const nlohmann::json& pJsonObject); 100 | void ToJson(nlohmann::json& pJsonObject, const HealWindowOptions& pDefault) const; 101 | }; 102 | static_assert(std::is_same::type, int>::value == true, "HealWindowOptions::DataSourceChoice size changed"); 103 | static_assert(std::is_same::type, int>::value == true, "HealWindowOptions::SortOrderChoice size changed"); 104 | static_assert(std::is_same::type, int>::value == true, "HealWindowOptions::CombatEndConditionChoice size changed"); 105 | static_assert(std::is_same::type, int>::value == true, "HealWindowOptions::WindowFlags size changed"); 106 | -------------------------------------------------------------------------------- /src/UpdateGUI.cpp: -------------------------------------------------------------------------------- 1 | #include "UpdateGUI.h" 2 | 3 | #include "Exports.h" 4 | #include "ImGuiEx.h" 5 | #include "Log.h" 6 | 7 | #include 8 | #include 9 | 10 | void UpdateChecker::Log(std::string&& pMessage) 11 | { 12 | LogI("{}", pMessage); 13 | } 14 | 15 | bool UpdateChecker::HttpDownload(const std::string& pUrl, const std::filesystem::path& pOutputFile) 16 | { 17 | std::ofstream outputStream(pOutputFile); 18 | cpr::Response response = cpr::Download(outputStream, cpr::Url{pUrl}); 19 | if (response.status_code != 200) { 20 | Log(std::format("Downloading {} failed - http failure {} {}", pUrl, response.status_code, 21 | response.status_line)); 22 | return false; 23 | } 24 | 25 | return true; 26 | } 27 | 28 | std::optional UpdateChecker::HttpGet(const std::string& pUrl) 29 | { 30 | cpr::Response response = cpr::Get(cpr::Url{pUrl}); 31 | if (response.status_code != 200) { 32 | Log(std::format("Getting {} failed - {} {}", pUrl, response.status_code, response.status_line)); 33 | return std::nullopt; 34 | } 35 | 36 | return response.text; 37 | } 38 | 39 | void Display_UpdateWindow() 40 | { 41 | auto& state = GlobalObjects::UPDATE_STATE; 42 | if (state == nullptr) 43 | { 44 | return; 45 | } 46 | 47 | std::lock_guard lock(state->Lock); 48 | if (state->UpdateStatus != UpdateChecker::Status::Unknown && state->UpdateStatus != UpdateChecker::Status::Dismissed) 49 | { 50 | bool shown = true; 51 | if (ImGui::Begin( 52 | "Healing Stats Update###HEALING_STATS_UPDATE", 53 | &shown, 54 | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize) == true) 55 | { 56 | const UpdateChecker::Version& currentVersion = *state->CurrentVersion; 57 | const UpdateChecker::Version& newVersion = state->NewVersion; 58 | 59 | ImGui::TextColored(ImVec4(1.f, 0.f, 0.f, 1.f), "A new update for the healing stats addon is available"); 60 | ImGui::TextColored(ImVec4(1.f, 0.f, 0.f, 1.f), "Current version: %u.%urc%u", currentVersion[0], currentVersion[1], currentVersion[2]); 61 | ImGui::TextColored(ImVec4(0.f, 1.f, 0.f, 1.f), "New version: %u.%urc%u", newVersion[0], newVersion[1], newVersion[2]); 62 | 63 | if (ImGui::Button("Open download page") == true) 64 | { 65 | ShellExecuteA(nullptr, nullptr, "https://github.com/Krappa322/arcdps_healing_stats/releases", nullptr, nullptr, SW_SHOW); 66 | } 67 | 68 | switch (state->UpdateStatus) 69 | { 70 | case UpdateChecker::Status::UpdateAvailable: 71 | if (ImGui::Button("Update automatically") == true) 72 | { 73 | GlobalObjects::UPDATE_CHECKER->PerformInstallOrUpdate(*state); 74 | } 75 | break; 76 | case UpdateChecker::Status::UpdateInProgress: 77 | ImGui::TextUnformatted("Update in progress"); 78 | break; 79 | case UpdateChecker::Status::UpdateSuccessful: 80 | ImGui::TextColored(ImVec4(0.f, 1.f, 0.f, 1.f), "Update finished, restart Guild Wars 2 for the update to take effect"); 81 | break; 82 | case UpdateChecker::Status::UpdateError: 83 | ImGui::TextColored(ImVec4(1.f, 0.f, 0.f, 1.f), "An error occured while updating"); 84 | break; 85 | default: 86 | break; 87 | } 88 | 89 | ImGui::End(); 90 | } 91 | 92 | if (shown != true) 93 | { 94 | state->UpdateStatus = ArcdpsExtension::UpdateCheckerBase::Status::Dismissed; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/UpdateGUI.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class UpdateChecker final : public ArcdpsExtension::UpdateCheckerBase 6 | { 7 | public: 8 | void Log(std::string&& pMessage) override; 9 | bool HttpDownload(const std::string& pUrl, const std::filesystem::path& pOutputFile) override; 10 | std::optional HttpGet(const std::string& pUrl) override; 11 | }; 12 | 13 | void Display_UpdateWindow(); -------------------------------------------------------------------------------- /src/Utilities.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Log.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | 17 | template(EnumType::Max)> 18 | class EnumStringArray : public std::array 19 | { 20 | public: 21 | template && ...)>> 23 | constexpr EnumStringArray(Args... pArgs) 24 | : std::array{pArgs...} 25 | { 26 | static_assert(sizeof...(Args) == Size, "Incorrect array size"); 27 | } 28 | 29 | using std::array::operator[]; 30 | 31 | constexpr const char* operator[](EnumType pIndex) const 32 | { 33 | return std::array::operator[](static_cast(pIndex)); 34 | } 35 | }; 36 | 37 | uint64_t constexpr divide_rounded_safe(uint64_t pDividend, uint64_t pDivisor) 38 | { 39 | if (pDivisor == 0) 40 | { 41 | return 0; 42 | } 43 | 44 | return (pDividend + (pDivisor / 2)) / pDivisor; 45 | } 46 | 47 | template 48 | double constexpr divide_safe(T pDividend, U pDivisor) 49 | { 50 | if (pDivisor == 0) 51 | { 52 | return 0; 53 | } 54 | 55 | return static_cast(pDividend) / static_cast(pDivisor); 56 | } 57 | 58 | size_t constexpr constexpr_strlen(const char* pString) 59 | { 60 | size_t result = 0; 61 | for (const char* curChar = pString; *curChar != '\0'; curChar++) 62 | { 63 | result++; 64 | } 65 | 66 | return result; 67 | } 68 | 69 | // Overly complicated solution so the compiler errors aren't too readable 70 | template 71 | struct LongestStringInArray 72 | { 73 | constexpr const static uint64_t value = (std::max)(constexpr_strlen(Array[i]), LongestStringInArray::value); 74 | }; 75 | 76 | template 77 | struct LongestStringInArray 78 | { 79 | constexpr const static uint64_t value = constexpr_strlen(Array[0]); 80 | }; 81 | 82 | // Returns number of characters (as opposed to number of bytes) in a utf8 string 83 | static inline size_t utf8_strlen(std::string_view pString) 84 | { 85 | // _mbstrlen is broken for utf8 so we use MultiByteToWideChar to get the character count (passing no destination 86 | // buffer makes it return the amount of space needed - our character count). 87 | int charCount = MultiByteToWideChar(CP_UTF8, 0, pString.data(), static_cast(pString.size()), nullptr, 0); 88 | assert(charCount > 0); 89 | 90 | // charCount does not includes a null character since the source is not null terminated 91 | return static_cast(charCount); 92 | } 93 | 94 | // Returns number of characters (as opposed to number of bytes) in a utf8 string 95 | static inline size_t utf8_strlen(const char* pString) 96 | { 97 | // _mbstrlen is broken for utf8 so we use MultiByteToWideChar to get the character count (passing no destination 98 | // buffer makes it return the amount of space needed - our character count). 99 | int charCount = MultiByteToWideChar(CP_UTF8, 0, pString, -1, nullptr, 0); 100 | assert(charCount > 0); 101 | 102 | // charCount includes null character so remove 1 103 | return static_cast(charCount) - 1; 104 | } 105 | 106 | static inline std::string VirtualKeyToString(int pVirtualKey) 107 | { 108 | char buffer[1024]; 109 | 110 | uint32_t scanCode = MapVirtualKeyA(pVirtualKey, MAPVK_VK_TO_VSC); 111 | 112 | switch (pVirtualKey) 113 | { 114 | case VK_LEFT: case VK_UP: case VK_RIGHT: case VK_DOWN: 115 | case VK_RCONTROL: case VK_RMENU: 116 | case VK_LWIN: case VK_RWIN: case VK_APPS: 117 | case VK_PRIOR: case VK_NEXT: 118 | case VK_END: case VK_HOME: 119 | case VK_INSERT: case VK_DELETE: 120 | case VK_DIVIDE: 121 | case VK_NUMLOCK: 122 | scanCode |= KF_EXTENDED; 123 | [[fallthrough]]; 124 | default: 125 | int result = GetKeyNameTextA(scanCode << 16, buffer, sizeof(buffer)); 126 | if (result == 0) 127 | { 128 | //LOG("Translating key %i failed - error %u", pVirtualKey, GetLastError()); 129 | return std::string{}; 130 | } 131 | break; 132 | } 133 | 134 | return std::string{buffer}; 135 | } 136 | 137 | // Prints pNumber to pResultBuffer with magnitude suffix if necessary 138 | // Returns output with the same rules as snprintf 139 | template 140 | static inline int snprint_magnitude(char* pResultBuffer, size_t pResultBufferLength, NumberType pNumber) 141 | { 142 | if (pNumber < 10000) 143 | { 144 | if constexpr (std::is_same::value == true) 145 | { 146 | return snprintf(pResultBuffer, pResultBufferLength, "%llu", pNumber); 147 | } 148 | else if constexpr (std::is_same::value == true) 149 | { 150 | return snprintf(pResultBuffer, pResultBufferLength, "%.1f", pNumber); 151 | } 152 | else 153 | { 154 | assert(false); 155 | //static_assert(false, "unhandled type"); 156 | } 157 | } 158 | 159 | static constexpr char bases[] = {'k', 'M', 'G', 'T', 'P', 'E'}; 160 | 161 | int magnitude = static_cast(log(pNumber) / log(1000)); 162 | return snprintf(pResultBuffer, pResultBufferLength, "%.1f%c", pNumber / pow(1000, magnitude), bases[magnitude - 1]); 163 | } 164 | 165 | // Replaces "{1}", "{2}", etc. with pArgs[0], pArgs[1], etc. If that argument is nullopt, the entry is not replaced. 166 | // Returns the amount of bytes written 167 | template 168 | static inline size_t ReplaceFormatted(char* pResultBuffer, size_t pResultBufferLength, const char* pFormatString, std::array>, ArgCount> pArgs) 169 | { 170 | char* startBuffer = pResultBuffer; 171 | assert(pFormatString != nullptr); 172 | 173 | for (const char* curChar = pFormatString; *curChar != '\0'; curChar++) 174 | { 175 | if (*curChar == '{') 176 | { 177 | const char* key = curChar + 1; 178 | const char* closeBrace = curChar + 2; 179 | if (*key >= '1' && *key <= '9' && *closeBrace == '}') 180 | { 181 | uint32_t num = *key - '1'; 182 | 183 | if (pArgs.size() > num && pArgs[num].has_value() == true) 184 | { 185 | int count; 186 | if (std::holds_alternative(*pArgs[num]) == true) 187 | { 188 | count = snprint_magnitude(pResultBuffer, pResultBufferLength, std::get(*pArgs[num])); 189 | } 190 | else 191 | { 192 | assert(std::holds_alternative(*pArgs[num]) == true); 193 | count = snprint_magnitude(pResultBuffer, pResultBufferLength, std::get(*pArgs[num])); 194 | } 195 | 196 | if (static_cast(count) >= pResultBufferLength) 197 | { 198 | // Value was truncated. Break the loop, which will insert a null character at the start of the 199 | // printed value (thus not showing the truncated value) 200 | LOG("Truncated value for format string %s at pos %llu - would require %i bytes but only %zu are available", pFormatString, curChar - pFormatString, count, pResultBufferLength); 201 | break; 202 | } 203 | 204 | curChar += 2; // We've processed the braces as well 205 | pResultBuffer += count; 206 | pResultBufferLength -= count; 207 | continue; 208 | } 209 | } 210 | } 211 | 212 | if (pResultBufferLength == 1) 213 | { 214 | // Not enough space in buffer 215 | break; 216 | } 217 | 218 | *pResultBuffer = *curChar; 219 | pResultBufferLength--; 220 | pResultBuffer++; 221 | } 222 | 223 | assert(pResultBufferLength > 0); 224 | *pResultBuffer = '\0'; 225 | 226 | return pResultBuffer - startBuffer; 227 | } 228 | 229 | static inline std::string_view utf8_substr(std::string_view pStr, size_t pCharacterCount) 230 | { 231 | int32_t bytesToSkip = 0; 232 | int32_t length = pStr.size(); 233 | U8_FWD_N(pStr.data(), bytesToSkip, length, static_cast(pCharacterCount)); 234 | 235 | return pStr.substr(0, bytesToSkip); 236 | } 237 | -------------------------------------------------------------------------------- /test/ConfigTest.cpp: -------------------------------------------------------------------------------- 1 | #pragma warning(push, 0) 2 | #pragma warning(disable : 4005) 3 | #pragma warning(disable : 4389) 4 | #pragma warning(disable : 26439) 5 | #pragma warning(disable : 26495) 6 | #include 7 | #pragma warning(pop) 8 | 9 | #include "Options.h" 10 | 11 | namespace 12 | { 13 | template 14 | T rand_t() 15 | { 16 | uint8_t components[sizeof(T)]; 17 | for (size_t i = 0; i < std::size(components); i++) 18 | { 19 | components[i] = static_cast(rand()); 20 | } 21 | 22 | T result; 23 | memcpy(&result, &components, sizeof(result)); 24 | return result; 25 | } 26 | 27 | template 28 | void rand_string(char(&pStringBuffer)[Size]) 29 | { 30 | uint64_t length = rand_t() % Size; 31 | 32 | for (size_t i = 0; i < length; i++) 33 | { 34 | do 35 | { 36 | pStringBuffer[i] = rand_t(); 37 | } while (pStringBuffer[i] < 1 || isprint(pStringBuffer[i]) == 0); 38 | } 39 | pStringBuffer[length] = '\0'; 40 | } 41 | }; // anonymous namespace 42 | 43 | TEST(ConfigTest, Serialize_Deserialize) 44 | { 45 | uint64_t raw_seed = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); 46 | uint32_t seed = (reinterpret_cast(&raw_seed)[0] ^ reinterpret_cast(&raw_seed)[1]); 47 | LogD("Seed {} (raw_seed={})", seed, raw_seed); 48 | srand(seed); 49 | 50 | HealTableOptions options; 51 | options.DebugMode = rand_t(); 52 | options.LogLevel = rand_t(); 53 | options.EvtcLoggingEnabled = rand_t(); 54 | rand_string(options.EvtcRpcEndpoint); 55 | options.EvtcRpcEnabled = rand_t(); 56 | 57 | for (HealWindowContext& window : options.Windows) 58 | { 59 | window.Shown = rand_t(); 60 | 61 | window.DataSourceChoice = rand_t(); 62 | window.SortOrderChoice = rand_t(); 63 | window.CombatEndConditionChoice = rand_t(); 64 | 65 | window.ExcludeGroup = rand_t(); 66 | window.ExcludeOffGroup = rand_t(); 67 | window.ExcludeOffSquad = rand_t(); 68 | window.ExcludeMinions = rand_t(); 69 | window.ExcludeUnmapped = rand_t(); 70 | window.ExcludeHealing = rand_t(); 71 | window.ExcludeBarrierGeneration = rand_t(); 72 | 73 | window.ShowProgressBars = rand_t(); 74 | window.UseSubgroupForBarColour = rand_t(); 75 | window.IndexNumbers = rand_t(); 76 | window.ProfessionText = rand_t(); 77 | window.ProfessionIcons = rand_t(); 78 | window.ReplacePlayerWithAccountName = rand_t(); 79 | window.UseProfessionForNameColour = rand_t(); 80 | window.UseSubgroupForNameColour = rand_t(); 81 | window.SelfOnTop = rand_t(); 82 | window.HideSelfFromList = rand_t(); 83 | window.SelfOnly = rand_t(); 84 | window.AnonymousMode = rand_t(); 85 | rand_string(window.Name); 86 | rand_string(window.TitleFormat); 87 | rand_string(window.EntryFormat); 88 | rand_string(window.DetailsEntryFormat); 89 | 90 | window.WindowFlags = rand_t(); 91 | 92 | window.Hotkey = rand_t(); 93 | 94 | window.PositionRule = rand_t(); 95 | window.RelativeScreenCorner = rand_t(); 96 | window.RelativeSelfCorner = rand_t(); 97 | window.RelativeAnchorWindowCorner = rand_t(); 98 | window.RelativeX = rand_t(); 99 | window.RelativeY = rand_t(); 100 | window.AnchorWindowId = rand_t(); 101 | 102 | window.AutoResize = rand_t(); 103 | window.MinLinesDisplayed = rand_t(); 104 | window.MaxLinesDisplayed = rand_t(); 105 | window.FixedWindowWidth = rand_t(); 106 | } 107 | 108 | HealTableOptions options2; 109 | 110 | nlohmann::json temp; 111 | options.ToJson(temp); 112 | 113 | LogI("Json: {}", temp.dump()); 114 | 115 | options2.FromJson(temp); 116 | 117 | ASSERT_EQ(options.DebugMode, options2.DebugMode); 118 | ASSERT_EQ(options.LogLevel, options2.LogLevel); 119 | ASSERT_EQ(options.EvtcLoggingEnabled, options2.EvtcLoggingEnabled); 120 | ASSERT_EQ(strcmp(options.EvtcRpcEndpoint, options2.EvtcRpcEndpoint), 0); 121 | ASSERT_EQ(options.EvtcRpcEnabled, options2.EvtcRpcEnabled); 122 | 123 | for (size_t i = 0; i < options2.Windows.size(); i++) 124 | { 125 | const HealWindowContext& windowLeft = options.Windows[i]; 126 | const HealWindowContext& windowRight = options2.Windows[i]; 127 | 128 | ASSERT_EQ(windowLeft.Shown, windowRight.Shown); 129 | 130 | ASSERT_EQ(windowLeft.DataSourceChoice, windowRight.DataSourceChoice); 131 | ASSERT_EQ(windowLeft.SortOrderChoice, windowRight.SortOrderChoice); 132 | ASSERT_EQ(windowLeft.CombatEndConditionChoice, windowRight.CombatEndConditionChoice); 133 | 134 | ASSERT_EQ(windowLeft.ExcludeGroup, windowRight.ExcludeGroup); 135 | ASSERT_EQ(windowLeft.ExcludeOffGroup, windowRight.ExcludeOffGroup); 136 | ASSERT_EQ(windowLeft.ExcludeOffSquad, windowRight.ExcludeOffSquad); 137 | ASSERT_EQ(windowLeft.ExcludeMinions, windowRight.ExcludeMinions); 138 | ASSERT_EQ(windowLeft.ExcludeUnmapped, windowRight.ExcludeUnmapped); 139 | ASSERT_EQ(windowLeft.ExcludeHealing, windowRight.ExcludeHealing); 140 | ASSERT_EQ(windowLeft.ExcludeBarrierGeneration, windowRight.ExcludeBarrierGeneration); 141 | 142 | ASSERT_EQ(windowLeft.ShowProgressBars, windowRight.ShowProgressBars); 143 | ASSERT_EQ(windowLeft.UseSubgroupForBarColour, windowRight.UseSubgroupForBarColour); 144 | ASSERT_EQ(windowLeft.IndexNumbers, windowRight.IndexNumbers); 145 | ASSERT_EQ(windowLeft.ProfessionText, windowRight.ProfessionText); 146 | ASSERT_EQ(windowLeft.ProfessionIcons, windowRight.ProfessionIcons); 147 | ASSERT_EQ(windowLeft.ReplacePlayerWithAccountName, windowRight.ReplacePlayerWithAccountName); 148 | ASSERT_EQ(windowLeft.UseProfessionForNameColour, windowRight.UseProfessionForNameColour); 149 | ASSERT_EQ(windowLeft.UseSubgroupForNameColour, windowRight.UseSubgroupForNameColour); 150 | ASSERT_EQ(windowLeft.SelfOnTop, windowRight.SelfOnTop); 151 | ASSERT_EQ(windowLeft.HideSelfFromList, windowRight.HideSelfFromList); 152 | ASSERT_EQ(windowLeft.SelfOnly, windowRight.SelfOnly); 153 | ASSERT_EQ(windowLeft.AnonymousMode, windowRight.AnonymousMode); 154 | ASSERT_EQ(strcmp(windowLeft.Name, windowRight.Name), 0); 155 | ASSERT_EQ(strcmp(windowLeft.TitleFormat, windowRight.TitleFormat), 0); 156 | ASSERT_EQ(strcmp(windowLeft.EntryFormat, windowRight.EntryFormat), 0); 157 | ASSERT_EQ(strcmp(windowLeft.DetailsEntryFormat, windowRight.DetailsEntryFormat), 0); 158 | 159 | ASSERT_EQ(windowLeft.WindowFlags, windowRight.WindowFlags); 160 | 161 | ASSERT_EQ(windowLeft.Hotkey, windowRight.Hotkey); 162 | 163 | ASSERT_EQ(windowLeft.PositionRule, windowRight.PositionRule); 164 | ASSERT_EQ(windowLeft.RelativeScreenCorner, windowRight.RelativeScreenCorner); 165 | ASSERT_EQ(windowLeft.RelativeSelfCorner, windowRight.RelativeSelfCorner); 166 | ASSERT_EQ(windowLeft.RelativeAnchorWindowCorner, windowRight.RelativeAnchorWindowCorner); 167 | ASSERT_EQ(windowLeft.RelativeX, windowRight.RelativeX); 168 | ASSERT_EQ(windowLeft.RelativeY, windowRight.RelativeY); 169 | ASSERT_EQ(windowLeft.AnchorWindowId, windowRight.AnchorWindowId); 170 | 171 | ASSERT_EQ(windowLeft.AutoResize, windowRight.AutoResize); 172 | ASSERT_EQ(windowLeft.MinLinesDisplayed, windowRight.MinLinesDisplayed); 173 | ASSERT_EQ(windowLeft.MaxLinesDisplayed, windowRight.MaxLinesDisplayed); 174 | ASSERT_EQ(windowLeft.FixedWindowWidth, windowRight.FixedWindowWidth); 175 | } 176 | } 177 | 178 | TEST(ConfigTest, Defaults) 179 | { 180 | HealTableOptions options; 181 | 182 | nlohmann::json temp; 183 | options.ToJson(temp); 184 | 185 | ASSERT_EQ(temp.dump(), "{\"Version\":1}"); 186 | } 187 | -------------------------------------------------------------------------------- /test/EnvironmentTest.cpp: -------------------------------------------------------------------------------- 1 | #pragma warning(push, 0) 2 | #pragma warning(disable : 4005) 3 | #pragma warning(disable : 4389) 4 | #pragma warning(disable : 26439) 5 | #pragma warning(disable : 26495) 6 | #include 7 | #pragma warning(pop) 8 | 9 | #include "Exports.h" 10 | 11 | #include 12 | #include 13 | 14 | TEST(EnvironmentTest, shutdown_race) 15 | { 16 | ModInitSignature mod_init = get_init_addr("unit_test", nullptr, nullptr, GetModuleHandle(NULL), malloc, free, 0); 17 | arcdps_exports exports = *mod_init(); 18 | ASSERT_NE(exports.sig, 0); 19 | 20 | ag ag1{}; 21 | ag ag2{}; 22 | ag1.elite = 0; 23 | ag1.prof = static_cast(1); 24 | ag2.self = 1; 25 | ag2.id = 100; 26 | ag2.name = "testagent.1234"; 27 | exports.combat(nullptr, &ag1, &ag2, nullptr, 0, 0); 28 | exports.combat_local(nullptr, &ag1, &ag2, nullptr, 0, 0); 29 | 30 | ModReleaseSignature mod_release = get_release_addr(); 31 | mod_release(); 32 | 33 | exports.combat(nullptr, &ag1, &ag2, nullptr, 0, 0); 34 | exports.combat_local(nullptr, &ag1, &ag2, nullptr, 0, 0); 35 | exports.imgui(1, 0); 36 | exports.options_end(); 37 | auto res = exports.wnd_nofilter(0, 123, 0, 0); 38 | ASSERT_EQ(res, 123); 39 | } -------------------------------------------------------------------------------- /test/GUITest.cpp: -------------------------------------------------------------------------------- 1 | #pragma warning(push, 0) 2 | #pragma warning(disable : 4005) 3 | #pragma warning(disable : 4389) 4 | #pragma warning(disable : 26439) 5 | #pragma warning(disable : 26495) 6 | #include 7 | #pragma warning(pop) 8 | 9 | #include "GUI.h" 10 | 11 | TEST(GUITest, LoopDetection) 12 | { 13 | HealTableOptions options; 14 | for (size_t i = 0; i < options.Windows.size(); i++) 15 | { 16 | options.Windows[0].WindowId = 100; 17 | options.Windows[0].AnchorWindowId = 200; 18 | options.Windows[1].WindowId = 200; 19 | options.Windows[1].AnchorWindowId = 300; 20 | options.Windows[2].WindowId = 300; 21 | options.Windows[2].AnchorWindowId = 0; 22 | 23 | options.Windows[3].WindowId = 1000; 24 | options.Windows[3].AnchorWindowId = 2000; 25 | options.Windows[4].WindowId = 2000; 26 | options.Windows[4].AnchorWindowId = 3000; // points to non-existant window 27 | 28 | options.Windows[5].WindowId = 10; 29 | options.Windows[5].AnchorWindowId = 20; 30 | options.Windows[6].WindowId = 20; 31 | options.Windows[6].AnchorWindowId = 30; 32 | options.Windows[7].WindowId = 30; 33 | options.Windows[7].AnchorWindowId = 40; 34 | options.Windows[8].WindowId = 40; 35 | options.Windows[8].AnchorWindowId = 20; 36 | 37 | FindAndResolveCyclicDependencies(options, i); 38 | if (i == 0 || i == 1 || i == 3 || i == 4) 39 | { 40 | // If non-cyclical chain, AnchorWindowId should not be reset 41 | ASSERT_NE(options.Windows[i].AnchorWindowId, 0); 42 | } 43 | else 44 | { 45 | // Else if no chain or if loop was detected, AnchorWindowId should be reset 46 | ASSERT_EQ(options.Windows[i].AnchorWindowId, 0); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/StressTest.cpp: -------------------------------------------------------------------------------- 1 | #pragma warning(push, 0) 2 | #pragma warning(disable : 4005) 3 | #pragma warning(disable : 4389) 4 | #pragma warning(disable : 26439) 5 | #pragma warning(disable : 26495) 6 | #include 7 | #pragma warning(pop) 8 | 9 | #include "Exports.h" 10 | #include "Log.h" 11 | 12 | #include "spdlog/stopwatch.h" 13 | 14 | #include 15 | 16 | TEST(Stress, DISABLED_Stress) 17 | { 18 | constexpr static size_t CLIENT_COUNT = 10; 19 | constexpr static size_t SENDER_COUNT = 10; 20 | constexpr static uint64_t EVENT_COUNT = 10'000; 21 | constexpr static uint64_t REPORT_INTERVAL = 10'000; 22 | 23 | extern const char* LoadRootCertificatesFromResource(); 24 | const char* res = LoadRootCertificatesFromResource(); 25 | ASSERT_EQ(res, nullptr); 26 | 27 | std::atomic_bool failed = false; 28 | 29 | std::array, CLIENT_COUNT> clients; 30 | std::array, CLIENT_COUNT> client_threads; 31 | std::array, SENDER_COUNT> sender_threads; 32 | 33 | std::array, CLIENT_COUNT> next_event = {}; 34 | 35 | auto getEndpoint = []() -> std::string 36 | { 37 | return std::string{"dev-evtc-rpc.kappa322.com:443"}; 38 | }; 39 | auto getCertificates = []() -> std::string 40 | { 41 | return std::string{GlobalObjects::ROOT_CERTIFICATES}; 42 | }; 43 | 44 | for (size_t i = 0; i < CLIENT_COUNT; i++) 45 | { 46 | auto eventHandler = [&next_event, &failed, i](cbtevent* pEvent, uint16_t /*pInstanceId*/) 47 | { 48 | size_t sender_num; 49 | memcpy(&sender_num, pEvent, sizeof(sender_num)); 50 | size_t event_num; 51 | memcpy(&event_num, reinterpret_cast(pEvent) + sizeof(sender_num), sizeof(event_num)); 52 | 53 | if (sender_num > next_event[i].size()) 54 | { 55 | LogE("Client {} got event from sender {} which is invalid", i, sender_num); 56 | failed = true; 57 | return; 58 | } 59 | 60 | if (event_num != next_event[i][sender_num]) 61 | { 62 | LogE("Client {} got event {} from {} when it was expecting {}", i, event_num, sender_num, next_event[i][sender_num]); 63 | failed = true; 64 | } 65 | 66 | next_event[i][sender_num] += 1; 67 | }; 68 | 69 | clients[i] = std::make_unique(std::function{getEndpoint}, std::function{getCertificates}, std::move(eventHandler)); 70 | client_threads[i] = std::make_unique(evtc_rpc_client::ThreadStartServe, clients[i].get()); 71 | for (size_t j = 0; j < CLIENT_COUNT; j++) 72 | { 73 | char nameBuffer[128]; 74 | snprintf(nameBuffer, sizeof(nameBuffer), "testagent%zu.1234", j); 75 | 76 | ag ag1{}; 77 | ag ag2{}; 78 | ag1.elite = 0; 79 | ag1.prof = static_cast(1); 80 | ag2.id = 100ULL + j; 81 | ag2.name = nameBuffer; 82 | 83 | if (i == j) 84 | { 85 | ag2.self = 1; 86 | clients[i]->ProcessLocalEvent(nullptr, &ag1, &ag2, nullptr, 0, 0); 87 | } 88 | else 89 | { 90 | ag2.self = 0; 91 | clients[i]->ProcessAreaEvent(nullptr, &ag1, &ag2, nullptr, 0, 0); 92 | } 93 | } 94 | } 95 | 96 | for (size_t i = 0; i < CLIENT_COUNT; i++) 97 | { 98 | // Make sure server receives the client registration before sending any events 99 | clients[i]->FlushEvents(0); 100 | } 101 | 102 | for (size_t i = 0; i < SENDER_COUNT; i++) 103 | { 104 | sender_threads[i] = std::make_unique([](size_t pSenderNum, evtc_rpc_client& pClient, size_t pEventCount, std::atomic_bool& pFailedFlag) 105 | { 106 | cbtevent ev; 107 | memset(&ev, 0x00, sizeof(ev)); 108 | for (uint64_t i = 0; i < pEventCount; i++) 109 | { 110 | memcpy(&ev, &pSenderNum, sizeof(pSenderNum)); 111 | memcpy(reinterpret_cast(&ev) + sizeof(pSenderNum), &i, sizeof(i)); 112 | 113 | pClient.FlushEvents(4999); 114 | pClient.ProcessLocalEvent(&ev, nullptr, nullptr, nullptr, 0, 0); 115 | 116 | if (i % REPORT_INTERVAL == 0) 117 | { 118 | printf("%zu: loop %llu\n", pSenderNum, i); 119 | if (pFailedFlag.load() == true) 120 | { 121 | printf("%zu: Flagged as failed, stopping\n", pSenderNum); 122 | break; 123 | } 124 | } 125 | } 126 | pClient.FlushEvents(0); 127 | }, i, std::ref(*clients[i]), EVENT_COUNT, std::ref(failed)); 128 | } 129 | for (size_t i = 0; i < SENDER_COUNT; i++) 130 | { 131 | sender_threads[i]->join(); 132 | } 133 | 134 | spdlog::stopwatch complete_timer; 135 | 136 | bool completed = false; 137 | std::chrono::steady_clock::time_point last_received_event = std::chrono::steady_clock::now(); 138 | std::array lastCount = {}; 139 | while (failed.load() == false && last_received_event + std::chrono::milliseconds(1000) > std::chrono::steady_clock::now()) 140 | { 141 | for (size_t sender_index = 0; sender_index < SENDER_COUNT; sender_index++) 142 | { 143 | uint64_t count = 0; 144 | for (size_t client_index = 0; client_index < CLIENT_COUNT; client_index++) 145 | { 146 | count += next_event[client_index][sender_index]; 147 | } 148 | 149 | EXPECT_GE(count, lastCount[sender_index]); 150 | if (count > lastCount[sender_index]) 151 | { 152 | last_received_event = std::chrono::steady_clock::now(); 153 | if ((count / REPORT_INTERVAL) > (lastCount[sender_index] / REPORT_INTERVAL)) 154 | { 155 | printf("%llu: Received %llu events\n", sender_index, (count / REPORT_INTERVAL) * REPORT_INTERVAL); 156 | } 157 | lastCount[sender_index] = count; 158 | } 159 | } 160 | 161 | uint64_t totalCount = 0; 162 | totalCount = std::accumulate(lastCount.begin(), lastCount.end(), 0ULL); 163 | 164 | if (totalCount >= SENDER_COUNT * EVENT_COUNT * (CLIENT_COUNT - 1)) 165 | { 166 | completed = true; 167 | break; 168 | } 169 | 170 | Sleep(1); 171 | } 172 | 173 | LogI("Awaiting client finish took {}", complete_timer); 174 | EXPECT_TRUE(completed); 175 | EXPECT_FALSE(failed.load()); 176 | 177 | for (size_t i = 0; i < CLIENT_COUNT; i++) 178 | { 179 | clients[i]->Shutdown(); 180 | } 181 | for (size_t i = 0; i < CLIENT_COUNT; i++) 182 | { 183 | client_threads[i]->join(); 184 | } 185 | 186 | for (size_t sender_index = 0; sender_index < SENDER_COUNT; sender_index++) 187 | { 188 | for (size_t client_index = 0; client_index < CLIENT_COUNT; client_index++) 189 | { 190 | if (client_index == sender_index) 191 | { 192 | // Sender shouldn't send events to itself 193 | EXPECT_EQ(next_event[client_index][sender_index], 0); 194 | } 195 | else 196 | { 197 | if (next_event[client_index][sender_index] != EVENT_COUNT) 198 | { 199 | LogE("Client {} only received {} events from {} when it was expecting {}", 200 | client_index, next_event[client_index][sender_index], sender_index, EVENT_COUNT); 201 | EXPECT_EQ(next_event[client_index][sender_index], EVENT_COUNT); // Just log with values 202 | } 203 | } 204 | } 205 | } 206 | } -------------------------------------------------------------------------------- /test/UtilitiesTest.cpp: -------------------------------------------------------------------------------- 1 | #include "Utilities.h" 2 | 3 | #pragma warning(push, 0) 4 | #pragma warning(disable : 4005) 5 | #pragma warning(disable : 4389) 6 | #pragma warning(disable : 26439) 7 | #pragma warning(disable : 26495) 8 | #include 9 | #pragma warning(pop) 10 | 11 | TEST(UtilitiesTest, utf8_substr_ascii) 12 | { 13 | const char* sourceDataConst = "Hello World!!!"; 14 | char sourceDataBuf[14]; // Ensure there's no null termination 15 | memcpy(sourceDataBuf, sourceDataConst, 14); 16 | std::string_view sourceData = std::string_view{sourceDataBuf, 14}; 17 | ASSERT_EQ(utf8_substr(sourceData, 0), ""); 18 | ASSERT_EQ(utf8_substr(sourceData, 1), "H"); 19 | ASSERT_EQ(utf8_substr(sourceData, 4), "Hell"); 20 | ASSERT_EQ(utf8_substr(sourceData, 13), "Hello World!!"); 21 | ASSERT_EQ(utf8_substr(sourceData, 14), "Hello World!!!"); 22 | ASSERT_EQ(utf8_substr(sourceData, 15), "Hello World!!!"); 23 | ASSERT_EQ(utf8_substr(sourceData, 100000000), "Hello World!!!"); 24 | } 25 | 26 | TEST(UtilitiesTest, utf8_substr_unicode) 27 | { 28 | const char* sourceDataConst = "ホヽのö Wó卂丂ᗪ"; 29 | char sourceDataBuf[24]; // Ensure there's no null termination 30 | memcpy(sourceDataBuf, sourceDataConst, 24); 31 | std::string_view sourceData = std::string_view{sourceDataBuf, 24}; 32 | ASSERT_EQ(utf8_substr(sourceData, 0), ""); 33 | ASSERT_EQ(utf8_substr(sourceData, 1), "ホ"); 34 | ASSERT_EQ(utf8_substr(sourceData, 4), "ホヽのö"); 35 | ASSERT_EQ(utf8_substr(sourceData, 8), "ホヽのö Wó卂"); 36 | ASSERT_EQ(utf8_substr(sourceData, 9), "ホヽのö Wó卂丂"); 37 | ASSERT_EQ(utf8_substr(sourceData, 10), "ホヽのö Wó卂丂ᗪ"); 38 | ASSERT_EQ(utf8_substr(sourceData, 11), "ホヽのö Wó卂丂ᗪ"); 39 | ASSERT_EQ(utf8_substr(sourceData, 100000000), "ホヽのö Wó卂丂ᗪ"); 40 | } 41 | -------------------------------------------------------------------------------- /test/main.cpp: -------------------------------------------------------------------------------- 1 | #include "Exports.h" 2 | #include "Log.h" 3 | 4 | #include 5 | 6 | #pragma warning(push, 0) 7 | #pragma warning(disable : 4005) 8 | #pragma warning(disable : 4389) 9 | #include 10 | #pragma warning(pop) 11 | 12 | #include 13 | 14 | extern "C" __declspec(dllexport) void e3(const char* pString); 15 | extern "C" __declspec(dllexport) void e5(ImVec4** pColors); 16 | extern "C" __declspec(dllexport) uint64_t e6(); 17 | extern "C" __declspec(dllexport) uint64_t e7(); 18 | extern "C" __declspec(dllexport) void e9(cbtevent* pEvent, uint32_t pSignature); 19 | extern "C" __declspec(dllexport) void e10(cbtevent* pEvent, uint32_t pSignature); 20 | 21 | #pragma pack(push, 1) 22 | namespace 23 | { 24 | struct ArcModifiers 25 | { 26 | uint16_t _1 = VK_SHIFT; 27 | uint16_t _2 = VK_MENU; 28 | uint16_t Multi = 0; 29 | uint16_t Fill = 0; 30 | }; 31 | } // anonymous namespace 32 | #pragma pack(pop) 33 | 34 | void e3(const char* /*pString*/) 35 | { 36 | return; // Logging, ignored 37 | } 38 | 39 | void e5(ImVec4** /*pColors*/) 40 | { 41 | return; // Logging, ignored 42 | } 43 | 44 | uint64_t e6() 45 | { 46 | return 0; // everything set to false 47 | } 48 | 49 | uint64_t e7() 50 | { 51 | ArcModifiers mods; 52 | return *reinterpret_cast(&mods); 53 | } 54 | 55 | void e9(cbtevent*, uint32_t) 56 | { 57 | return; // Ignore, can be overridden by specific test if need be 58 | } 59 | 60 | void e10(cbtevent*, uint32_t) 61 | { 62 | return; // Ignore, can be overridden by specific test if need be 63 | } 64 | 65 | class TestLogFlusher : public testing::EmptyTestEventListener 66 | { 67 | void OnTestStart(const ::testing::TestInfo& pTestInfo) override 68 | { 69 | LogI("Starting test {}.{}.{}", pTestInfo.test_suite_name(), pTestInfo.test_case_name(), pTestInfo.name()); 70 | } 71 | 72 | // Called after a failed assertion or a SUCCESS(). 73 | void OnTestPartResult(const testing::TestPartResult& /*pTestInfo*/) override 74 | { 75 | Log_::FlushLogFile(); 76 | } 77 | 78 | // Called after a test ends. 79 | void OnTestEnd(const testing::TestInfo& /*pTestInfo*/) override 80 | { 81 | Log_::FlushLogFile(); 82 | } 83 | }; 84 | 85 | int main(int pArgumentCount, char** pArgumentVector) 86 | { 87 | GlobalObjects::IS_UNIT_TEST = true; 88 | 89 | Log_::Init(true, "logs/unit_tests.txt"); 90 | Log_::SetLevel(spdlog::level::trace); 91 | Log_::LockLogger(); 92 | 93 | ::testing::InitGoogleTest(&pArgumentCount, pArgumentVector); 94 | 95 | testing::UnitTest::GetInstance()->listeners().Append(new TestLogFlusher); 96 | 97 | int result = RUN_ALL_TESTS(); 98 | 99 | Log_::LOGGER = nullptr; 100 | spdlog::shutdown(); 101 | 102 | return result; 103 | } 104 | -------------------------------------------------------------------------------- /test/test.vcxproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | --gtest_catch_exceptions=0 --gtest_break_on_failure 5 | WindowsLocalDebugger 6 | 7 | 8 | --gtest_catch_exceptions=0 --gtest_break_on_failure 9 | WindowsLocalDebugger 10 | 11 | 12 | false 13 | 14 | 15 | --gtest_catch_exceptions=0 --gtest_break_on_failure 16 | WindowsLocalDebugger 17 | 18 | -------------------------------------------------------------------------------- /test/xevtc_logs/berserker_solo.xevtc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/test/xevtc_logs/berserker_solo.xevtc -------------------------------------------------------------------------------- /test/xevtc_logs/druid_MO.xevtc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/test/xevtc_logs/druid_MO.xevtc -------------------------------------------------------------------------------- /test/xevtc_logs/druid_solo.xevtc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/test/xevtc_logs/druid_solo.xevtc -------------------------------------------------------------------------------- /test/xevtc_logs/null_names.xevtc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/test/xevtc_logs/null_names.xevtc -------------------------------------------------------------------------------- /test/xevtc_logs/renegade_solo.xevtc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krappa322/arcdps_healing_stats/5552300db4e358258973237475779313fc304021/test/xevtc_logs/renegade_solo.xevtc -------------------------------------------------------------------------------- /vcpkg-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg-configuration.schema.json", 3 | "default-registry": { 4 | "kind": "git", 5 | "repository": "https://github.com/microsoft/vcpkg", 6 | "baseline": "15d59ec83cc48b7fb9ecc1ef667c401ef4270fea" 7 | }, 8 | "registries": [ 9 | { 10 | "kind": "git", 11 | "repository": "https://github.com/Zinn-o-Matics/vcpkg-registry", 12 | "baseline": "6ceffbd8198b9c9e4a8418d01e7604acd5e81a49", 13 | "packages": [ "arcdps-extension", "arcdps-mock", "arcdps-unofficial-extras", "imgui" ] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json", 3 | "name": "arcdps-personal-stats", 4 | "version-string": "2.2rc2", 5 | "dependencies": [ 6 | "cpr", 7 | "grpc", 8 | "gtest", 9 | "jemalloc", 10 | "nlohmann-json", 11 | "prometheus-cpp", 12 | "spdlog", 13 | "imgui", 14 | "arcdps-mock", 15 | { 16 | "name": "arcdps-extension", 17 | "default-features": false, 18 | "features": [ 19 | "imgui" 20 | ] 21 | } 22 | ], 23 | "overrides": [ 24 | { 25 | "name": "c-ares", 26 | "version": "1.18.1" 27 | }, 28 | { 29 | "name": "curl", 30 | "version": "8.7.1" 31 | }, 32 | { 33 | "name": "cpr", 34 | "version": "1.10.5#2" 35 | }, 36 | { 37 | "name": "civetweb", 38 | "version": "1.15#1" 39 | }, 40 | { 41 | "name": "imgui", 42 | "version": "1.80#1" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /vcpkg_install_dependencies/vcpkg_install_dependencies.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Address Sanitizer 6 | x64 7 | 8 | 9 | Debug 10 | x64 11 | 12 | 13 | Release 14 | x64 15 | 16 | 17 | 18 | 16.0 19 | Win32Proj 20 | {8e446279-f584-484b-aecb-9e98e51d4b6f} 21 | vcpkginstalldependencies 22 | 10.0 23 | 24 | 25 | 26 | Application 27 | true 28 | v143 29 | Unicode 30 | 31 | 32 | Application 33 | false 34 | ClangCL 35 | Unicode 36 | 37 | 38 | Application 39 | false 40 | v143 41 | true 42 | Unicode 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | true 61 | 62 | 63 | true 64 | 65 | 66 | false 67 | 68 | 69 | true 70 | 71 | 72 | true 73 | 74 | 75 | true 76 | 77 | 78 | true 79 | Release 80 | 81 | 82 | 83 | 84 | 85 | Level3 86 | true 87 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 88 | true 89 | stdcpp23 90 | 91 | 92 | Console 93 | true 94 | 95 | 96 | 97 | 98 | Level3 99 | true 100 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 101 | true 102 | stdcpp23 103 | 104 | 105 | Console 106 | true 107 | 108 | 109 | 110 | 111 | Level3 112 | true 113 | true 114 | true 115 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 116 | true 117 | stdcpp23 118 | 119 | 120 | Console 121 | true 122 | true 123 | true 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /vcpkg_install_dependencies/vcpkg_install_dependencies.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | -------------------------------------------------------------------------------- /vcpkg_install_dependencies/vcpkg_install_dependencies.vcxproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /xmake.lua: -------------------------------------------------------------------------------- 1 | rule("protobuf") 2 | set_extensions(".proto") 3 | before_buildcmd_file(function (target, batchcmds, sourcefile, opt) 4 | local targetfile = path.filename(sourcefile) 5 | local basedir = sourcefile:sub(1, - (#targetfile + 1)) 6 | local basename = path.basename(sourcefile) 7 | local outputdir = target:autogendir() 8 | 9 | if not os.isdir(outputdir) then 10 | print("Creating output dir "..outputdir) 11 | os.mkdir(outputdir) 12 | end 13 | 14 | target:add("includedirs", outputdir) 15 | 16 | local grpc_cpp_plugin = "vcpkg_installed/x64-linux/x64-linux/tools/grpc/grpc_cpp_plugin" 17 | local protoc = "vcpkg_installed/x64-linux/x64-linux/tools/protobuf/protoc" 18 | 19 | batchcmds:show_progress(opt.progress, "${color.build.object}compiling.proto_source %s", sourcefile) 20 | batchcmds:vrunv(protoc, {"--cpp_out="..outputdir, "--grpc_out="..outputdir, "--plugin=protoc-gen-grpc="..grpc_cpp_plugin, "--proto_path="..basedir, targetfile}) 21 | 22 | local lowest_mtime = nil 23 | for _, extension in pairs({".pb.cc", ".pb.h", ".grpc.pb.cc", ".grpc.pb.h"}) do 24 | file = path.join(outputdir, basename..extension) 25 | 26 | local mtime = os.mtime(file) 27 | if (lowest_mtime == nil) or (mtime < lowest_mtime) then 28 | lowest_mtime = mtime 29 | end 30 | end 31 | 32 | local depcache = target:dependfile(sourcefile.."_source") 33 | batchcmds:add_depfiles(sourcefile, protoc) 34 | batchcmds:set_depmtime(lowest_mtime) 35 | batchcmds:set_depcache(depcache) 36 | 37 | --batchcmds:show("depcache "..depcache.." mtime "..lowest_mtime) 38 | end) 39 | on_buildcmd_file(function (target, batchcmds, sourcefile, opt) 40 | local targetfile = path.filename(sourcefile) 41 | local basedir = sourcefile:sub(1, - (#targetfile + 1)) 42 | local basename = path.basename(sourcefile) 43 | local outputdir = target:autogendir() 44 | 45 | local lowest_mtime = nil 46 | for _, extension in pairs({".pb.cc", ".grpc.pb.cc"}) do 47 | file = path.join(outputdir, basename..extension) 48 | 49 | local objectfile = target:objectfile(file) 50 | table.insert(target:objectfiles(), objectfile) 51 | 52 | batchcmds:show_progress(opt.progress, "${color.build.object}compiling.proto_object %s", file) 53 | batchcmds:compile(file, objectfile) 54 | 55 | local mtime = os.mtime(objectfile) 56 | if (lowest_mtime == nil) or (mtime < lowest_mtime) then 57 | lowest_mtime = mtime 58 | end 59 | end 60 | 61 | local depcache = target:dependfile(sourcefile.."_object") 62 | 63 | batchcmds:add_depfiles(sourcefile) 64 | batchcmds:set_depmtime(lowest_mtime) 65 | batchcmds:set_depcache(depcache) 66 | 67 | --batchcmds:show("depcache "..depcache.." mtime "..lowest_mtime) 68 | end) 69 | 70 | target("evtc_rpc_server") 71 | add_rules("protobuf") 72 | 73 | set_kind("binary") 74 | set_warnings("all") 75 | set_languages("c++20") 76 | local toolset = "clang++" 77 | set_toolset("cxx", toolset) 78 | set_toolset("ld", toolset) 79 | 80 | compilerflags = {} 81 | if is_mode("debug") then 82 | add_options("debug") 83 | add_defines("_DEBUG") 84 | 85 | elseif is_mode("asan") then 86 | set_optimize("none") 87 | add_defines("_DEBUG") 88 | table.insert(compilerflags, "-fsanitize=address") 89 | add_ldflags("-fsanitize=address") 90 | 91 | elseif is_mode("tsan") then 92 | set_optimize("none") 93 | add_defines("_DEBUG") 94 | table.insert(compilerflags, "-fsanitize=thread") 95 | add_ldflags("-fsanitize=thread") 96 | 97 | elseif is_mode("ubsan") then 98 | set_optimize("none") 99 | add_defines("_DEBUG") 100 | table.insert(compilerflags, "-fsanitize=undefined") 101 | table.insert(compilerflags, "-fno-sanitize=alignment") 102 | add_ldflags("-fsanitize=undefined") 103 | --add_ldflags("-fno-sanitize=alignment") 104 | 105 | elseif is_mode("release") then 106 | set_optimize("fastest") 107 | add_defines("NDEBUG") 108 | add_cxxflags("-flto") 109 | add_ldflags("-flto") 110 | add_ldflags("-fuse-linker-plugin") 111 | 112 | elseif is_mode("profiling") then 113 | set_optimize("fastest") 114 | add_defines("NDEBUG") 115 | add_cxxflags("-flto") 116 | add_ldflags("-flto") 117 | add_ldflags("-fuse-linker-plugin") 118 | add_cxxflags("-pg") 119 | add_ldflags("-pg") 120 | 121 | end 122 | 123 | add_defines("LINUX") 124 | 125 | add_syslinks("pthread") 126 | 127 | add_includedirs("modules/arcdps_extension", "vcpkg_installed/x64-linux/x64-linux/include") 128 | add_linkdirs("vcpkg_installed/x64-linux/x64-linux/lib") 129 | 130 | -- Add everything absl as a group since they have circular dependencies (doesn't seem to be the case anymore?) 131 | add_linkgroups( 132 | "absl_failure_signal_handler", 133 | "absl_flags_usage", 134 | "absl_crc32c", 135 | "absl_spinlock_wait", 136 | "absl_log_internal_nullguard", 137 | "absl_malloc_internal", 138 | "absl_cordz_functions", 139 | "absl_strings_internal", 140 | "absl_random_internal_randen_slow", 141 | "absl_hash", 142 | "absl_hashtablez_sampler", 143 | "absl_random_seed_sequences", 144 | "absl_log_flags", 145 | "absl_vlog_config_internal", 146 | "absl_demangle_internal", 147 | "absl_bad_variant_access", 148 | "absl_crc_internal", 149 | "absl_random_internal_randen_hwaes_impl", 150 | "absl_flags_parse", 151 | "absl_time_zone", 152 | "absl_flags_marshalling", 153 | "absl_log_internal_check_op", 154 | "absl_random_internal_seed_material", 155 | "absl_low_level_hash", 156 | "absl_log_severity", 157 | "absl_log_internal_format", 158 | "absl_periodic_sampler", 159 | "absl_raw_logging_internal", 160 | "absl_cordz_sample_token", 161 | "absl_civil_time", 162 | "absl_graphcycles_internal", 163 | "absl_leak_check", 164 | "absl_log_internal_globals", 165 | "absl_string_view", 166 | "absl_symbolize", 167 | "absl_examine_stack", 168 | "absl_random_internal_pool_urbg", 169 | "absl_log_internal_proto", 170 | "absl_random_internal_platform", 171 | "absl_random_internal_distribution_test_util", 172 | "absl_flags_usage_internal", 173 | "absl_flags_commandlineflag", 174 | "absl_int128", 175 | "absl_synchronization", 176 | "absl_scoped_set_env", 177 | "absl_time", 178 | "absl_status", 179 | "absl_random_internal_randen_hwaes", 180 | "absl_cord", 181 | "absl_base", 182 | "absl_log_sink", 183 | "absl_log_initialize", 184 | "absl_log_globals", 185 | "absl_flags_commandlineflag_internal", 186 | "absl_log_internal_message", 187 | "absl_random_distributions", 188 | "absl_random_internal_randen", 189 | "absl_strings", 190 | "absl_strerror", 191 | "absl_flags_config", 192 | "absl_log_internal_conditions", 193 | "absl_str_format_internal", 194 | "absl_log_internal_log_sink_set", 195 | "absl_flags_program_name", 196 | "absl_die_if_null", 197 | "absl_debugging_internal", 198 | "absl_cordz_info", 199 | "absl_crc_cord_state", 200 | "absl_bad_any_cast_impl", 201 | "absl_cord_internal", 202 | "absl_kernel_timeout_internal", 203 | "absl_raw_hash_set", 204 | "absl_throw_delegate", 205 | "absl_statusor", 206 | "absl_stacktrace", 207 | "absl_cordz_handle", 208 | "absl_random_seed_gen_exception", 209 | "absl_flags_internal", 210 | "absl_flags_reflection", 211 | "absl_exponential_biased", 212 | "absl_city", 213 | "absl_bad_optional_access", 214 | "absl_log_entry", 215 | "absl_crc_cpu_detect", 216 | "absl_flags_private_handle_accessor", 217 | "absl_log_internal_fnmatch", 218 | {name = "absl", group = false, static = true}) 219 | 220 | -- Add all libraries from vcpkg (just mined from a directory listing, excluding the absl libraries above). Then libraries removed one by one and checking if it still links. 221 | add_linkgroups( 222 | "jemalloc", 223 | "upb", 224 | "address_sorting", 225 | "gpr", 226 | "prometheus-cpp-core", 227 | "grpc++", 228 | "prometheus-cpp-pull", 229 | "crypto", 230 | "ssl", 231 | "cares", 232 | "re2", 233 | "fmt", 234 | "upb_json", 235 | "z", 236 | "protobuf", 237 | "civetweb-cpp", 238 | "upb_textformat", 239 | "upb_mini_table", 240 | "upb_collections", 241 | "upb_utf8_range", 242 | "upb_fastdecode", 243 | "upb_reflection", 244 | "upb_extension_registry", 245 | "spdlog", 246 | "civetweb", 247 | "grpc", 248 | {name = "others", group = false, static = true}) 249 | 250 | add_files("src/Log.cpp", {cxxflags = compilerflags}) 251 | add_files("evtc_rpc_server/**.cpp", {cxxflags = compilerflags}) 252 | add_files("networking/**.cpp", {cxxflags = compilerflags}) 253 | add_files("networking/**.proto") 254 | 255 | add_cxxflags("-fPIC") 256 | add_cxxflags("-ggdb3") 257 | add_cxxflags("-march=native") 258 | add_cxxflags("-Wextra", "-Weffc++", "-pedantic") 259 | add_cxxflags("-Werror=ignored-qualifiers", "-Werror=return-type") 260 | if toolset == "clang++" then 261 | add_cxxflags("-Werror=shadow") 262 | add_cxxflags("-Werror=duplicate-decl-specifier") 263 | end 264 | add_cxxflags("-Wno-format") -- unsigned long long vs unsigned long issues (linux is stupid...) 265 | add_cxxflags("-Wno-gnu-zero-variadic-macro-arguments", "-Wno-format-pedantic") 266 | add_ldflags("-fuse-ld=lld") 267 | --------------------------------------------------------------------------------