commit dc38b57122a51eade88e56fab8ca8752ba846237 Author: avitasia Date: Wed Sep 3 12:56:59 2025 -0700 init diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..accc882 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Debug logs** +Please include the relevant nampower_debug.log which will be in the same directory as WoW.exe. It helps significantly with figuring out what happened. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Screenshots** +If applicable, add screenshots to help explain your problem. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 0000000..48d5f81 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/msvc_cmake.yml b/.github/workflows/msvc_cmake.yml new file mode 100644 index 0000000..7baa6e8 --- /dev/null +++ b/.github/workflows/msvc_cmake.yml @@ -0,0 +1,56 @@ +name: CMake + +on: + push: + +env: + BUILD_TYPE: Release + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + + - name: Download hadesmem release + run: | + Invoke-WebRequest https://github.com/namreeb/hadesmem/releases/download/v1.7.0/hadesmem-v142-Release-Win32.zip -OutFile hadesmem.zip + Expand-Archive -Path hadesmem.zip -DestinationPath . + + - name: Build Boost + run: | + Invoke-WebRequest https://boostorg.jfrog.io/artifactory/main/release/1.78.0/source/boost_1_78_0.zip -OutFile boost.zip + Expand-Archive -Path boost.zip -DestinationPath . + cd boost_1_78_0 + .\bootstrap + .\b2 --with-filesystem --with-program_options link=static threading=multi runtime-link=shared architecture=x86 address-model=32 stage + + - name: Configure CMake + run: cmake -A Win32 -B ${{github.workspace}}/build -DBOOST_ROOT=boost_1_78_0 -DHADESMEM_ROOT=hadesmem-v142-Release-Win32 -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=${{github.workspace}}/artifact + + - name: Build + run: | + cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + cmake --install ${{github.workspace}}/build + + - name: Publish artifact + uses: actions/upload-artifact@v3 + with: + name: artifact + path: ${{ github.workspace }}/artifact/* + + - name: Setup release + working-directory: ${{env.GITHUB_WORKSPACE}} + if: startsWith(github.ref, 'refs/tags/') + run: | + move artifact nampower-${{ github.ref_name }} + Compress-Archive nampower-${{ github.ref_name }} nampower-${{ github.ref_name }}.zip + + - uses: softprops/action-gh-release@v1 + name: Upload assets to release + if: startsWith(github.ref, 'refs/tags/') + with: + draft: true + files: nampower-${{ github.ref_name }}.zip + fail_on_unmatched_files: true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ead3702 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/.vs/* +/*.sdf +/loader/*.aps +/CMakeSettings.json +/out +/.idea/ +/.vscode/ +/build/ +/loader/RCa22240 +/cmake-build*/ +/nampower/cmake-build*/ +.claude/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..61f9170 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,105 @@ +cmake_minimum_required(VERSION 3.12) + +foreach(policy + CMP0074 # CMake 3.12 +) + if(POLICY ${policy}) + cmake_policy(SET ${policy} NEW) + endif() +endforeach() + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +set(PROJECT_NAME nampower) + +project(${PROJECT_NAME}) + +# build in Release-mode by default if not explicitly set +if( NOT CMAKE_BUILD_TYPE ) + set(CMAKE_BUILD_TYPE "RelWithDebInfo") +endif() +set(BOOST_ROOT "C:/software/boost_1_80_0/boost") +set(BOOST_INCLUDEDIR "C:/software/boost_1_80_0") +set(BOOST_LIBRARYDIR "C:/software/boost_1_80_0/lib32-msvc-14.3") +set (Boost_DETAILED_FAILURE_MSG ON) +set (Boost_DEBUG ON) + +set(Boost_USE_STATIC_LIBS ON) +set(Boost_USE_MULTITHREADED ON) +set(Boost_USE_STATIC_RUNTIME OFF) + +find_package(Boost REQUIRED COMPONENTS filesystem program_options) + +if (Boost_FOUND) + message(STATUS "boost found at ${Boost_INCLUDE_DIR}. Library dir: ${Boost_LIBRARY_DIR_DEBUG}") +else() + message(FATAL_ERROR "boost not found") +endif() + +if ("x_${CMAKE_BUILD_TYPE}" STREQUAL "x_Debug") + set(HADESMEM_ROOT "C:/software/hadesmem-v142-Debug-Win32") + set(HADESMEM_BUILD "Debug") + link_directories("${Boost_LIBRARY_DIR_DEBUG}") +else() + set(HADESMEM_ROOT "C:/software/hadesmem-v142-Release-Win32") + set(HADESMEM_BUILD "Release") + link_directories("${Boost_LIBRARY_DIR_RELEASE}") +endif() + +if (CMAKE_SIZEOF_VOID_P EQUAL 8) + message(FATAL_ERROR "64-bit unsupported. nampower integrates with the 1.12.1 client which is only 32 bit") +else() + set(HADESMEM_ARCH "Win32") +endif() + +if ("x_${HADESMEM_ROOT}" STREQUAL "x_") + if (EXISTS "${CMAKE_SOURCE_DIR}/hadesmem-v${MSVC_TOOLSET_VERSION}-${HADESMEM_BUILD}-${HADESMEM_ARCH}") + set(HADESMEM_ROOT "${CMAKE_SOURCE_DIR}/hadesmem-v${MSVC_TOOLSET_VERSION}-${HADESMEM_BUILD}-${HADESMEM_ARCH}") + set(HADESMEM_LIB_DIR "${HADESMEM_ROOT}/lib") + else() + message(FATAL_ERROR "HADESMEM_ROOT not set. ${PROJECT_NAME} requires hadesmem, available at https://github.com/namreeb/hadesmem") + endif() +else() + set(HADESMEM_LIB_DIR "${HADESMEM_ROOT}/lib") +endif() + +if (NOT EXISTS "${HADESMEM_ROOT}/include/memory/hadesmem") + message(FATAL_ERROR "hadesmem not found at ${HADESMEM_ROOT}") +else() + message(STATUS "hadesmem found at ${HADESMEM_ROOT}") +endif() + +message(STATUS "hadesmem library directory: ${HADESMEM_LIB_DIR}") + +# threading library is required +find_package(Threads REQUIRED) + +add_definitions(-DUNICODE -D_UNICODE -D_SCL_SECURE_NO_WARNINGS -D_CRT_SECURE_NO_WARNINGS -DASMJIT_STATIC -DASMJIT_BUILD_X86 -DHADESMEM_NO_PUGIXML) +include_directories( + "Include" + "${CMAKE_CURRENT_SOURCE_DIR}" + "${Boost_INCLUDE_DIR}" + "${HADESMEM_ROOT}/include/memory/" + "${HADESMEM_ROOT}/deps/udis86/udis86" + "${HADESMEM_ROOT}/deps/asmjit/asmjit/src" +) + +link_directories("${HADESMEM_LIB_DIR}") + +# Set static runtime library +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd") + +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /NODEFAULTLIB:MSVCRT /NODEFAULTLIB:MSVCRTD") + +# Link static libraries +add_subdirectory(loader) +add_subdirectory(nampower) + +install(FILES + "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.txt" + "${CMAKE_CURRENT_SOURCE_DIR}/README.md" + DESTINATION "${CMAKE_INSTALL_PREFIX}") \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..15fc1c0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) 2017-2023, namreeb (legal@namreeb.org) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. diff --git a/README.md b/README.md new file mode 100644 index 0000000..149f039 --- /dev/null +++ b/README.md @@ -0,0 +1,490 @@ + Checkout the list button above this to easily navigate the readme + +# v2.0.0 Changes + +Added spell queuing, automatic retry on error, and quickcasting with lots of customization. + +Some other key improvements over Namreeb's version: +- Using a buffer to avoid server rejections from casting too quickly (namreeb's uses 0 buffer). See 'Why do I need a buffer?' below for more info. +- Using high_resolution_clock instead of GetTickCount for faster timing on when to start casts +- Fix broken cast animations when casting spells back to back + +### Compatability with other addons +Queuing can cause issues with some addons that also manage spell casting. Quickheal/Healbot/Quiver do not work well with queuing. Check github issues for other potential incompatibilities. +If someone rewrites these addons to use guids from superwow that would likely fix all issues. + +Additionally, if you use pfui mouseover macros there is a timing issue that can occur causing it to target yourself instead of your mouseover target. If you have superwow and latest pfui issue is fixed. + +Notgrid mouseover needs to be updated to take advantage of superwow the default version won't work well with queuing. + +If you use healcomm can replace all instances of it in your addons with this version to work well with queuing https://github.com/MarcelineVQ/LunaUnitFrames/blob/TurtleWoW/libs/HealComm-1.0/HealComm-1.0.lua (requires superwow). + +If all else fails can turn off queuing for a specific macro like so depending on the spell being cast: +``` +/run SetCVar("NP_QueueCastTimeSpells", "0") +/run SetCVar("NP_QueueInstantSpells", "0") +/pfcast YOUR_SPELL +/run SetCVar("NP_QueueCastTimeSpells", "1") +/run SetCVar("NP_QueueInstantSpells", "1") +``` +### Installation +Grab the latest nampower.dll from https://gitea.com/avitasia/nampower/releases and place in the same directory as WoW.exe. You can also get the helper addon mentioned below and place that in Interface/Addons. + +You will need launch the game with a launcher like Vanillafixes https://github.com/hannesmann/vanillafixes or Unitxp https://github.com/allfoxwy/UnitXP_SP3 to actually have the nampower dll get loaded. + +If you would prefer to compile yourself you will need to get: +- boost 1.80 32 bit from https://www.boost.org/users/history/version_1_80_0.html +- hadesmem from https://github.com/namreeb/hadesmem + +CMakeLists.txt is currently looking for boost at `set(BOOST_INCLUDEDIR "C:/software/boost_1_80_0")` and hadesmem at `set(HADESMEM_ROOT "C:/software/hadesmem-v142-Debug-Win32")`. Edit as needed. + +### Configuration + +#### Configure with addon +There is a companion addon to make it easy to check/change the settings in game. You can download it here - [nampowersettings](https://gitea.com/avitasia/nampowersettings). + +#### Manual Configuration +The following CVars control the behavior of the spell queuing system: + +You can access CVars in game with `/run DEFAULT_CHAT_FRAME:AddMessage(GetCVar("CVarName"))`
+and set them with `/run SetCVar("CVarName", "Value")` + +You can also just place them in your Config.wtf file in your WTF folder. If they are the default value they will not be written to the file. +Example: +``` +SET EnableMusic "0" +SET MasterSoundEffects "0" + +SET NP_QuickcastTargetingSpells "1" +SET NP_SpellQueueWindowMs "1000" +SET NP_TargetingQueueWindowMs "1000" +``` + +- `NP_QueueCastTimeSpells` - Whether to enable spell queuing for spells with a cast time. 0 to disable, 1 to enable. Default is 1. +- `NP_QueueInstantSpells` - Whether to enable spell queuing for instant cast spells tied to gcd. 0 to disable, 1 to enable. Default is 1. +- `NP_QueueChannelingSpells` - Whether to enable channeling spell queuing as well as whether to allow any queuing during channels. 0 to disable, 1 to enable. Default is 1. +- `NP_QueueTargetingSpells` - Whether to enable terrain targeting spell queuing. 0 to disable, 1 to enable. Default is 1. +- `NP_QueueOnSwingSpells` - Whether to enable on swing spell queuing. 0 to disable, 1 to enable. Default is 0 (changed with 1.17.2 due to changes to on swing spells). +- `NP_QueueSpellsOnCooldown` - Whether to enable queuing for spells coming off cooldown. 0 to disable, 1 to enable. Default is 1. + +- `NP_InterruptChannelsOutsideQueueWindow` - Whether to allow interrupting channels (the original client behavior) when trying to cast a spell outside the channeling queue window. Default is 0. + +- `NP_SpellQueueWindowMs` - The window in ms before a cast finishes where the next will get queued. Default is 500. +- `NP_OnSwingBufferCooldownMs` - The cooldown time in ms after an on swing spell before you can queue on swing spells. Default is 500. +- `NP_ChannelQueueWindowMs` - The window in ms before a channel finishes where the next will get queued. Default is 1500. +- `NP_TargetingQueueWindowMs` - The window in ms before a terrain targeting spell finishes where the next will get queued. Default is 500. +- `NP_CooldownQueueWindowMs` - The window in ms of remaining cooldown where a spell will get queued instead of failing with 'Spell not Ready Yet'. Default is 250. + + +- `NP_MinBufferTimeMs` - The minimum buffer delay in ms added to each cast (covered more below). The dynamic buffer adjustments will not go below this value. Default is 55. +- `NP_NonGcdBufferTimeMs` - The buffer delay in ms added AFTER each cast that is not tied to the gcd. Default is 100. +- `NP_MaxBufferIncreaseMs` - The maximum amount of time in ms to increase the buffer by when the server rejects a cast. This prevents getting too long of a buffer if you happen to get a ton of rejections in a row. Default is 30. + + +- `NP_RetryServerRejectedSpells` - Whether to retry spells that are rejected by the server for these reasons: SPELL_FAILED_ITEM_NOT_READY, SPELL_FAILED_NOT_READY, SPELL_FAILED_SPELL_IN_PROGRESS. 0 to disable, 1 to enable. Default is 1. +- `NP_QuickcastTargetingSpells` - Whether to enable quick casting for ALL spells with terrain targeting. This will cause the spell to instantly cast on your cursor without waiting for you to confirm the targeting circle. Queuing targeting spells will use quickcasting regardless of this value (couldn't get it to work without doing this). 0 to disable, 1 to enable. Default is 0. +- `NP_ReplaceMatchingNonGcdCategory` - Whether to replace any queued non gcd spell when a new non gcd spell with the same StartRecoveryCategory is cast (more explanation below). 0 to disable, 1 to enable. Default is 0. +- `NP_OptimizeBufferUsingPacketTimings` - Whether to attempt to optimize your buffer using your latency and server packet timings (more explanation below). 0 to disable, 1 to enable. Default is 0. + +- `NP_PreventRightClickTargetChange` - Whether to prevent right-clicking from changing your current target when in combat. If you don't have a target right click will still change your target even with this on. This is mainly to prevent accidentally changing targets in combat when trying to adjust your camera. 0 to disable, 1 to enable. Default is 0. + +- `NP_DoubleCastToEndChannelEarly` - Whether to allow double casting a spell within 350ms to end channeling on the next tick. Takes into account your ChannelLatencyReductionPercentage. 0 to disable, 1 to enable. Default is 0. + +- `NP_ChannelLatencyReductionPercentage` - The percentage of your latency to subtract from the end of a channel duration to optimize cast time while hopefully not losing any ticks (more explanation below). Default is 75. + +- `NP_NameplateDistance` - The distance in yards to display nameplates. Defaults to whatever was set by the game or vanilla tweaks. + +### Custom Lua Functions + +#### QueueSpellByName(spellName) +Will force queue a spell regardless of the appropriate queue window. If no spell is currently being cast it will be cast immediately. +For example can make a macro with +``` +/run QueueSpellByName("Frostbolt");QueueSpellByName("Frostbolt") +``` +to cast 2 frostbolts in a row. Currently, can only queue 1 GCD spell at a time and 5 non gcd spells. This means you can't do 3 frostbolts in a row with one macro. + +#### CastSpellByNameNoQueue(spellName) +Will force a spell cast to never queue even if your settings would normally queue. Can be used to fix addons that don't work with queued spells. + +#### QueueScript(script, [priority]) +Queues any arbitrary script using the same logic as a regular spell using NP_SpellQueueWindowMs as the window. If no spell is being cast and you are not on the gcd the script will be run immediately. + +Priority is optional and defaults to 1. +Priority 1 means the script will run before any other queued spells. +Priority 2 means the script will run after any queued non gcd spells but before any queued normal spells. +Priority 3 means the script will run after any type of queued spells. + +Convert slash commands from other addons like `/equip` to their function form `SlashCmdList.EQUIP` to use them inside QueueScript. + +For example, you can equip a libram before casting a queued heal using +``` +/run QueueScript('SlashCmdList.EQUIP("Libram of +heal")') +``` + +#### IsSpellInRange(spellName, [target]) or IsSpellInRange(spellId, [target]) +Takes a spell name or spell id and an optional target. Target can the usual UNIT tokens like "player", "target", "mouseover", etc or a unit guid. + +If using spell name it must be a spell you have in your spellbook. If using spell id it can be any spell id. + +Returns 1 if the spell is in range, 0 if not in range, and -1 if the spell is not valid for this check (must be TARGET_UNIT_PET, TARGET_UNIT_TARGET_ENEMY, TARGET_UNIT_TARGET_ALLY, TARGET_UNIT_TARGET_ANY). +This is because this uses the same underlying function as `IsActionInRange` which returns 1 for spells that are not single target which can be misleading. + +Examples: +``` +/run local result=IsSpellInRange("Frostbolt"); if result == 1 then print("In range") else if result == 0 then print("Out of range") else print("Not single target") end +``` + +#### IsSpellUsable(spellName) or IsSpellUsable(spellId) +Takes a spell name or spell id. + +Usable does not equal castable. This is most often used to check if a reactive spell is usable. + +If using spell name it must be a spell you have in your spellbook. If using spell id it can be any spell id. + +Returns: + +1st param: 1 if the spell is usable, 0 if not usable. +2nd param: Always 0 if spell is not usable for a different reason other than mana. 1 if out of mana, 0 if not out of mana. + +Examples: +``` +/run local result=IsSpellUsable("Frostbolt"); if result == 1 then print("Frostbolt usable") else print("Frostbolt not usable") end +``` + +#### GetCurrentCastingInfo() +Returns: + +1st param: Casting spell id or 0 +2nd param: Visual spell id or 0. This won't always get cleared after a spell finishes. +3rd param: Auto repeating spell id or 0. +4th param: 1 if casting spell with a cast time, 0 if not. +5th param: 1 if channeling, 0 if not. +6th param: 1 if on swing spell is pending, 0 if not. +7th param: 1 if auto attacking, 0 if not. + +For normal spells these will be the same. For some spells like auto-repeating and channeling spells only the visual spell id will be set. + +Examples: +``` +/run local castId,visId,autoId,casting,channeling,onswing,autoattack=GetCurrentCastingInfo();print(castId);print(visId);print(autoId);print(casting);print(channeling);print(onswing);print(autoattack); +``` + +#### GetSpellIdForName(spellName) +Returns: + +1st param: the max rank spell id for a spell name if it exists in your spellbook. Returns 0 if the spell is not in your spellbook. + +Examples: +``` +/run local spellId=GetSpellIdForName("Frostbolt");print(spellId) +/run local spellId=GetSpellIdForName("Frostbolt(Rank 1)");print(spellId) +``` + +#### GetSpellNameAndRankForId(id) +Returns: + +1st param: the spell name for a spell id +2nd param: the spell rank for a spell id as a string such as "Rank 1" + +Examples: +``` +/run local spellName,spellRank=GetSpellNameAndRankForId(116);print(spellName);print(spellRank) +prints "Frostbolt" and "Rank 1" +``` + +#### GetSpellSlotTypeIdForName(spellName) +Returns: + +1st param: the 1 indexed (lua calls expect this) spell slot number for a spell name if it exists in your spellbook. Returns 0 if the spell is not in your spellbook. +2nd param: the book type of the spell, either "spell", "pet" or "unknown". +3rd param: the spell id of the spell. Returns 0 if the spell is not in your spellbook. + +Examples: +``` +/run local slot, bookType, spellId=GetSpellSlotTypeIdForName("Frostbolt");print(slot);print(bookType);print(spellId) +``` + +#### GetNampowerVersion() +Returns the current version of Nampower split into major, minor and patch numbers. + +So if version was v2.8.6 it would return 2, 8, 6 as integers. + +Examples: +``` +/run local major, minor, patch=GetNampowerVersion();print(major);print(minor);print(patch) +``` + +The previous version of this `GetSpellSlotAndTypeForName` was removed as it was returning a 0 indexed slot number which was confusing to use in lua. + +#### GetItemLevel(itemId) +Returns the item level of an item. Returns an error if the item id is invalid. + +Examples: +``` +/run local itemLevel=GetItemLevel(22589);print(itemLevel) +should print 90 for atiesh +``` + +#### ChannelStopCastingNextTick() +Will stop channeling early on the next tick if you have queue channeling spells enabled and try to cast a spell before the next tick (didn't know how to cancel channels without casting another spell). Uses your ChannelLatencyReductionPercentage to determine when to stop the channel. + +### Custom Events + +#### SPELL_QUEUE_EVENT +I've added a new event you can register in game to get updates when spells are added and popped from the queue. + +The event is `SPELL_QUEUE_EVENT` and has 2 parameters: +1. int eventCode - see below +2. int spellId + +Possible Event codes: +``` + ON_SWING_QUEUED = 0 + ON_SWING_QUEUE_POPPED = 1 + NORMAL_QUEUED = 2 + NORMAL_QUEUE_POPPED = 3 + NON_GCD_QUEUED = 4 + NON_GCD_QUEUE_POPPED = 5 +``` + +Example from NampowerSettings: +``` +local ON_SWING_QUEUED = 0 +local ON_SWING_QUEUE_POPPED = 1 +local NORMAL_QUEUED = 2 +local NORMAL_QUEUE_POPPED = 3 +local NON_GCD_QUEUED = 4 +local NON_GCD_QUEUE_POPPED = 5 + +local function spellQueueEvent(eventCode, spellId) + if eventCode == NORMAL_QUEUED or eventCode == NON_GCD_QUEUED then + local _, _, texture = SpellInfo(spellId) -- superwow function + Nampower.queued_spell.texture:SetTexture(texture) + Nampower.queued_spell:Show() + elseif eventCode == NORMAL_QUEUE_POPPED or eventCode == NON_GCD_QUEUE_POPPED then + Nampower.queued_spell:Hide() + end +end + +NampowerSettings:RegisterEvent("SPELL_QUEUE_EVENT", spellQueueEvent) +``` + +#### SPELL_CAST_EVENT +New event you can register in game to get updates when spells are cast with some additional information. + +The event is `SPELL_CAST_EVENT` and has 5 parameters: +1. int success - 1 if cast succeeded, 0 if failed +2. int spellId +3. int castType - see below +4. string targetGuid - guid string like "0xF5300000000000A5" +5. int itemId - the id of the item that triggered the spell, 0 if it wasn't triggered by an item + +Possible Cast Types: +``` +NORMAL=1 +NON_GCD=2 +ON_SWING=3 +CHANNEL=4 +TARGETING=5 (targeting is the term I used for spells with terrain targeting) +TARGETING_NON_GCD=6 +``` + +targetGuid will be "0x000000000" unless an explicit target is specified which currently only happens in 2 circumstances: +- It was specified as the 2nd param of CastSpellByName (added by superwow) +- Mouseover casts that use SpellTargetUnit to specify a target + +Example (uses ace RegisterEvent): +``` +Cursive:RegisterEvent("SPELL_CAST_EVENT", function(success, spellId, castType, targetGuid, itemId) + print(success) + print(spellId) + print(castType) + print(targetGuid) + print(itemId) +end); +``` + +#### SPELL_DAMAGE_EVENT_SELF and SPELL_DAMAGE_EVENT_OTHER +New events you can register in game to get updates whenever spell damage occurs. SPELL_DAMAGE_EVENT_SELF will only trigger for damage you deal, while SPELL_DAMAGE_EVENT_OTHER will only trigger for damage dealt by others. + +Both of these events have the following parameters: +1. string targetGuid - guid string like "0xF5300000000000A5" +2. string casterGuid - guid string like "0xF5300000000000A5" +3. int spellId +4. int amount - the amount of damage dealt. If the 4th value in effectAuraStr is 89 (SPELL_AURA_PERIODIC_DAMAGE_PERCENT) I believe this is the percentage of health lost. +5. string mitigationStr - comma separated string containing "aborb,block,resist" amounts +6. int hitInfo - see below but generally 0 unless the spell was a crit in which case it will be 2 +7. int spellSchool - the damage school of the spell, see below +8. string effectAuraStr - comma separated string containing the three spell effect numbers and the aura type (usually means a Dot but not all Dots will have an aura type) if applicable. So "effect1,effect2,effect3,auraType" + +Spell hit info enum: https://github.com/vmangos/core/blob/94f05231d4f1b160468744d4caa398cf8b337c48/src/game/Spells/SpellDefines.h#L109 + +Spell school enum: https://github.com/vmangos/core/blob/94f05231d4f1b160468744d4caa398cf8b337c48/src/game/Spells/SpellDefines.h#L641 + +Spell effect enum: https://github.com/vmangos/core/blob/94f05231d4f1b160468744d4caa398cf8b337c48/src/game/Spells/SpellDefines.h#L142 + +Aura type enum: https://github.com/vmangos/core/blob/94f05231d4f1b160468744d4caa398cf8b337c48/src/game/Spells/SpellAuraDefines.h#L43 + +Example (uses ace RegisterEvent): +``` +Cursive:RegisterEvent("SPELL_DAMAGE_EVENT_SELF", + function(targetGuidStr, + casterGuidStr, + spellId, + amount, + mitigationStr, + hitInfo, + spellSchool, + effectAuraStr) + print(targetGuidStr .. " " .. casterGuidStr .. " " .. tostring(spellId) .. " " .. tostring(amount) .. " " .. tostring(spellSchool) .. " " .. mitigationStr .. " " .. hitInfo .. " " .. effectAuraStr) + end); +``` + +### Bug Reporting +If you encounter any bugs please report them in the issues tab. Please include the nampower_debug.txt file in the same directory as your WoW.exe to help me diagnose the issue. If you are able to reproduce the bug please include the steps to reproduce it. In a future version once bugs are ironed out I'll make logging optional. + +### FAQ & Additional Info + +#### How does queuing work? +Trying to cast a spell within the appropriate window before your current spell finishes will queue your new spell. +The spell will be cast as soon as possible after the current spell finishes. + +There are separate configurable queue windows for: +- Normal spells +- On swing spells (the window functions as a cooldown instead where you cannot immediately double queue on swing spells so that I don't have to track swing timers) +- Channeling spells +- Spells with terrain targeting + +There are 3 separate queues for the following types of spells: GCD(max size:1), non GCD(max size:6), and on-hit(max size:1). + +Additionally the queuing system will ignore spells with any of the following attributes/effects to avoid issues with tradeskills/enchants/other out of combat activities: +- SpellAttributes::SPELL_ATTR_TRADESPELL +- SpellEffects::SPELL_EFFECT_TRADE_SKILL +- SpellEffects::SPELL_EFFECT_ENCHANT_ITEM +- SpellEffects::SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY +- SpellEffects::SPELL_EFFECT_CREATE_ITEM +- SpellEffects::SPELL_EFFECT_OPEN_LOCK +- SpellEffects::SPELL_EFFECT_OPEN_LOCK_ITEM + +#### Why do I need a buffer? +From my own testing it seems that a buffer is required on spells to avoid "This ability isn't ready yet"/"Another action in progress" errors. +By that I mean that if you cast a 1.5 second cast time spell every 1.5 seconds without your ping changing you will occasionally get +errors from the server and your cast will get rejected. If you have 150ms+ ping this can be very punishing. + +I believe this is related to the server tick during which incoming spells are processed. There is logic to +subtract the server processing time from your gcd in vmangos but other servers do not appear to be doing this. + +To compensate for what seems to be a 50ms server tick the default buffer in nampower.cfg is 55ms. If you are close to the server +you can experiment with lowering this value. You will occasionally get errors but if they are infrequent enough for you +the time saved will be worth it. + +Non gcd spells also seem to be affected by this. I suspect that only one spell can be processed per server tick. +This means that if you try to cast 2 non gcd spells in the same server tick only one will be processed. +To avoid this happening there is `NP_NonGcdBufferTimeMs` which is added after each non gcd spell. There might be more to +it than this as using the normal buffer of 55ms was still resulting in skipped casts for me. I found 100ms to be a safe value. + +#### GCD Spells +Only one gcd spell can be queued at a time. Pressing a new gcd spell will replace any existing queued gcd spell. + +As of 5/13/2025 the server tick is now subtracted from the gcd timer so a buffer is no longer required for spells with a cast time at least ~50ms less than their gcd :) + +#### Non GCD Spells +Non gcd spells have special handling. You can queue up to 6 non gcd spells, +and they will execute in the order queued with `NP_NonGcdBufferTimeMs` delay after each of them to help avoid server rejection. +The non gcd queue always has priority over queued normal spells. +You can only queue a given spellId once in the non gcd queue, any subsequent attempts will just replace the existing entry in the queue. + +`NP_ReplaceMatchingNonGcdCategory` will cause non gcd spells with the same non zero StartRecoveryCategory to replace each other in the queue. +The vast majority of spells not on the gcd have category '0' so it ignores them to avoid causing issues. +One notable exception is shaman totems that were changed to have separate categories according to their elements. + +This can be useful if you want to change your mind about the non gcd spell you have queued. For example, if you queue a mana potion and decide you want to use LIP instead last minute. + +#### On hit Spells +Only one on hit spell can be queued at a time. Pressing a new on hit spell will replace any existing queued on hit spell. +On hit spells have no effect on the gcd or non gcd queues as they are handled entirely separately and are resolved by your auto attack. + +#### Channeling Spells +Channeling spells function differently than other spells in that the channel in the client actually begins when you receive +the CHANNEL_START packet from the server. This means the client channel is happening 1/2 your latency after the server channel +and that server tick delay is already included in the cast, whereas regular spells are the other way around (the client is ahead of the server). + +From my testing it seems that you can usually subtract your full latency from the end of the channel duration without losing +any ticks. Since your latency can vary it is safer to do a percentage of your latency instead to minimize the chance of +having a tick cut off. This is controlled by the cvar `NP_ChannelLatencyReductionPercentage` which defaults to 75. + +Channeling spells can be interrupted outside the channel queue window by casting any spell if `NP_InterruptChannelsOutsideQueueWindow` is set to 1. During the channel queue window +you cannot interrupt the channel unless you turn off `NP_QueueChannelingSpells`. You can always move to interrupt a channel at any time. + +#### Spells on Cooldown +If using `NP_QueueSpellsOnCooldown` when you attempt to cast a spell that has a remaining cooldown of less than `NP_CooldownQueueWindowMs` it will be queued instead of failing with 'Spell not Ready Yet'. +There is a separate queue of size 1 for normal spells and non gcd spells. If something is in either of these cooldown queues and you try to cast a spell that is not on cooldown it will be cast immediately and clear the appropriate cooldown queue. + +For example, if Fire Blast is on cooldown and I queue it and then try to cast Fireball it will cast Fireball immediately and Fire Blast will not get automatically cast anymore. + +This currently doesn't work for item cooldowns as they work differently, will add in the future. + +#### NP_OptimizeBufferUsingPacketTimings +This feature will attempt to optimize your buffer on individual casts using your latency and server packet timings. +After you begin to cast a spell you will get a cast result packet back from the server letting you know if the cast was successful. +The time between when you send your start cast packet and when you receive the cast result packet consists of: +- The time it takes for your packet to reach the server +- The time it takes for the server to process the packet (see Why do I need a buffer?) +- The time it takes for the server to send the result packet back to you +- Other delays I'm not sure about like the time it takes for the client to process packets from the server due to being single threaded + +If we take this 'Spell Response Time' and subtract your regular latency from it, we should be able to get a rough idea of the time it +took for the server to process the cast. If that time was less than your current default buffer we can use that time as the new buffer +for the next cast only. In theory if it is more than your current buffer we should also use it, but in +practice it seems to regularly be way larger than expected and using the default buffer doesn't result in an error. + +Due to this delay varying wildly in testing I'm unsure how reliable this technique is. It needs more testing +and a better understanding of all the factors introducing delay. It is disabled by default for now. + +# v1.0.0 Changes + +Now looks for a nampower.cfg file in the same directory with two lines: + +1. The first line should contain the "buffer" time between each cast. This is the amount of time to delay casts to ensure you don't try to cast again too early due to server/packet lag and get rejected by the server with a "This ability isn't ready yet" error. For 150ms I found 30ms to be a reasonable buffer. +2. The second line is the window in ms before each cast during which nampower will delay cast attempts to send them to the server at the perfect time. So 300 would mean if you cast anytime in the 300ms window before your next optimal cast your cast will be sent at the idea time. This means you don't have to spam cast as aggressively. This feature will cause a small stutter because it is pausing your UI (I couldn't findSpellId a better way to do this but I'm sure one exists now with superwow) so if you don't like that set this to 0. + +# Namreeb readme + +**Please consider donating if you use this tool. - Namreeb** + +[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=QFWZUEMC5N3SW) + +An auto stop-cast tool for World of Warcraft 1.12.1.5875 (for Windows) + +There is a design flaw in this version of the client. A player is not allowed to cast a +second spell until after the client receives word of the completion of the previous spell. +This means that in addition to the cast time, you have to wait for the time it takes a +message to arrive from the server. For many U.S. based players connected to E.U. based +realms, this can result in approximately a 20% drop in effective DPS. + +Consider the following timeline, assuming a latency of 200ms. + +* t = 0, the player begins casting fireball (assume a cast time of one second or 1000ms) + and spell cast message is sent to the server. at this time, the client places + a lock on itself, preventing the player from requesting another spell cast. +* t = 200, the spell cast message arrives at the server, and the spell cast begins +* t = 1200, the spell cast finishes and a finish message is sent to the client +* t = 1400, the client receives the finish message and removes the lock it had placed + 1400ms ago. + +In this scenario, a 1000ms spell takes 1400ms to cast. This tool will work around that +design flaw by altering the client behavior to not wait for the server to acknowledge +anything. + +## Using ## + +If you use my launcher, known as [wowreeb](https://github.com/namreeb/wowreeb), you can add +the following line to a `` block to tell the launcher to include this tool: + +```xml + +``` + +To launch with the built-in launcher, run loader.exe -p c:\path\to\wow.exe (or just loader.exe +with it inside the main wow folder) diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt new file mode 100644 index 0000000..4d26cdf --- /dev/null +++ b/loader/CMakeLists.txt @@ -0,0 +1,13 @@ +set(EXECUTABLE_NAME loader) + +include_directories(Include ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}) + +set(SOURCE_FILES + main.cpp + loader.rc +) + +add_executable(${EXECUTABLE_NAME} ${SOURCE_FILES}) +target_link_libraries(${EXECUTABLE_NAME} shlwapi.lib asmjit.lib udis86.lib) + +install(TARGETS ${EXECUTABLE_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}") diff --git a/loader/Icons8-Windows-8-Sports-Stopwatch.ico b/loader/Icons8-Windows-8-Sports-Stopwatch.ico new file mode 100644 index 0000000..0d2e697 Binary files /dev/null and b/loader/Icons8-Windows-8-Sports-Stopwatch.ico differ diff --git a/loader/loader.rc b/loader/loader.rc new file mode 100644 index 0000000..4bfcaef Binary files /dev/null and b/loader/loader.rc differ diff --git a/loader/main.cpp b/loader/main.cpp new file mode 100644 index 0000000..a10e3a2 --- /dev/null +++ b/loader/main.cpp @@ -0,0 +1,106 @@ +/* + Copyright (c) 2017-2023, namreeb (legal@namreeb.org) + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The views and conclusions contained in the software and documentation are those + of the authors and should not be interpreted as representing official policies, + either expressed or implied, of the FreeBSD Project. +*/ + +#define NAME "nampower" +#define VERSION "v2.0" + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +int main(int argc, char *argv[]) +{ + try + { + std::cout << NAME << " " << VERSION << " injector" << std::endl; + + std::wstring dll, program; + std::string exportFunc = "Load"; + bool enableConsole; + + boost::program_options::options_description desc("Allowed options"); + desc.add_options() + ("help,h", "display help message") + ("console,c", "enable wow console") + ("dll,d", boost::program_options::wvalue(&dll)->default_value(L"nampower.dll", "nampower.dll"), "dll to inject into program") +#ifdef _DEBUG + ("export,e", boost::program_options::value(&exportFunc)->default_value("Load"), "export function to call upon injection") +#endif + ("program,p", boost::program_options::wvalue(&program)->default_value(L"wow.exe", "wow.exe"), "program name"); + + boost::program_options::variables_map vm; + + try + { + boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), vm); + boost::program_options::notify(vm); + + if (vm.count("help")) + { + std::cout << desc << std::endl; + return EXIT_SUCCESS; + } + + enableConsole = !!(vm.count("console")); + } + catch (boost::program_options::error const &e) + { + std::cerr << "ERROR: " << e.what() << std::endl << std::endl; + std::cerr << desc << std::endl; + return EXIT_FAILURE; + } + + std::vector createArgs; + + if (enableConsole) + createArgs.push_back(L"-console"); + + const hadesmem::CreateAndInjectData injectData = + hadesmem::CreateAndInject(program, L"", std::begin(createArgs), std::end(createArgs), dll, exportFunc, hadesmem::InjectFlags::kPathResolution); + + std::cout << "Injected. Process ID: " << injectData.GetProcess().GetId() << std::endl; + } + catch (std::exception const &e) + { + std::cerr << std::endl << "Error: " << std::endl; + std::cerr << boost::diagnostic_information(e) << std::endl; + + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/loader/resource.h b/loader/resource.h new file mode 100644 index 0000000..20042ad Binary files /dev/null and b/loader/resource.h differ diff --git a/nampower/CMakeLists.txt b/nampower/CMakeLists.txt new file mode 100644 index 0000000..fb468e1 --- /dev/null +++ b/nampower/CMakeLists.txt @@ -0,0 +1,32 @@ +set(DLL_NAME nampower) + +include_directories(Include ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}) + +set(SOURCE_FILES + types.h + logging.hpp + logging.cpp + cdatastore.hpp + cdatastore.cpp + castqueue.h + game.hpp + game.cpp + main.hpp + main.cpp + offsets.hpp + helper.cpp + helper.hpp + spellcast.hpp + spellcast.cpp + spellchannel.hpp + spellchannel.cpp + spellevents.hpp + spellevents.cpp + scripts.hpp + scripts.cpp +) + +add_library(${DLL_NAME} SHARED ${SOURCE_FILES}) +target_link_libraries(${DLL_NAME} shlwapi.lib asmjit.lib udis86.lib) + +install(TARGETS ${DLL_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}") diff --git a/nampower/castqueue.h b/nampower/castqueue.h new file mode 100644 index 0000000..8df945f --- /dev/null +++ b/nampower/castqueue.h @@ -0,0 +1,164 @@ +#pragma once + +#include "types.h" +#include "logging.hpp" +#include + +namespace Nampower { + class CastQueue { + private: + int maxSize; + std::vector queue; + int front; + int rear; + int size; + + public: + explicit CastQueue(int maxSize) + : maxSize(maxSize), queue(maxSize), front(0), rear(-1), size(0) {} + + bool isFull() const { + return size == maxSize; + } + + bool isEmpty() const { + return size == 0; + } + + void clear() { + front = 0; + rear = -1; + size = 0; + } + + void pushFront(const CastSpellParams ¶ms) { + if (isFull()) { + // Shift all elements one position to the right + for (int i = size - 1; i > 0; --i) { + queue[(front + i) % maxSize] = queue[(front + i - 1) % maxSize]; + } + queue[front] = params; + } else { + front = (front - 1 + maxSize) % maxSize; + queue[front] = params; + if (size == 0) { + rear = front; + } + size++; + } + } + + void push(const CastSpellParams ¶ms, bool replaceMatchingNonGcdCategory) { + if (replaceMatchingNonGcdCategory && params.castType == CastType::NON_GCD && params.gcDCategory != 0) { + auto nonGcdParams = findGcdCategory(params.gcDCategory); + if (nonGcdParams) { + DEBUG_LOG("Replacing queued nonGcd spell " << game::GetSpellName(nonGcdParams->spellId) << " with " + << game::GetSpellName(params.spellId) + << " for gcdCategory " << params.gcDCategory); + *nonGcdParams = params; + return; + } + } + if (isFull()) { + front = (front + 1) % maxSize; + } else { + size++; + } + rear = (rear + 1) % maxSize; + queue[rear] = params; + } + + CastSpellParams pop() { + if (isEmpty()) { + return CastSpellParams{}; + } + CastSpellParams result = queue[front]; + front = (front + 1) % maxSize; + size--; + return result; + } + + CastSpellParams *peek() { + if (isEmpty()) { + return nullptr; + } + return &queue[front]; + } + + CastSpellParams *findSpellIdWithMaxStartTime(uint32_t spellId, uint32_t maxStartTimeMs) { + for (int i = 0; i < size; i++) { + int index = (front + i) % maxSize; + if (queue[index].spellId == spellId && queue[index].castStartTimeMs < maxStartTimeMs) { + return &queue[index]; + } + } + return nullptr; + } + + CastSpellParams *findSpellId(uint32_t spellId) { + for (int i = 0; i < size; i++) { + int index = (front + i) % maxSize; + if (queue[index].spellId == spellId) { + return &queue[index]; + } + } + return nullptr; + } + + CastSpellParams *findOldestWaitingForServerSpellId(uint32_t spellId) { + for (int i = size - 1; i >= 0; i--) { + int index = (front + i) % maxSize; + if (queue[index].spellId == spellId && queue[index].castResult == CastResult::WAITING_FOR_SERVER) { + return &queue[index]; + } + } + return nullptr; + } + + CastSpellParams *findNewestWaitingForServerSpellId(uint32_t spellId) { + for (int i = 0; i < size; i++) { + int index = (front + i) % maxSize; + if (queue[index].spellId == spellId && queue[index].castResult == CastResult::WAITING_FOR_SERVER) { + return &queue[index]; + } + } + return nullptr; + } + + CastSpellParams *findNewestSuccessfulSpellId(uint32_t spellId) { + for (int i = 0; i < size; i++) { + int index = (front + i) % maxSize; + if (queue[index].spellId == spellId && queue[index].castResult == CastResult::SERVER_SUCCESS) { + return &queue[index]; + } + } + return nullptr; + } + + CastSpellParams *findGcdCategory(uint32_t gcdCategory) { + for (int i = 0; i < size; i++) { + int index = (front + i) % maxSize; + if (queue[index].gcDCategory == gcdCategory) { + return &queue[index]; + } + } + return nullptr; + } + + void logHistory() { + for (int i = 0; i < size; i++) { + int index = (front + i) % maxSize; + DEBUG_LOG("Cast history " << i << ": " << game::GetSpellName(queue[index].spellId) << " result " + << queue[index].castResult); + } + } + + int getSize() const { + return size; + } + + int getMaxSize() const { + return maxSize; + } + }; +} diff --git a/nampower/cdatastore.cpp b/nampower/cdatastore.cpp new file mode 100644 index 0000000..d7a80c3 --- /dev/null +++ b/nampower/cdatastore.cpp @@ -0,0 +1,248 @@ +#include "cdatastore.hpp" + +#include +#include + +namespace Nampower { + + void CDataStore::InternalInitialize(unsigned char *&data, unsigned int &base, unsigned int &alloc) { + } + + void CDataStore::InternalDestroy(unsigned char *&data, unsigned int &base, unsigned int &alloc) { + if (alloc != (unsigned int) -1 && alloc && data) { + delete[] data; + } + data = 0; + base = 0; + alloc = 0; + } + + int CDataStore::InternalFetchRead(unsigned int pos, unsigned int bytes, unsigned char *&data, unsigned int &base, + unsigned int &alloc) { + return 1; + } + + int CDataStore::InternalFetchWrite(unsigned int pos, unsigned int bytes, unsigned char *&data, unsigned int &base, + unsigned int &alloc) { + // if (pos + bytes > alloc) { + alloc = (pos + bytes + 255) & 0xFFFFFF00; + unsigned char *newData = new unsigned char[alloc]; + memcpy(newData, data, pos); + delete[] data; + data = newData; + return 1; + } + + void CDataStore::Reset() { + if (m_alloc == -1) { + m_buffer = 0; + m_alloc = 0; + } + m_size = 0; + m_read = -1; + } + + + int CDataStore::IsRead() { + return (m_read != (unsigned int) -1); + } + + void CDataStore::Finalize() { + m_read = 0; + } + + void CDataStore::GetBufferParams(const void **data, unsigned int *size, unsigned int *alloc) { + if (data != 0) { + *data = m_buffer; + } + if (size != 0) { + *size = m_size; + } + if (alloc != 0) { + *alloc = m_alloc; + } + } + + void CDataStore::DetachBuffer(void **buffer, unsigned int *size, unsigned int *alloc) { + if (buffer) { + *buffer = m_buffer; + } + if (size) { + *size = m_size; + } + if (alloc) { + *alloc = m_alloc; + } + m_buffer = 0; + m_alloc = 0; + + Reset(); + } + + int CDataStore::FetchWrite(unsigned int pos, unsigned int bytes) { + if (pos >= m_base) { + m_alloc += m_base; + bytes += pos; + if (bytes <= m_alloc) + return 1; + } + if (!InternalFetchWrite(pos, bytes, m_buffer, m_base, m_alloc)) { + return 0; + } + return 1; + } + + void CDataStore::Initialize() { + if ((int) m_alloc != -1) { + InternalInitialize(m_buffer, m_base, m_alloc); + } + } + + void CDataStore::Destroy() { + if ((int) m_alloc != -1) { + InternalDestroy(m_buffer, m_base, m_alloc); + } + } + + bool CDataStore::AssertFetchWrite(unsigned int pos, unsigned int bytes) { + if (!InternalFetchWrite(pos, bytes, m_buffer, m_base, m_alloc)) { + return false; + } + return true; + } + + bool CDataStore::AssertFetchRead(unsigned int pos, unsigned int bytes) { + if (!InternalFetchRead(pos, bytes, m_buffer, m_base, m_alloc)) { + return false; + } + return true; + } + + void CDataStore::GetPackedGuid(uint64_t &val) { + unsigned int bytes = m_read + sizeof(val); + if (bytes > m_size) { + m_read = m_size + 1; + return; + } + if ((m_read < m_base) || (bytes > m_alloc + m_base)) { + if (!AssertFetchRead(m_read, sizeof(val))) { + m_read = m_alloc + 1; + return; + } + } + + uint64_t guid = 0; + uint8_t mask; // Read the bitmap + Get(mask); + + for (uint8_t i = 0; i < 8; ++i) { + if (mask & (1 << i)) { + uint8_t byte; + Get(byte); + + guid |= static_cast(byte) << (i * 8); + } + } + + val = guid; + } + + void CDataStore::PutPackedGuid(uint64_t guid) { + uint8_t mask = 0; // Bitmap mask to indicate which bytes are non-zero + uint8_t bytes[8]; // Array to store non-zero bytes from the GUID + + // Iterate over each byte of the GUID + for (uint8_t i = 0; i < 8; ++i) { + uint8_t byte = (guid >> (i * 8)) & 0xFF; // Extract each byte from the GUID + + if (byte != 0) { + mask |= (1 << i); // Set the corresponding bit in the mask if the byte is non-zero + bytes[i] = byte; // Store the non-zero byte + } + } + + // Write the mask byte first + Put(mask); + + // Write the non-zero bytes based on the mask + for (uint8_t i = 0; i < 8; ++i) { + if (mask & (1 << i)) { + Put(bytes[i]); // Only write bytes that are non-zero according to the mask + } + } + } + + + class CDataStore &CDataStore::PutString(char const *pStr) { + if (pStr) { + PutArray((unsigned char const *) pStr, (unsigned int) strlen(pStr) + 1); + } + return *this; + } + + class CDataStore &CDataStore::GetString(char *pString, unsigned int maxChars) { + unsigned int begin = 0; + unsigned int read = 0; + unsigned int end = 0; + + if (pString == 0) { + return *this; + } + + if ((maxChars == 0) || (m_read > m_size)) { + *pString = 0; + return *this; + } + + while (true) { + begin = m_read; + if (m_read + 1 > m_size) { + m_read = m_size + 1; + *pString = 0; + return *this; + } + if ((begin < m_base) || (m_read + 1 > m_alloc + m_base)) { + if (!AssertFetchRead(m_read, 1)) { + m_read = m_size + 1; + *pString = 0; + + return *this; + } + } + end = m_alloc + m_base; + + if (end > m_size) + end = m_size; + + end -= m_read; + + if (end < maxChars - read) + end = maxChars - read; + + unsigned char *data = m_buffer - m_base + read; + int pos = 0; + + if (end != 0) { + do { + char cl = data[pos++]; + pString[++read - 1] = cl; + if (cl == 0) break; + end--; + } while (end != 0); + } + m_read += pos; + + if (end != 0) { + if (m_read > m_size) + *pString = 0; + return *this; + } + if (read >= maxChars) { + m_read = m_size + 1; + *pString = 0; + return *this; + } + } + } + +} \ No newline at end of file diff --git a/nampower/cdatastore.hpp b/nampower/cdatastore.hpp new file mode 100644 index 0000000..95ab4d4 --- /dev/null +++ b/nampower/cdatastore.hpp @@ -0,0 +1,211 @@ +// +// Created by pmacc on 9/27/2024. +// + +#pragma once + +#include +#include +#include +#include + +namespace Nampower { + + class CDataStore { + public: + unsigned char *m_buffer; + unsigned int m_base; // base offset + unsigned int m_alloc; // amount of space allocated, -1 = no ownership of data + unsigned int m_size; // total written data (write position) + unsigned int m_read; // read position. -1 when not finalized. + + protected: + + // Buffer Control + virtual void InternalInitialize(unsigned char *&buffer, unsigned int &, unsigned int &); + + virtual void InternalDestroy(unsigned char *&buffer, unsigned int &, unsigned int &); + + virtual int + InternalFetchRead(unsigned int, unsigned int size, unsigned char *&data, unsigned int &, unsigned int &); + + virtual int InternalFetchWrite(unsigned int, unsigned int, unsigned char *&, unsigned int &, unsigned int &); + + // Cleanup / Destroy + void Initialize(); + + void Destroy(); + + // Misc. + int FetchWrite(unsigned int, unsigned int); + + public: + + // Create an empty buffer for writing + CDataStore() : m_buffer(0), m_base(0), m_alloc(0), + m_size(0), m_read((unsigned int) -1) { + Initialize(); + } + + // Read an already created buffer. Read-Only (no writing) + CDataStore(void *data, int length) : m_buffer((unsigned char *) data), m_base(0), + m_alloc((unsigned int) -1), m_size(length), m_read(0) {} + + virtual ~CDataStore() { Destroy(); } + + virtual void Reset(); + + virtual int IsRead(); + + virtual void Finalize(); + + class CDataStore &PutData(const void *, unsigned int); + + class CDataStore &PutString(const char *); + + class CDataStore &GetString(char *, unsigned int); + + class CDataStore &GetData(void *, unsigned int); + + class CDataStore &GetDataInSitu(void *&, unsigned int); + + void CDataStore::GetPackedGuid(uint64_t &val); + + void CDataStore::PutPackedGuid(uint64_t guid); + + template + void Set(unsigned int pos, T val) { + if ((pos < m_base) || (pos + sizeof(T) > m_alloc + m_base)) { + InternalFetchWrite(pos, sizeof(val), m_buffer, m_base, m_alloc); + } + *(T *) (m_buffer - m_base + pos) = val; + } + + template + void Put(T val) { + if ((m_size < m_base) || (m_size + sizeof(T) > m_alloc + m_base)) { + // make sure we can write sizeof(T) data + if (!AssertFetchWrite(m_size, sizeof(T))) { + return; + } + } + T *pos = (T *) (m_buffer - m_base + m_size); + *pos = val; + + m_size += sizeof(T); + } + + template + void Get(T &val) { + unsigned int bytes = m_read + sizeof(T); + if (bytes > m_size) { + m_read = m_size + 1; + return; + } + + if ((m_read < m_base) || (bytes > m_alloc + m_base)) { + if (!AssertFetchRead(m_read, sizeof(T))) { + m_read = m_alloc + 1; + return; + } + } + val = *(T *) (m_buffer - m_base + m_read); + m_read += sizeof(T); + } + + template + void PutArray(const T *pVal, unsigned int count) { + count *= sizeof(T); + unsigned int pos = count; + + if (pVal != 0) { + if ((m_size < m_base) || (m_size + count > m_alloc)) { + InternalFetchWrite(m_size, count, m_buffer, m_base, m_alloc); + } + while (count > 0) { + if (pos >= m_alloc) + pos = m_alloc; + else + pos = count; + + if (pos < sizeof(T)) + pos = sizeof(T); + + if ((m_size >= m_base) && (m_size + pos < m_alloc + m_base)) { + InternalFetchWrite(m_size, pos, m_buffer, m_base, m_alloc); + } + + if ((T *) (m_size - m_base + m_buffer) != pVal) + memcpy(m_buffer + m_size, pVal, count); + + pVal += pos; + m_size += pos; + count -= pos; + } + } + } + + template + void GetArray(T *pVal, unsigned int count) { + // total amount to be copied + unsigned int total = count; + if ((pVal != 0) && (m_read <= m_size) && (count != 0)) { + do { + unsigned int len = m_size - m_read; + + // shave off the length to avoid overflow + if (len > count) len = count; + if (len > m_alloc) len = m_alloc; + if (len < sizeof(T)) len = sizeof(T); + + count = m_read + len; + if (count > m_size) { + m_read = m_size + sizeof(T); + return *this; + } + + // check to make sure we can read + if ((m_read < (unsigned int) m_base) || (count > m_base + m_alloc)) { + if (!AssertFetchRead(m_read, len)) { + m_read = m_size + sizeof(T); + return *this; + } + } + if (pVal != (T *) (m_buffer - m_base + m_read)) { + memcpy(pVal, m_buffer - m_base + m_read, len * sizeof(T)); + } + m_read = m_read + len; + pVal += len; + total -= len; + } while (total > 0); + } + } + + template + CDataStore &operator<<(T val) { + return Put(val); + } + + template + CDataStore &operator>>(T &val) { + return Get(val); + } + + void *Buffer() { return m_buffer; } + + int Size() { return m_size; } + + bool IsFinal() { return m_read != -1; } + + // Assertion Methods + bool AssertFetchWrite(unsigned int pos, unsigned int bytes); + + bool AssertFetchRead(unsigned int pos, unsigned int bytes); + + // Buffer Related + virtual void GetBufferParams(const void **, unsigned int *, unsigned int *); + + virtual void DetachBuffer(void **, unsigned *, unsigned int *); + }; + +} diff --git a/nampower/game.cpp b/nampower/game.cpp new file mode 100644 index 0000000..46947ac --- /dev/null +++ b/nampower/game.cpp @@ -0,0 +1,155 @@ +/* + Copyright (c) 2017-2023, namreeb (legal@namreeb.org) + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The views and conclusions contained in the software and documentation are those + of the authors and should not be interpreted as representing official policies, + either expressed or implied, of the FreeBSD Project. +*/ + +#include "game.hpp" +#include "offsets.hpp" + +#include + +#include + +namespace game { + uintptr_t *GetObjectPtr(std::uint64_t guid) { + uintptr_t *(__stdcall *getObjectPtr)(std::uint64_t) = hadesmem::detail::AliasCast( + Offsets::GetObjectPtr); + + return getObjectPtr(guid); + } + + uintptr_t *ClntObjMgrObjectPtr(TypeMask typeMask, std::uint64_t guid) { + using ClntObjMgrObjectPtrT = uintptr_t* (__fastcall *)(TypeMask typeMask, const char *debugMessage, unsigned __int64 guid, int debugCode); + auto const clntObjMgrObjectPtr = reinterpret_cast(Offsets::ClntObjMgrObjectPtr); + + return clntObjMgrObjectPtr(typeMask, nullptr, guid, 0); + } + + + std::uint32_t GetCastTime(void *unit, uint32_t spellId) { + auto const vmt = *reinterpret_cast(unit); + int + (__thiscall *getSpellCastingTime)(void *, uint32_t) = *reinterpret_cast(vmt + + 4 * + static_cast(Offsets::GetCastingTimeIndex)); + + return getSpellCastingTime(unit, spellId); + } + + CDuration *GetDurationObject(uint32_t durationIndex) { + auto const durationListPtr = *reinterpret_cast(Offsets::GetDurationObject); + if (durationListPtr) { + auto const durationObjectPtr = *reinterpret_cast(durationListPtr + durationIndex*4); + if (durationObjectPtr) { + return reinterpret_cast(durationObjectPtr); + } + } + return nullptr; + } + + int GetSpellDuration(const SpellRec *spellRec, bool ignoreModifiers) { + using Spell_C_GetDurationT = int (__fastcall *)(const SpellRec *spellRec, int unknownFlag, char ignoreModifiers); + + auto const getDuration = reinterpret_cast(Offsets::Spell_C_GetDuration); + if (ignoreModifiers){ + return getDuration(spellRec, 1, 1); + } else { + return getDuration(spellRec, 1, 0); + } + } + + int GetSpellModifier(const SpellRec *spellRec, SpellModOp spellMod) { + using Spell_C_GetSpellModifiersT = void (__fastcall *)(const SpellRec *spellRec, int *returnVal, SpellModOp modOp); + + auto const getModifiers = reinterpret_cast(Offsets::Spell_C_GetSpellModifiers); + auto modificationPercentage = 0; + + getModifiers(spellRec, &modificationPercentage, spellMod); + + return modificationPercentage; + } + + const SpellRec *GetSpellInfo(uint32_t spellId) { + auto const spellDb = reinterpret_cast *>(Offsets::SpellDb); + + if (spellId > spellDb->m_maxId) + return nullptr; + + return spellDb->m_recordsById[spellId]; + } + + uint32_t GetItemId(CGItem_C *item) { + uintptr_t *itemInfo = item->m_itemInfo; + + return *reinterpret_cast(itemInfo + 3); // item id offset + } + + const char *GetSpellName(uint32_t spellId) { + auto const spell = GetSpellInfo(spellId); + + if (!spell || spell->AttributesEx3 & SPELL_ATTR_EX3_NO_CASTING_BAR_TEXT) + return ""; + + auto const language = *reinterpret_cast(Offsets::Language); + + return spell->SpellName[language]; + } + + std::uint64_t ClntObjMgrGetActivePlayerGuid() { + auto const getActivePlayer = hadesmem::detail::AliasCast( + Offsets::GetActivePlayer); + + return getActivePlayer(); + } + + std::uint64_t GetCurrentTargetGuid() { + return *reinterpret_cast(Offsets::LockedTargetGuid); + } + + uint64_t UnitGetGuid(uintptr_t *unit) { + if (!unit) { + return 0; + } + + uint64_t guid = *reinterpret_cast(unit + 12); + return guid; + } + + uint64_t UnitGetTargetGuid(uintptr_t *unit) { + if (!unit) { + return 0; + } + + auto *unitFields = *reinterpret_cast(unit + 68); + + if (unitFields == nullptr) { + return 0; + } + + return unitFields->target; + } +} \ No newline at end of file diff --git a/nampower/game.hpp b/nampower/game.hpp new file mode 100644 index 0000000..72eb324 --- /dev/null +++ b/nampower/game.hpp @@ -0,0 +1,1296 @@ +/* + Copyright (c) 2017-2023, namreeb (legal@namreeb.org) + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The views and conclusions contained in the software and documentation are those + of the authors and should not be interpreted as representing official policies, + either expressed or implied, of the FreeBSD Project. +*/ + +#pragma once + +#include + +namespace game { +#pragma pack(push, 1) + struct SpellRec { + unsigned int Id; + unsigned int School; + unsigned int Category; + unsigned int castUI; + unsigned int Dispel; + unsigned int Mechanic; + unsigned int Attributes; + unsigned int AttributesEx; + unsigned int AttributesEx2; + unsigned int AttributesEx3; + unsigned int AttributesEx4; + unsigned int Stances; + unsigned int StancesNot; + unsigned int Targets; + unsigned int TargetCreatureType; + unsigned int RequiresSpellFocus; + unsigned int CasterAuraState; + unsigned int TargetAuraState; + unsigned int CastingTimeIndex; + unsigned int RecoveryTime; + unsigned int CategoryRecoveryTime; + unsigned int InterruptFlags; + unsigned int AuraInterruptFlags; + unsigned int ChannelInterruptFlags; + unsigned int procFlags; + unsigned int procChance; + unsigned int procCharges; + unsigned int maxLevel; + unsigned int baseLevel; + unsigned int spellLevel; + unsigned int DurationIndex; + unsigned int powerType; + unsigned int manaCost; + unsigned int manaCostPerlevel; + unsigned int manaPerSecond; + unsigned int manaPerSecondPerLevel; + unsigned int rangeIndex; + float speed; + unsigned int modalNextSpell; + unsigned int StackAmount; + unsigned int Totem[2]; + int Reagent[8]; + unsigned int ReagentCount[8]; + int EquippedItemClass; + int EquippedItemSubClassMask; + int EquippedItemInventoryTypeMask; + unsigned int Effect[3]; + int EffectDieSides[3]; + unsigned int EffectBaseDice[3]; + float EffectDicePerLevel[3]; + float EffectRealPointsPerLevel[3]; + int EffectBasePoints[3]; + unsigned int EffectMechanic[3]; + unsigned int EffectImplicitTargetA[3]; + unsigned int EffectImplicitTargetB[3]; + unsigned int EffectRadiusIndex[3]; + unsigned int EffectApplyAuraName[3]; + unsigned int EffectAmplitude[3]; + float EffectMultipleValue[3]; + unsigned int EffectChainTarget[3]; + unsigned int EffectItemType[3]; + int EffectMiscValue[3]; + unsigned int EffectTriggerSpell[3]; + float EffectPointsPerComboPoint[3]; + unsigned int SpellVisual; + unsigned int SpellVisual2; + unsigned int SpellIconID; + unsigned int activeIconID; + unsigned int spellPriority; + const char *SpellName[8]; + unsigned int SpellNameFlag; + unsigned int Rank[8]; + unsigned int RankFlags; + unsigned int Description[8]; + unsigned int DescriptionFlags; + unsigned int ToolTip[8]; + unsigned int ToolTipFlags; + unsigned int ManaCostPercentage; + unsigned int StartRecoveryCategory; + unsigned int StartRecoveryTime; + unsigned int MaxTargetLevel; + unsigned int SpellFamilyName; + unsigned __int64 SpellFamilyFlags; + unsigned int MaxAffectedTargets; + unsigned int DmgClass; + unsigned int PreventionType; + unsigned int StanceBarOrder; + float DmgMultiplier[3]; + unsigned int MinFactionId; + unsigned int MinReputation; + unsigned int RequiredAuraVision; + }; + + template + struct WowClientDB { + T *m_records; + uint32_t m_numRecords; + T **m_recordsById; + uint32_t m_maxId; + int m_loaded; + }; + + struct SpellCast { + unsigned __int64 casterUnit; + unsigned __int64 caster; + int spellId; + unsigned __int16 targets; + unsigned __int16 unk; + unsigned __int64 unitTarget; + unsigned __int64 itemTarget; + unsigned int Fields2[18]; + char targetString[128]; + }; + + struct CSpriteClickEvent { + unsigned __int64 objectGUID; + unsigned int button; + unsigned int time; + unsigned __int64 *pos; + }; + + enum ItemStatsFlags { + ITEM_FLAG_NO_PICKUP = 1, + ITEM_FLAG_CONJURED = 2, + ITEM_FLAG_HAS_LOOT = 4, + ITEM_FLAG_EXOTIC = 8, + ITEM_FLAG_NUM = 14, + ITEM_FLAG_DEPRECATED = 16, + ITEM_FLAG_OBSOLETE = 32, + ITEM_FLAG_PLAYERCAST = 64, + ITEM_FLAG_NO_EQUIPCOOLDOWN = 128, + ITEM_FLAG_INTBONUSINSTEAD = 256, + ITEM_FLAG_IS_WRAPPER = 512, + ITEM_FLAG_USES_RESOURCES = 1024, + ITEM_FLAG_MULTI_DROP = 2048, + ITEM_FLAG_BRIEFSPELLEFFECTS = 4096, + ITEM_FLAG_PETITION = 8192, + MAX_ITEM_FLAG = 32768 + }; + + struct CGItem_C { + uint32_t m_unk; + uint32_t m_flags; + uintptr_t *m_itemInfo; + uint32_t m_expirationTime; + uint32_t m_enchantmentExpiration[5]; + uintptr_t *m_soundsRec; + }; + + struct __declspec(align(4)) ItemStats_C { + int m_class; + int m_subclass; + char *m_displayName[4]; + int m_displayInfoID; + int m_quality; + ItemStatsFlags m_flags; + int m_buyPrice; + int m_sellPrice; + int m_inventoryType; + int m_allowableClass; + int m_allowableRace; + int m_itemLevel; + int m_requiredLevel; + int m_requiredSkill; + int m_requiredSkillRank; + int m_requiredSpell; + int m_requiredHonorRank; + int m_requiredCityRank; + int m_requiredRep; + int m_requiredRepRank; + int m_maxCount; + int m_stackable; + int m_containerSlots; + int m_bonusStat[10]; + int m_bonusAmount[10]; + int m_minDamage[5]; + float m_maxDamage[5]; + int m_damageType[5]; + int m_resistances[7]; + int m_delay; + int m_ammoType; + int m_rangedModRange; + int m_spellID[5]; + int m_spellTrigger[5]; + int m_spellCharges[5]; + int m_spellCooldown[5]; + int m_spellCategory[5]; + int m_spellCategoryCooldown[5]; + int m_bonding; + char *m_description; + int m_pageText; + int m_languageID; + int m_pageMaterial; + int m_startQuestID; + int m_lockID; + char *m_material; + int m_sheatheType; + int m_randomProperty; + int m_block; + int m_itemSet; + int m_maxDurability; + int m_area; + int m_map; + int m_duration; + int m_bagFamily; + }; +#pragma pack(pop) + + enum SpellEffects { + SPELL_EFFECT_NONE = 0, + SPELL_EFFECT_INSTAKILL = 1, + SPELL_EFFECT_SCHOOL_DAMAGE = 2, + SPELL_EFFECT_DUMMY = 3, + SPELL_EFFECT_PORTAL_TELEPORT = 4, + SPELL_EFFECT_TELEPORT_UNITS = 5, + SPELL_EFFECT_APPLY_AURA = 6, + SPELL_EFFECT_ENVIRONMENTAL_DAMAGE = 7, + SPELL_EFFECT_POWER_DRAIN = 8, + SPELL_EFFECT_HEALTH_LEECH = 9, + SPELL_EFFECT_HEAL = 10, + SPELL_EFFECT_BIND = 11, + SPELL_EFFECT_PORTAL = 12, + SPELL_EFFECT_RITUAL_BASE = 13, + SPELL_EFFECT_RITUAL_SPECIALIZE = 14, + SPELL_EFFECT_RITUAL_ACTIVATE_PORTAL = 15, + SPELL_EFFECT_QUEST_COMPLETE = 16, + SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL = 17, + SPELL_EFFECT_RESURRECT = 18, + SPELL_EFFECT_ADD_EXTRA_ATTACKS = 19, + SPELL_EFFECT_DODGE = 20, + SPELL_EFFECT_EVADE = 21, + SPELL_EFFECT_PARRY = 22, + SPELL_EFFECT_BLOCK = 23, + SPELL_EFFECT_CREATE_ITEM = 24, + SPELL_EFFECT_WEAPON = 25, + SPELL_EFFECT_DEFENSE = 26, + SPELL_EFFECT_PERSISTENT_AREA_AURA = 27, + SPELL_EFFECT_SUMMON = 28, + SPELL_EFFECT_LEAP = 29, + SPELL_EFFECT_ENERGIZE = 30, + SPELL_EFFECT_WEAPON_PERCENT_DAMAGE = 31, + SPELL_EFFECT_TRIGGER_MISSILE = 32, + SPELL_EFFECT_OPEN_LOCK = 33, + SPELL_EFFECT_SUMMON_CHANGE_ITEM = 34, + SPELL_EFFECT_APPLY_AREA_AURA_PARTY = 35, + SPELL_EFFECT_LEARN_SPELL = 36, + SPELL_EFFECT_SPELL_DEFENSE = 37, + SPELL_EFFECT_DISPEL = 38, + SPELL_EFFECT_LANGUAGE = 39, + SPELL_EFFECT_DUAL_WIELD = 40, + SPELL_EFFECT_SUMMON_WILD = 41, + SPELL_EFFECT_SUMMON_GUARDIAN = 42, + SPELL_EFFECT_TELEPORT_UNITS_FACE_CASTER = 43, + SPELL_EFFECT_SKILL_STEP = 44, + SPELL_EFFECT_ADD_HONOR = 45, + SPELL_EFFECT_SPAWN = 46, + SPELL_EFFECT_TRADE_SKILL = 47, + SPELL_EFFECT_STEALTH = 48, + SPELL_EFFECT_DETECT = 49, + SPELL_EFFECT_TRANS_DOOR = 50, + SPELL_EFFECT_FORCE_CRITICAL_HIT = 51, + SPELL_EFFECT_GUARANTEE_HIT = 52, + SPELL_EFFECT_ENCHANT_ITEM = 53, + SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY = 54, + SPELL_EFFECT_TAMECREATURE = 55, + SPELL_EFFECT_SUMMON_PET = 56, + SPELL_EFFECT_LEARN_PET_SPELL = 57, + SPELL_EFFECT_WEAPON_DAMAGE = 58, + SPELL_EFFECT_OPEN_LOCK_ITEM = 59, + SPELL_EFFECT_PROFICIENCY = 60, + SPELL_EFFECT_SEND_EVENT = 61, + SPELL_EFFECT_POWER_BURN = 62, + SPELL_EFFECT_THREAT = 63, + SPELL_EFFECT_TRIGGER_SPELL = 64, + SPELL_EFFECT_HEALTH_FUNNEL = 65, + SPELL_EFFECT_POWER_FUNNEL = 66, + SPELL_EFFECT_HEAL_MAX_HEALTH = 67, + SPELL_EFFECT_INTERRUPT_CAST = 68, + SPELL_EFFECT_DISTRACT = 69, + SPELL_EFFECT_PULL = 70, + SPELL_EFFECT_PICKPOCKET = 71, + SPELL_EFFECT_ADD_FARSIGHT = 72, + SPELL_EFFECT_SUMMON_POSSESSED = 73, + SPELL_EFFECT_SUMMON_TOTEM = 74, + SPELL_EFFECT_HEAL_MECHANICAL = 75, + SPELL_EFFECT_SUMMON_OBJECT_WILD = 76, + SPELL_EFFECT_SCRIPT_EFFECT = 77, + SPELL_EFFECT_ATTACK = 78, + SPELL_EFFECT_SANCTUARY = 79, + SPELL_EFFECT_ADD_COMBO_POINTS = 80, + SPELL_EFFECT_CREATE_HOUSE = 81, + SPELL_EFFECT_BIND_SIGHT = 82, + SPELL_EFFECT_DUEL = 83, + SPELL_EFFECT_STUCK = 84, + SPELL_EFFECT_SUMMON_PLAYER = 85, + SPELL_EFFECT_ACTIVATE_OBJECT = 86, + SPELL_EFFECT_SUMMON_TOTEM_SLOT1 = 87, + SPELL_EFFECT_SUMMON_TOTEM_SLOT2 = 88, + SPELL_EFFECT_SUMMON_TOTEM_SLOT3 = 89, + SPELL_EFFECT_SUMMON_TOTEM_SLOT4 = 90, + SPELL_EFFECT_THREAT_ALL = 91, + SPELL_EFFECT_ENCHANT_HELD_ITEM = 92, + SPELL_EFFECT_SUMMON_PHANTASM = 93, + SPELL_EFFECT_SELF_RESURRECT = 94, + SPELL_EFFECT_SKINNING = 95, + SPELL_EFFECT_CHARGE = 96, + SPELL_EFFECT_SUMMON_CRITTER = 97, + SPELL_EFFECT_KNOCK_BACK = 98, + SPELL_EFFECT_DISENCHANT = 99, + SPELL_EFFECT_INEBRIATE = 100, + SPELL_EFFECT_FEED_PET = 101, + SPELL_EFFECT_DISMISS_PET = 102, + SPELL_EFFECT_REPUTATION = 103, + SPELL_EFFECT_SUMMON_OBJECT_SLOT1 = 104, + SPELL_EFFECT_SUMMON_OBJECT_SLOT2 = 105, + SPELL_EFFECT_SUMMON_OBJECT_SLOT3 = 106, + SPELL_EFFECT_SUMMON_OBJECT_SLOT4 = 107, + SPELL_EFFECT_DISPEL_MECHANIC = 108, + SPELL_EFFECT_SUMMON_DEAD_PET = 109, + SPELL_EFFECT_DESTROY_ALL_TOTEMS = 110, + SPELL_EFFECT_DURABILITY_DAMAGE = 111, + SPELL_EFFECT_SUMMON_DEMON = 112, + SPELL_EFFECT_RESURRECT_NEW = 113, + SPELL_EFFECT_ATTACK_ME = 114, + SPELL_EFFECT_DURABILITY_DAMAGE_PCT = 115, + SPELL_EFFECT_SKIN_PLAYER_CORPSE = 116, + SPELL_EFFECT_SPIRIT_HEAL = 117, + SPELL_EFFECT_SKILL = 118, + SPELL_EFFECT_APPLY_AREA_AURA_PET = 119, + SPELL_EFFECT_TELEPORT_GRAVEYARD = 120, + SPELL_EFFECT_NORMALIZED_WEAPON_DMG = 121, + SPELL_EFFECT_122 = 122, + SPELL_EFFECT_SEND_TAXI = 123, + SPELL_EFFECT_PLAYER_PULL = 124, + SPELL_EFFECT_MODIFY_THREAT_PERCENT = 125, + SPELL_EFFECT_126 = 126, + SPELL_EFFECT_127 = 127, + // Effets "backportes" depuis MaNGOS BC+. + SPELL_EFFECT_APPLY_AREA_AURA_FRIEND = 128, + SPELL_EFFECT_APPLY_AREA_AURA_ENEMY = 129, + // Custom + SPELL_EFFECT_DESPAWN_OBJECT = 130, + SPELL_EFFECT_NOSTALRIUS = 131, + SPELL_EFFECT_APPLY_AREA_AURA_RAID = 132, + SPELL_EFFECT_APPLY_AREA_AURA_OWNER = 133, + TOTAL_SPELL_EFFECTS = 134 + }; + + + enum SpellCastResult : std::uint8_t { + SPELL_FAILED_AFFECTING_COMBAT = 0, // 0x0 + SPELL_FAILED_ALREADY_AT_FULL_HEALTH = 1, // 0x1 + SPELL_FAILED_ALREADY_AT_FULL_MANA = 2, // 0x2 + SPELL_FAILED_ALREADY_BEING_TAMED = 3, // 0x3 + SPELL_FAILED_ALREADY_HAVE_CHARM = 4, // 0x4 + SPELL_FAILED_ALREADY_HAVE_SUMMON = 5, // 0x5 + SPELL_FAILED_ALREADY_OPEN = 6, // 0x6 + SPELL_FAILED_MORE_POWERFUL_SPELL_ACTIVE = 7, // 0x7 + SPELL_FAILED_BAD_IMPLICIT_TARGETS = 9, // 0x9 + SPELL_FAILED_BAD_TARGETS = 10, // 0xA + SPELL_FAILED_CANT_BE_CHARMED = 11, // 0xB + SPELL_FAILED_CANT_BE_DISENCHANTED = 12, // 0xC + SPELL_FAILED_CANT_BE_PROSPECTED = 13, // 0xD + SPELL_FAILED_CANT_CAST_ON_TAPPED = 14, // 0xE + SPELL_FAILED_CANT_DUEL_WHILE_INVISIBLE = 15, // 0xF + SPELL_FAILED_CANT_DUEL_WHILE_STEALTHED = 16, // 0x10 + SPELL_FAILED_CANT_TOO_CLOSE_TO_ENEMY = 17, // 0x11 + SPELL_FAILED_CANT_DO_THAT_YET = 18, // 0x12 + SPELL_FAILED_CASTER_DEAD = 19, // 0x13 + SPELL_FAILED_CHARMED = 20, // 0x14 + SPELL_FAILED_CHEST_IN_USE = 21, // 0x15 + SPELL_FAILED_CONFUSED = 22, // 0x16 + SPELL_FAILED_DONT_REPORT = 23, // 0x17 + SPELL_FAILED_EQUIPPED_ITEM = 24, // 0x18 + SPELL_FAILED_EQUIPPED_ITEM_CLASS = 25, // 0x19 + SPELL_FAILED_EQUIPPED_ITEM_CLASS_MAINHAND = 26, // 0x1A + SPELL_FAILED_EQUIPPED_ITEM_CLASS_OFFHAND = 27, // 0x1B + SPELL_FAILED_ERROR = 28, // 0x1C + SPELL_FAILED_FIZZLE = 29, // 0x1D + SPELL_FAILED_FLEEING = 30, // 0x1E + SPELL_FAILED_FOOD_LOWLEVEL = 31, // 0x1F + SPELL_FAILED_HIGHLEVEL = 32, // 0x20 + SPELL_FAILED_IMMUNE = 34, // 0x22 + SPELL_FAILED_INTERRUPTED = 35, // 0x23 + SPELL_FAILED_INTERRUPTED_COMBAT = 36, // 0x24 + SPELL_FAILED_ITEM_ALREADY_ENCHANTED = 37, // 0x25 + SPELL_FAILED_ITEM_GONE = 38, // 0x26 + SPELL_FAILED_ENCHANT_NOT_EXISTING_ITEM = 39, // 0x27 + SPELL_FAILED_ITEM_NOT_READY = 40, // 0x28 + SPELL_FAILED_LEVEL_REQUIREMENT = 41, // 0x29 + SPELL_FAILED_LINE_OF_SIGHT = 42, // 0x2A + SPELL_FAILED_LOWLEVEL = 43, // 0x2B + SPELL_FAILED_SKILL_NOT_HIGH_ENOUGH = 44, // 0x2C + SPELL_FAILED_MAINHAND_EMPTY = 45, // 0x2D + SPELL_FAILED_MOVING = 46, // 0x2E + SPELL_FAILED_NEED_AMMO = 47, // 0x2F + SPELL_FAILED_NEED_REQUIRES_SOMETHING = 48, // 0x30 + SPELL_FAILED_NEED_EXOTIC_AMMO = 49, // 0x31 + SPELL_FAILED_NOPATH = 50, // 0x32 + SPELL_FAILED_NOT_BEHIND = 51, // 0x33 + SPELL_FAILED_NOT_FISHABLE = 52, // 0x34 + SPELL_FAILED_NOT_HERE = 53, // 0x35 + SPELL_FAILED_NOT_INFRONT = 54, // 0x36 + SPELL_FAILED_NOT_IN_CONTROL = 55, // 0x37 + SPELL_FAILED_NOT_KNOWN = 56, // 0x38 + SPELL_FAILED_NOT_MOUNTED = 57, // 0x39 + SPELL_FAILED_NOT_ON_TAXI = 58, // 0x3A + SPELL_FAILED_NOT_ON_TRANSPORT = 59, // 0x3B + SPELL_FAILED_NOT_READY = 60, // 0x3C + SPELL_FAILED_NOT_SHAPESHIFT = 61, // 0x3D + SPELL_FAILED_NOT_STANDING = 62, // 0x3E + SPELL_FAILED_NOT_TRADEABLE = 63, // 0x3F + SPELL_FAILED_NOT_TRADING = 64, // 0x40 + SPELL_FAILED_NOT_UNSHEATHED = 65, // 0x41 + SPELL_FAILED_NOT_WHILE_GHOST = 66, // 0x42 + SPELL_FAILED_NO_AMMO = 67, // 0x43 + SPELL_FAILED_NO_CHARGES_REMAIN = 68, // 0x44 + SPELL_FAILED_NO_CHAMPION = 69, // 0x45 + SPELL_FAILED_NO_COMBO_POINTS = 70, // 0x46 + SPELL_FAILED_NO_DUELING = 71, // 0x47 + SPELL_FAILED_NO_ENDURANCE = 72, // 0x48 + SPELL_FAILED_NO_FISH = 73, // 0x49 + SPELL_FAILED_NO_ITEMS_WHILE_SHAPESHIFTED = 74, // 0x4A + SPELL_FAILED_NO_MOUNTS_ALLOWED = 75, // 0x4B + SPELL_FAILED_NO_PET = 76, // 0x4C + SPELL_FAILED_NO_POWER = 77, // 0x4D + SPELL_FAILED_NOTHING_TO_DISPEL = 78, // 0x4E + SPELL_FAILED_NOTHING_TO_STEAL = 79, // 0x4F + SPELL_FAILED_ONLY_ABOVEWATER = 80, // 0x50 + SPELL_FAILED_ONLY_DAYTIME = 81, // 0x51 + SPELL_FAILED_ONLY_INDOORS = 82, // 0x52 + SPELL_FAILED_ONLY_MOUNTED = 83, // 0x53 + SPELL_FAILED_ONLY_NIGHTTIME = 84, // 0x54 + SPELL_FAILED_ONLY_OUTDOORS = 85, // 0x55 + SPELL_FAILED_ONLY_SHAPESHIFT = 86, // 0x56 + SPELL_FAILED_ONLY_STEALTHED = 87, // 0x57 + SPELL_FAILED_ONLY_UNDERWATER = 88, // 0x58 + SPELL_FAILED_OUT_OF_RANGE = 89, // 0x59 + SPELL_FAILED_PACIFIED = 90, // 0x5A + SPELL_FAILED_POSSESSED = 91, // 0x5B + SPELL_FAILED_REQUIRES_AREA = 93, // 0x5D + SPELL_FAILED_REQUIRES_SPELL_FOCUS = 94, // 0x5E + SPELL_FAILED_ROOTED = 95, // 0x5F + SPELL_FAILED_SILENCED = 96, // 0x60 + SPELL_FAILED_SPELL_IN_PROGRESS = 97, // 0x61 + SPELL_FAILED_SPELL_LEARNED = 98, // 0x62 + SPELL_FAILED_SPELL_UNAVAILABLE = 99, // 0x63 + SPELL_FAILED_STUNNED = 100, // 0x64 + SPELL_FAILED_TARGETS_DEAD = 101, // 0x65 + SPELL_FAILED_TARGET_AFFECTING_COMBAT = 102, // 0x66 + SPELL_FAILED_TARGET_AURASTATE = 103, // 0x67 + SPELL_FAILED_TARGET_DUELING = 104, // 0x68 + SPELL_FAILED_TARGET_ENEMY = 105, // 0x69 + SPELL_FAILED_TARGET_ENRAGED = 106, // 0x6A + SPELL_FAILED_TARGET_FRIENDLY = 107, // 0x6B + SPELL_FAILED_TARGET_IN_COMBAT = 108, // 0x6C + SPELL_FAILED_TARGET_IS_PLAYER = 109, // 0x6D + SPELL_FAILED_TARGET_NOT_DEAD = 110, // 0x6E + SPELL_FAILED_TARGET_NOT_IN_PARTY = 111, // 0x6F + SPELL_FAILED_TARGET_NOT_LOOTED = 112, // 0x70 + SPELL_FAILED_TARGET_NOT_PLAYER = 113, // 0x71 + SPELL_FAILED_TARGET_NO_POCKETS = 114, // 0x72 + SPELL_FAILED_TARGET_NO_WEAPONS = 115, // 0x73 + SPELL_FAILED_TARGET_UNSKINNABLE = 116, // 0x74 + SPELL_FAILED_THIRST_SATIATED = 117, // 0x75 + SPELL_FAILED_TOO_CLOSE = 118 + }; + + enum SpellAttributes { + SPELL_ATTR_UNK0 = 0x1, + SPELL_ATTR_RANGED = 0x2, + SPELL_ATTR_ON_NEXT_SWING_1 = 0x4, + SPELL_ATTR_UNK3 = 0x8, + SPELL_ATTR_ABILITY = 0x10, + SPELL_ATTR_TRADESPELL = 0x20, + SPELL_ATTR_PASSIVE = 0x40, + SPELL_ATTR_HIDDEN_CLIENTSIDE = 0x80, + SPELL_ATTR_HIDE_IN_COMBAT_LOG = 0x100, + SPELL_ATTR_TARGET_MAINHAND_ITEM = 0x200, + SPELL_ATTR_ON_NEXT_SWING_2 = 0x400, + SPELL_ATTR_UNK11 = 0x800, + SPELL_ATTR_DAYTIME_ONLY = 0x1000, + SPELL_ATTR_NIGHT_ONLY = 0x2000, + SPELL_ATTR_INDOORS_ONLY = 0x4000, + SPELL_ATTR_OUTDOORS_ONLY = 0x8000, + SPELL_ATTR_NOT_SHAPESHIFT = 0x10000, + SPELL_ATTR_ONLY_STEALTHED = 0x20000, + SPELL_ATTR_DONT_AFFECT_SHEATH_STATE = 0x40000, + SPELL_ATTR_LEVEL_DAMAGE_CALCULATION = 0x80000, + SPELL_ATTR_STOP_ATTACK_TARGET = 0x100000, + SPELL_ATTR_IMPOSSIBLE_DODGE_PARRY_BLOCK = 0x200000, + SPELL_ATTR_SET_TRACKING_TARGET = 0x400000, + SPELL_ATTR_CASTABLE_WHILE_DEAD = 0x800000, + SPELL_ATTR_CASTABLE_WHILE_MOUNTED = 0x1000000, + SPELL_ATTR_DISABLED_WHILE_ACTIVE = 0x2000000, + SPELL_ATTR_NEGATIVE = 0x4000000, + SPELL_ATTR_CASTABLE_WHILE_SITTING = 0x8000000, + SPELL_ATTR_CANT_USED_IN_COMBAT = 0x10000000, + SPELL_ATTR_UNAFFECTED_BY_INVULNERABILITY = 0x20000000, + SPELL_ATTR_UNK30 = 0x40000000, + SPELL_ATTR_CANT_CANCEL = 0x80000000, + }; + + enum SpellAttributesEx { + SPELL_ATTR_EX_DISMISS_PET_FIRST = 0x00000001, // 0 For spells without this flag client doesn't allow to summon pet if caster has a pet + SPELL_ATTR_EX_USE_ALL_MANA = 0x00000002, // 1 Use all power (Only paladin Lay of Hands and Bunyanize) + SPELL_ATTR_EX_IS_CHANNELED = 0x00000004, // 2 + SPELL_ATTR_EX_NO_REDIRECTION = 0x00000008, // 3 + SPELL_ATTR_EX_NO_SKILL_INCREASE = 0x00000010, // 4 Only assigned to stealth spells for some reason + SPELL_ATTR_EX_ALLOW_WHILE_STEALTHED = 0x00000020, // 5 Does not break stealth + SPELL_ATTR_EX_IS_SELF_CHANNELED = 0x00000040, // 6 + SPELL_ATTR_EX_NO_REFLECTION = 0x00000080, // 7 + SPELL_ATTR_EX_ONLY_PEACEFUL_TARGETS = 0x00000100, // 8 Target must not be in combat + SPELL_ATTR_EX_INITIATES_COMBAT = 0x00000200, // 9 Enables Auto-Attack + SPELL_ATTR_EX_NO_THREAT = 0x00000400, // 10 + SPELL_ATTR_EX_AURA_UNIQUE = 0x00000800, // 11 + SPELL_ATTR_EX_FAILURE_BREAKS_STEALTH = 0x00001000, // 12 + SPELL_ATTR_EX_TOGGLE_FARSIGHT = 0x00002000, // 13 + SPELL_ATTR_EX_TRACK_TARGET_IN_CHANNEL = 0x00004000, // 14 Client automatically forces player to face target when channeling + SPELL_ATTR_EX_IMMUNITY_PURGES_EFFECT = 0x00008000, // 15 Remove auras on immunity + SPELL_ATTR_EX_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS = 0x00010000, // 16 Aura that provides immunity prevents positive effects too + SPELL_ATTR_EX_NO_AUTOCAST_AI = 0x00020000, // 17 + SPELL_ATTR_EX_PREVENTS_ANIM = 0x00040000, // 18 Stun, polymorph, daze, sleep + SPELL_ATTR_EX_EXCLUDE_CASTER = 0x00080000, // 19 + SPELL_ATTR_EX_FINISHING_MOVE_DAMAGE = 0x00100000, // 20 Uses combo points + SPELL_ATTR_EX_THREAT_ONLY_ON_MISS = 0x00200000, // 21 + SPELL_ATTR_EX_FINISHING_MOVE_DURATION = 0x00400000, // 22 Uses combo points (in 4.x not required combo point target selected) + SPELL_ATTR_EX_IGNORE_CASTER_AND_TARGET_RESTRICTIONS = 0x00800000, // 23 Skips all cast checks, moved to AttributesEx3 after 1.10 (100% correlation) + SPELL_ATTR_EX_SPECIAL_SKILLUP = 0x01000000, // 24 Only fishing spells + SPELL_ATTR_EX_UNK25 = 0x02000000, // 25 Different in vanilla + SPELL_ATTR_EX_REQUIRE_ALL_TARGETS = 0x04000000, // 26 + SPELL_ATTR_EX_DISCOUNT_POWER_ON_MISS = 0x08000000, // 27 All these spells refund power on parry or deflect + SPELL_ATTR_EX_NO_AURA_ICON = 0x10000000, // 28 Client doesn't display these spells in aura bar + SPELL_ATTR_EX_NAME_IN_CHANNEL_BAR = 0x20000000, // 29 Spell name is displayed in cast bar instead of 'channeling' text + SPELL_ATTR_EX_COMBO_ON_BLOCK = 0x40000000, // 30 Overpower + SPELL_ATTR_EX_CAST_WHEN_LEARNED = 0x80000000 // 31 + }; + + enum SpellAttributesEx2 { + SPELL_ATTR_EX2_ALLOW_DEAD_TARGET = 0x00000001, // 0 Can target dead unit or corpse + SPELL_ATTR_EX2_NO_SHAPESHIFT_UI = 0x00000002, // 1 + SPELL_ATTR_EX2_IGNORE_LINE_OF_SIGHT = 0x00000004, // 2 + SPELL_ATTR_EX2_ALLOW_LOW_LEVEL_BUFF = 0x00000008, // 3 + SPELL_ATTR_EX2_USE_SHAPESHIFT_BAR = 0x00000010, // 4 Client displays icon in stance bar when learned, even if not shapeshift + SPELL_ATTR_EX2_AUTO_REPEAT = 0x00000020, // 5 + SPELL_ATTR_EX2_CANNOT_CAST_ON_TAPPED = 0x00000040, // 6 Target must be tapped by caster + SPELL_ATTR_EX2_DO_NOT_REPORT_SPELL_FAILURE = 0x00000080, // 7 + SPELL_ATTR_EX2_UNK8 = 0x00000100, // 8 Unused + SPELL_ATTR_EX2_UNK9 = 0x00000200, // 9 Unused + SPELL_ATTR_EX2_SPECIAL_TAMING_FLAG = 0x00000400, // 10 + SPELL_ATTR_EX2_NO_TARGET_PER_SECOND_COSTS = 0x00000800, // 11 + SPELL_ATTR_EX2_CHAIN_FROM_CASTER = 0x00001000, // 12 + SPELL_ATTR_EX2_ENCHANT_OWN_ITEM_ONLY = 0x00002000, // 13 + SPELL_ATTR_EX2_ALLOW_WHILE_INVISIBLE = 0x00004000, // 14 + SPELL_ATTR_EX2_ENABLE_AFTER_PARRY = 0x00008000, // 15 Deprecated in patch 1.8 and moved to CasterAuraState + SPELL_ATTR_EX2_NO_ACTIVE_PETS = 0x00010000, // 16 + SPELL_ATTR_EX2_DO_NOT_RESET_COMBAT_TIMERS = 0x00020000, // 17 Don't reset timers for melee autoattacks (swings) or ranged autoattacks (autoshoots) + SPELL_ATTR_EX2_REQ_DEAD_PET = 0x00040000, // 18 Only Revive pet has it + SPELL_ATTR_EX2_ALLOW_WHILE_NOT_SHAPESHIFTED = 0x00080000, // 19 Does not necessary need shapeshift (pre-3.x not have passive spells with this attribute) + SPELL_ATTR_EX2_INITIATE_COMBAT_POST_CAST = 0x00100000, // 20 Client will send CMSG_ATTACK_SWING after SMSG_SPELL_GO + SPELL_ATTR_EX2_FAIL_ON_ALL_TARGETS_IMMUNE = 0x00200000, // 21 For ice blocks, pala immunity buffs, priest absorb shields + SPELL_ATTR_EX2_NO_INITIAL_THREAT = 0x00400000, // 22 + SPELL_ATTR_EX2_PROC_COOLDOWN_ON_FAILURE = 0x00800000, // 23 + SPELL_ATTR_EX2_ITEM_CAST_WITH_OWNER_SKILL = 0x01000000, // 24 NYI + SPELL_ATTR_EX2_DONT_BLOCK_MANA_REGEN = 0x02000000, // 25 + SPELL_ATTR_EX2_NO_SCHOOL_IMMUNITIES = 0x04000000, // 26 + SPELL_ATTR_EX2_IGNORE_WEAPONSKILL = 0x08000000, // 27 NYI (only fishing has it) + SPELL_ATTR_EX2_NOT_AN_ACTION = 0x10000000, // 28 + SPELL_ATTR_EX2_CANT_CRIT = 0x20000000, // 29 + SPELL_ATTR_EX2_ACTIVE_THREAT = 0x40000000, // 30 Caster is put in combat for 5.5 seconds on cast at enemy unit + SPELL_ATTR_EX2_RETAIN_ITEM_CAST = 0x80000000 // 31 Food or Drink Buff (like Well Fed) + }; + + enum SpellAttributesEx3 { + SPELL_ATTR_EX3_PVP_ENABLING = 0x00000001, // 0 Spell landed counts as hostile action against enemy even if it doesn't trigger combat state, propagates PvP flags + SPELL_ATTR_EX3_NO_PROC_EQUIP_REQUIREMENT = 0x00000002, // 1 + SPELL_ATTR_EX3_NO_CASTING_BAR_TEXT = 0x00000004, // 2 + SPELL_ATTR_EX3_COMPLETELY_BLOCKED = 0x00000008, // 3 All effects prevented on block + SPELL_ATTR_EX3_NO_RES_TIMER = 0x00000010, // 4 Corpse reclaim delay does not apply to accepting resurrection (only Rebirth has it) + SPELL_ATTR_EX3_NO_DURABILITY_LOSS = 0x00000020, // 5 + SPELL_ATTR_EX3_NO_AVOIDANCE = 0x00000040, // 6 Persistent Area Aura not removed on leaving radius + SPELL_ATTR_EX3_DOT_STACKING_RULE = 0x00000080, // 7 Create a separate (de)buff stack for each caster + SPELL_ATTR_EX3_ONLY_ON_PLAYER = 0x00000100, // 8 Can target only players + SPELL_ATTR_EX3_NOT_A_PROC = 0x00000200, // 9 Aura periodic trigger is not evaluated as triggered + SPELL_ATTR_EX3_REQUIRES_MAIN_HAND_WEAPON = 0x00000400, // 10 + SPELL_ATTR_EX3_ONLY_BATTLEGROUNDS = 0x00000800, // 11 + SPELL_ATTR_EX3_ONLY_ON_GHOSTS = 0x00001000, // 12 + SPELL_ATTR_EX3_HIDE_CHANNEL_BAR = 0x00002000, // 13 Client will not display channeling bar + SPELL_ATTR_EX3_HIDE_IN_RAID_FILTER = 0x00004000, // 14 Only "Honorless Target" has this flag + SPELL_ATTR_EX3_NORMAL_RANGED_ATTACK = 0x00008000, // 15 Spells with this attribute are processed as ranged attacks in client + SPELL_ATTR_EX3_SUPPRESS_CASTER_PROCS = 0x00010000, // 16 + SPELL_ATTR_EX3_SUPPRESS_TARGET_PROCS = 0x00020000, // 17 + SPELL_ATTR_EX3_ALWAYS_HIT = 0x00040000, // 18 Spell should always hit its target + SPELL_ATTR_EX3_INSTANT_TARGET_PROCS = 0x00080000, // 19 Related to spell batching + SPELL_ATTR_EX3_ALLOW_AURA_WHILE_DEAD = 0x00100000, // 20 Death persistent spells + SPELL_ATTR_EX3_ONLY_PROC_OUTDOORS = 0x00200000, // 21 + SPELL_ATTR_EX3_CASTING_CANCELS_AUTOREPEAT = 0x00400000, // 22 NYI (only Shoot with Wand has it) + SPELL_ATTR_EX3_NO_DAMAGE_HISTORY = 0x00800000, // 23 NYI + SPELL_ATTR_EX3_REQUIRES_OFFHAND_WEAPON = 0x01000000, // 24 + SPELL_ATTR_EX3_TREAT_AS_PERIODIC = 0x02000000, // 25 Does not cause spell pushback + SPELL_ATTR_EX3_CAN_PROC_FROM_PROCS = 0x04000000, // 26 Auras with this attribute can proc off procced spells (periodic triggers etc) + SPELL_ATTR_EX3_ONLY_PROC_ON_CASTER = 0x08000000, // 27 + SPELL_ATTR_EX3_IGNORE_CASTER_AND_TARGET_RESTRICTIONS = 0x10000000, // 28 Skips all cast checks, moved from AttributesEx after 1.10 (100% correlation) + SPELL_ATTR_EX3_IGNORE_CASTER_MODIFIERS = 0x20000000, // 29 + SPELL_ATTR_EX3_DO_NOT_DISPLAY_RANGE = 0x40000000, // 30 + SPELL_ATTR_EX3_NOT_ON_AOE_IMMUNE = 0x80000000 // 31 + }; + + enum SpellAttributesEx4 { + SPELL_ATTR_EX4_IGNORE_RESISTANCES = 0x00000001, // 0 From TC 3.3.5, but not present in 1.12 native DBCs. Add it with spell_mod to prevent a spell from being resisted. + SPELL_ATTR_EX4_CLASS_TRIGGER_ONLY_ON_TARGET = 0x00000002, // 1 + SPELL_ATTR_EX4_AURA_EXPIRES_OFFLINE = 0x00000004, // 2 Aura continues to expire while player is offline + SPELL_ATTR_EX4_NO_HELPFUL_THREAT = 0x00000008, // 3 + SPELL_ATTR_EX4_NO_HARMFUL_THREAT = 0x00000010, // 4 + SPELL_ATTR_EX4_ALLOW_CLIENT_TARGETING = 0x00000020, // 5 NYI + SPELL_ATTR_EX4_CANNOT_BE_STOLEN = 0x00000040, // 6 Unused + SPELL_ATTR_EX4_CAN_CAST_WHILE_CASTING = 0x00000080, // 7 NYI (does not seem to work client side either) + SPELL_ATTR_EX4_IGNORE_DAMAGE_TAKEN_MODIFIERS = 0x00000100, // 8 + SPELL_ATTR_EX4_COMBAT_FEEDBACK_WHEN_USABLE = 0x00000200, // 9 Initially disabled / Trigger activate from event (Execute, Riposte, Deep Freeze...) + }; + +// Custom flags assigned in the db + enum SpellAttributesCustom { + SPELL_CUSTOM_NONE = 0x000, + SPELL_CUSTOM_ALLOW_STACK_BETWEEN_CASTER = 0x001, // For example 'Siphon Soul' must be able to stack between the warlocks on a mob + SPELL_CUSTOM_NEGATIVE = 0x002, + SPELL_CUSTOM_POSITIVE = 0x004, + SPELL_CUSTOM_CHAN_NO_DIST_LIMIT = 0x008, + SPELL_CUSTOM_FIXED_DAMAGE = 0x010, // Not affected by damage/healing done bonus + SPELL_CUSTOM_IGNORE_ARMOR = 0x020, + SPELL_CUSTOM_BEHIND_TARGET = 0x040, // For spells that require the caster to be behind the target + SPELL_CUSTOM_FACE_TARGET = 0x080, // For spells that require the target to be in front of the caster + SPELL_CUSTOM_SINGLE_TARGET_AURA = 0x100, // Aura applied by spell can only be on 1 target at a time + SPELL_CUSTOM_AURA_APPLY_BREAKS_STEALTH = 0x200, // Stealth is removed when this aura is applied + SPELL_CUSTOM_NOT_REMOVED_ON_EVADE = 0x400, // Aura persists after creature evades + SPELL_CUSTOM_SEND_CHANNEL_VISUAL = 0x800, // Will periodically send the channeling spell visual kit + SPELL_CUSTOM_SEPARATE_AURA_PER_CASTER = 0x1000, // Each caster has his own aura slot, instead of replacing others + }; + enum SpellTarget { + TARGET_NONE = 1, + TARGET_UNIT_CASTER = 2, + TARGET_UNIT_ENEMY_NEAR_CASTER = 3, + TARGET_UNIT_FRIEND_NEAR_CASTER = 4, + TARGET_UNIT_NEAR_CASTER = 5, + TARGET_UNIT_CASTER_PET = 6, + TARGET_UNIT_ENEMY = 7, + TARGET_ENUM_UNITS_SCRIPT_AOE_AT_SRC_LOC = 8, + TARGET_ENUM_UNITS_SCRIPT_AOE_AT_DEST_LOC = 9, + TARGET_LOCATION_CASTER_HOME_BIND = 10, + TARGET_LOCATION_CASTER_DIVINE_BIND_NYI = 11, + TARGET_PLAYER_NYI = 12, + TARGET_PLAYER_NEAR_CASTER_NYI = 13, + TARGET_PLAYER_ENEMY_NYI = 14, + TARGET_PLAYER_FRIEND_NYI = 15, + TARGET_ENUM_UNITS_ENEMY_AOE_AT_SRC_LOC = 16, + TARGET_ENUM_UNITS_ENEMY_AOE_AT_DEST_LOC = 17, + TARGET_LOCATION_DATABASE = 18, + TARGET_LOCATION_CASTER_DEST = 19, + TARGET_UNK_19 = 20, + TARGET_ENUM_UNITS_PARTY_WITHIN_CASTER_RANGE = 21, + TARGET_UNIT_FRIEND = 22, + TARGET_LOCATION_CASTER_SRC = 23, + TARGET_GAMEOBJECT = 24, + TARGET_ENUM_UNITS_ENEMY_IN_CONE_24 = 25, + TARGET_UNIT = 26, + TARGET_LOCKED = 27, + TARGET_UNIT_CASTER_MASTER = 28, + TARGET_ENUM_UNITS_ENEMY_AOE_AT_DYNOBJ_LOC = 29, + TARGET_ENUM_UNITS_FRIEND_AOE_AT_DYNOBJ_LOC = 30, + TARGET_ENUM_UNITS_FRIEND_AOE_AT_SRC_LOC = 31, + TARGET_ENUM_UNITS_FRIEND_AOE_AT_DEST_LOC = 32, + TARGET_LOCATION_UNIT_MINION_POSITION = 33, + TARGET_ENUM_UNITS_PARTY_AOE_AT_SRC_LOC = 34, + TARGET_ENUM_UNITS_PARTY_AOE_AT_DEST_LOC = 35, + TARGET_UNIT_PARTY = 36, + TARGET_ENUM_UNITS_ENEMY_WITHIN_CASTER_RANGE = 37, // TODO: only used with dest-effects - reinvestigate naming + TARGET_UNIT_FRIEND_AND_PARTY = 38, + TARGET_UNIT_SCRIPT_NEAR_CASTER = 39, + TARGET_LOCATION_CASTER_FISHING_SPOT = 40, + TARGET_GAMEOBJECT_SCRIPT_NEAR_CASTER = 41, + TARGET_LOCATION_CASTER_FRONT_RIGHT = 42, + TARGET_LOCATION_CASTER_BACK_RIGHT = 43, + TARGET_LOCATION_CASTER_BACK_LEFT = 44, + TARGET_LOCATION_CASTER_FRONT_LEFT = 45, + TARGET_UNIT_FRIEND_CHAIN_HEAL = 46, + TARGET_LOCATION_SCRIPT_NEAR_CASTER = 47, + TARGET_LOCATION_CASTER_FRONT = 48, + TARGET_LOCATION_CASTER_BACK = 49, + TARGET_LOCATION_CASTER_LEFT = 50, + TARGET_LOCATION_CASTER_RIGHT = 51, + TARGET_ENUM_GAMEOBJECTS_SCRIPT_AOE_AT_SRC_LOC = 52, + TARGET_ENUM_GAMEOBJECTS_SCRIPT_AOE_AT_DEST_LOC = 53, + TARGET_LOCATION_CASTER_TARGET_POSITION = 54, + TARGET_ENUM_UNITS_ENEMY_IN_CONE_54 = 55, + TARGET_LOCATION_CASTER_FRONT_LEAP = 56, + TARGET_ENUM_UNITS_RAID_WITHIN_CASTER_RANGE = 57, + TARGET_UNIT_RAID = 58, + TARGET_UNIT_RAID_NEAR_CASTER = 59, + TARGET_ENUM_UNITS_FRIEND_IN_CONE = 60, + TARGET_ENUM_UNITS_SCRIPT_IN_CONE_60 = 61, + TARGET_UNIT_RAID_AND_CLASS = 62, + TARGET_PLAYER_RAID_NYI = 63, + TARGET_LOCATION_UNIT_POSITION = 64, + }; + + enum Events : std::uint32_t { + UNIT_NAME_UPDATE = 183, + UNIT_PORTRAIT_UPDATE = 184, + UNIT_MODEL_CHANGED = 185, + UNIT_INVENTORY_CHANGED = 186, + UNIT_CLASSIFICATION_CHANGED = 187, + ITEM_LOCK_CHANGED = 188, + PLAYER_XP_UPDATE = 189, + PLAYER_REGEN_DISABLED = 190, + PLAYER_REGEN_ENABLED = 191, + PLAYER_AURAS_CHANGED = 192, + PLAYER_ENTER_COMBAT = 193, + PLAYER_LEAVE_COMBAT = 194, + PLAYER_TARGET_CHANGED = 195, + PLAYER_CONTROL_LOST = 196, + PLAYER_CONTROL_GAINED = 197, + PLAYER_FARSIGHT_FOCUS_CHANGED = 198, + PLAYER_LEVEL_UP = 199, + PLAYER_MONEY = 200, + PLAYER_DAMAGE_DONE_MODS = 201, + PLAYER_COMBO_POINTS = 202, + ZONE_CHANGED = 203, + ZONE_CHANGED_INDOORS = 204, + ZONE_CHANGED_NEW_AREA = 205, + MINIMAP_ZONE_CHANGED = 206, + MINIMAP_UPDATE_ZOOM = 207, + SCREENSHOT_SUCCEEDED = 208, + SCREENSHOT_FAILED = 209, + ACTIONBAR_SHOWGRID = 210, + ACTIONBAR_HIDEGRID = 211, + ACTIONBAR_PAGE_CHANGED = 212, + ACTIONBAR_SLOT_CHANGED = 213, + ACTIONBAR_UPDATE_STATE = 214, + ACTIONBAR_UPDATE_USABLE = 215, + ACTIONBAR_UPDATE_COOLDOWN = 216, + UPDATE_BONUS_ACTIONBAR = 217, + PARTY_MEMBERS_CHANGED = 218, + PARTY_LEADER_CHANGED = 219, + PARTY_MEMBER_ENABLE = 220, + PARTY_MEMBER_DISABLE = 221, + PARTY_LOOT_METHOD_CHANGED = 222, + SYSMSG = 223, + UI_ERROR_MESSAGE = 224, + UI_INFO_MESSAGE = 225, + UPDATE_CHAT_COLOR = 226, + CHAT_MSG_ADDON = 227, + CHAT_MSG_SAY = 228, + CHAT_MSG_PARTY = 229, + CHAT_MSG_RAID = 230, + CHAT_MSG_GUILD = 231, + CHAT_MSG_OFFICER = 232, + CHAT_MSG_YELL = 233, + CHAT_MSG_WHISPER = 234, + CHAT_MSG_WHISPER_INFORM = 235, + CHAT_MSG_EMOTE = 236, + CHAT_MSG_TEXT_EMOTE = 237, + CHAT_MSG_SYSTEM = 238, + CHAT_MSG_MONSTER_SAY = 239, + CHAT_MSG_MONSTER_YELL = 240, + CHAT_MSG_MONSTER_WHISPER = 241, + CHAT_MSG_MONSTER_EMOTE = 242, + CHAT_MSG_CHANNEL = 243, + CHAT_MSG_CHANNEL_JOIN = 244, + CHAT_MSG_CHANNEL_LEAVE = 245, + CHAT_MSG_CHANNEL_LIST = 246, + CHAT_MSG_CHANNEL_NOTICE = 247, + CHAT_MSG_CHANNEL_NOTICE_USER = 248, + CHAT_MSG_AFK = 249, + CHAT_MSG_DND = 250, + CHAT_MSG_COMBAT_LOG = 251, + CHAT_MSG_IGNORED = 252, + CHAT_MSG_SKILL = 253, + CHAT_MSG_LOOT = 254, + CHAT_MSG_MONEY = 255, + CHAT_MSG_RAID_LEADER = 256, + CHAT_MSG_RAID_WARNING = 257, + LANGUAGE_LIST_CHANGED = 258, + TIME_PLAYED_MSG = 259, + SPELLS_CHANGED = 260, + CURRENT_SPELL_CAST_CHANGED = 261, + SPELL_UPDATE_COOLDOWN = 262, + SPELL_UPDATE_USABLE = 263, + CHARACTER_POINTS_CHANGED = 264, + SKILL_LINES_CHANGED = 265, + ITEM_PUSH = 266, + LOOT_OPENED = 267, + LOOT_SLOT_CLEARED = 268, + LOOT_CLOSED = 269, + PLAYER_LOGIN = 270, + PLAYER_LOGOUT = 271, + PLAYER_ENTERING_WORLD = 272, + PLAYER_LEAVING_WORLD = 273, + PLAYER_ALIVE = 274, + PLAYER_DEAD = 275, + PLAYER_CAMPING = 276, + PLAYER_QUITING = 277, + LOGOUT_CANCEL = 278, + RESURRECT_REQUEST = 279, + PARTY_INVITE_REQUEST = 280, + PARTY_INVITE_CANCEL = 281, + GUILD_INVITE_REQUEST = 282, + GUILD_INVITE_CANCEL = 283, + GUILD_MOTD = 284, + TRADE_REQUEST = 285, + TRADE_REQUEST_CANCEL = 286, + LOOT_BIND_CONFIRM = 287, + EQUIP_BIND_CONFIRM = 288, + AUTOEQUIP_BIND_CONFIRM = 289, + USE_BIND_CONFIRM = 290, + DELETE_ITEM_CONFIRM = 291, + CURSOR_UPDATE = 292, + ITEM_TEXT_BEGIN = 293, + ITEM_TEXT_TRANSLATION = 294, + ITEM_TEXT_READY = 295, + ITEM_TEXT_CLOSED = 296, + GOSSIP_SHOW = 297, + GOSSIP_ENTER_CODE = 298, + GOSSIP_CLOSED = 299, + QUEST_GREETING = 300, + QUEST_DETAIL = 301, + QUEST_PROGRESS = 302, + QUEST_COMPLETE = 303, + QUEST_FINISHED = 304, + QUEST_ITEM_UPDATE = 305, + TAXIMAP_OPENED = 306, + TAXIMAP_CLOSED = 307, + QUEST_LOG_UPDATE = 308, + TRAINER_SHOW = 309, + TRAINER_UPDATE = 310, + TRAINER_CLOSED = 311, + CVAR_UPDATE = 312, + TRADE_SKILL_SHOW = 313, + TRADE_SKILL_UPDATE = 314, + TRADE_SKILL_CLOSE = 315, + MERCHANT_SHOW = 316, + MERCHANT_UPDATE = 317, + MERCHANT_CLOSED = 318, + TRADE_SHOW = 319, + TRADE_CLOSED = 320, + TRADE_UPDATE = 321, + TRADE_ACCEPT_UPDATE = 322, + TRADE_TARGET_ITEM_CHANGED = 323, + TRADE_PLAYER_ITEM_CHANGED = 324, + TRADE_MONEY_CHANGED = 325, + PLAYER_TRADE_MONEY = 326, + BAG_OPEN = 327, + BAG_UPDATE = 328, + BAG_CLOSED = 329, + BAG_UPDATE_COOLDOWN = 330, + LOCALPLAYER_PET_RENAMED = 331, + UNIT_ATTACK = 332, + UNIT_DEFENSE = 333, + PET_ATTACK_START = 334, + PET_ATTACK_STOP = 335, + UPDATE_MOUSEOVER_UNIT = 336, + SPELLCAST_START = 337, + SPELLCAST_STOP = 338, + SPELLCAST_FAILED = 339, + SPELLCAST_INTERRUPTED = 340, + SPELLCAST_DELAYED = 341, + SPELLCAST_CHANNEL_START = 342, + SPELLCAST_CHANNEL_UPDATE = 343, + SPELLCAST_CHANNEL_STOP = 344, + PLAYER_GUILD_UPDATE = 345, + QUEST_ACCEPT_CONFIRM = 346, + PLAYERBANKSLOTS_CHANGED = 347, + BANKFRAME_OPENED = 348, + BANKFRAME_CLOSED = 349, + PLAYERBANKBAGSLOTS_CHANGED = 350, + FRIENDLIST_UPDATE = 351, + IGNORELIST_UPDATE = 352, + PET_BAR_UPDATE_COOLDOWN = 354, + PET_BAR_UPDATE = 353, + PET_BAR_SHOWGRID = 355, + PET_BAR_HIDEGRID = 356, + MINIMAP_PING = 357, + CHAT_MSG_COMBAT_MISC_INFO = 358, + CRAFT_SHOW = 359, + CRAFT_UPDATE = 360, + CRAFT_CLOSE = 361, + MIRROR_TIMER_START = 362, + MIRROR_TIMER_PAUSE = 363, + MIRROR_TIMER_STOP = 364, + WORLD_MAP_UPDATE = 365, + WORLD_MAP_NAME_UPDATE = 366, + AUTOFOLLOW_BEGIN = 367, + AUTOFOLLOW_END = 368, + CINEMATIC_START = 370, + CINEMATIC_STOP = 371, + UPDATE_FACTION = 372, + CLOSE_WORLD_MAP = 373, + OPEN_TABARD_FRAME = 374, + CLOSE_TABARD_FRAME = 375, + SHOW_COMPARE_TOOLTIP = 377, + TABARD_CANSAVE_CHANGED = 376, + GUILD_REGISTRAR_SHOW = 378, + GUILD_REGISTRAR_CLOSED = 379, + DUEL_REQUESTED = 380, + DUEL_OUTOFBOUNDS = 381, + DUEL_INBOUNDS = 382, + DUEL_FINISHED = 383, + TUTORIAL_TRIGGER = 384, + PET_DISMISS_START = 385, + UPDATE_BINDINGS = 386, + UPDATE_SHAPESHIFT_FORMS = 387, + WHO_LIST_UPDATE = 388, + UPDATE_LFG = 389, + PETITION_SHOW = 390, + PETITION_CLOSED = 391, + EXECUTE_CHAT_LINE = 392, + UPDATE_MACROS = 393, + UPDATE_TICKET = 394, + UPDATE_CHAT_WINDOWS = 395, + CONFIRM_XP_LOSS = 396, + CORPSE_IN_RANGE = 397, + CORPSE_IN_INSTANCE = 398, + CORPSE_OUT_OF_RANGE = 399, + UPDATE_GM_STATUS = 400, + PLAYER_UNGHOST = 401, + BIND_ENCHANT = 402, + REPLACE_ENCHANT = 403, + TRADE_REPLACE_ENCHANT = 404, + PLAYER_UPDATE_RESTING = 405, + UPDATE_EXHAUSTION = 406, + PLAYER_FLAGS_CHANGED = 407, + GUILD_ROSTER_UPDATE = 408, + GM_PLAYER_INFO = 409, + MAIL_SHOW = 410, + MAIL_CLOSED = 411, + SEND_MAIL_MONEY_CHANGED = 412, + SEND_MAIL_COD_CHANGED = 413, + MAIL_SEND_INFO_UPDATE = 414, + MAIL_SEND_SUCCESS = 415, + MAIL_INBOX_UPDATE = 416, + BATTLEFIELDS_SHOW = 417, + BATTLEFIELDS_CLOSED = 418, + UPDATE_BATTLEFIELD_STATUS = 419, + UPDATE_BATTLEFIELD_SCORE = 420, + AUCTION_HOUSE_SHOW = 421, + AUCTION_HOUSE_CLOSED = 422, + NEW_AUCTION_UPDATE = 423, + AUCTION_ITEM_LIST_UPDATE = 424, + AUCTION_OWNED_LIST_UPDATE = 425, + AUCTION_BIDDER_LIST_UPDATE = 426, + PET_UI_UPDATE = 427, + PET_UI_CLOSE = 428, + ADDON_LOADED = 429, + VARIABLES_LOADED = 430, + MACRO_ACTION_FORBIDDEN = 431, + ADDON_ACTION_FORBIDDEN = 432, + MEMORY_EXHAUSTED = 433, + MEMORY_RECOVERED = 434, + START_AUTOREPEAT_SPELL = 435, + STOP_AUTOREPEAT_SPELL = 436, + PET_STABLE_SHOW = 437, + PET_STABLE_UPDATE = 438, + PET_STABLE_UPDATE_PAPERDOLL = 439, + PET_STABLE_CLOSED = 440, + CHAT_MSG_COMBAT_SELF_HITS = 441, + CHAT_MSG_COMBAT_SELF_MISSES = 442, + CHAT_MSG_COMBAT_PET_HITS = 443, + CHAT_MSG_COMBAT_PET_MISSES = 444, + CHAT_MSG_COMBAT_PARTY_HITS = 445, + CHAT_MSG_COMBAT_PARTY_MISSES = 446, + CHAT_MSG_COMBAT_FRIENDLYPLAYER_HITS = 447, + CHAT_MSG_COMBAT_FRIENDLYPLAYER_MISSES = 448, + CHAT_MSG_COMBAT_HOSTILEPLAYER_HITS = 449, + CHAT_MSG_COMBAT_HOSTILEPLAYER_MISSES = 450, + CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS = 451, + CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES = 452, + CHAT_MSG_COMBAT_CREATURE_VS_PARTY_HITS = 453, + CHAT_MSG_COMBAT_CREATURE_VS_PARTY_MISSES = 454, + CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_HITS = 455, + CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_MISSES = 456, + CHAT_MSG_COMBAT_FRIENDLY_DEATH = 457, + CHAT_MSG_COMBAT_HOSTILE_DEATH = 458, + CHAT_MSG_COMBAT_XP_GAIN = 459, + CHAT_MSG_COMBAT_HONOR_GAIN = 460, + CHAT_MSG_SPELL_SELF_DAMAGE = 461, + CHAT_MSG_SPELL_SELF_BUFF = 462, + CHAT_MSG_SPELL_PET_DAMAGE = 463, + CHAT_MSG_SPELL_PET_BUFF = 464, + CHAT_MSG_SPELL_PARTY_DAMAGE = 465, + CHAT_MSG_SPELL_PARTY_BUFF = 466, + CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE = 467, + CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF = 468, + CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE = 469, + CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF = 470, + CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE = 471, + CHAT_MSG_SPELL_CREATURE_VS_SELF_BUFF = 472, + CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE = 473, + CHAT_MSG_SPELL_CREATURE_VS_PARTY_BUFF = 474, + CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE = 475, + CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF = 476, + CHAT_MSG_SPELL_TRADESKILLS = 477, + CHAT_MSG_SPELL_DAMAGESHIELDS_ON_SELF = 478, + CHAT_MSG_SPELL_DAMAGESHIELDS_ON_OTHERS = 479, + CHAT_MSG_SPELL_AURA_GONE_SELF = 480, + CHAT_MSG_SPELL_AURA_GONE_PARTY = 481, + CHAT_MSG_SPELL_AURA_GONE_OTHER = 482, + CHAT_MSG_SPELL_ITEM_ENCHANTMENTS = 483, + CHAT_MSG_SPELL_BREAK_AURA = 484, + CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE = 485, + CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS = 486, + CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE = 487, + CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS = 488, + CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE = 489, + CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS = 490, + CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE = 491, + CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS = 492, + CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE = 493, + CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS = 494, + CHAT_MSG_SPELL_FAILED_LOCALPLAYER = 495, + CHAT_MSG_BG_SYSTEM_NEUTRAL = 496, + CHAT_MSG_BG_SYSTEM_ALLIANCE = 497, + CHAT_MSG_BG_SYSTEM_HORDE = 498, + RAID_ROSTER_UPDATE = 499, + UPDATE_PENDING_MAIL = 500, + UPDATE_INVENTORY_ALERTS = 501, + UPDATE_TRADESKILL_RECAST = 502, + OPEN_MASTER_LOOT_LIST = 503, + UPDATE_MASTER_LOOT_LIST = 504, + START_LOOT_ROLL = 505, + CANCEL_LOOT_ROLL = 506, + CONFIRM_LOOT_ROLL = 507, + INSTANCE_BOOT_START = 508, + INSTANCE_BOOT_STOP = 509, + LEARNED_SPELL_IN_TAB = 510, + DISPLAY_SIZE_CHANGED = 511, + CONFIRM_TALENT_WIPE = 512, + CONFIRM_BINDER = 513, + MAIL_FAILED = 514, + CLOSE_INBOX_ITEM = 515, + CONFIRM_SUMMON = 516, + BILLING_NAG_DIALOG = 517, + IGR_BILLING_NAG_DIALOG = 518, + MEETINGSTONE_CHANGED = 519, + PLAYER_SKINNED = 520, + TABARD_SAVE_PENDING = 521, + UNIT_QUEST_LOG_CHANGED = 522, + PLAYER_PVP_KILLS_CHANGED = 523, + PLAYER_PVP_RANK_CHANGED = 524, + INSPECT_HONOR_UPDATE = 525, + UPDATE_WORLD_STATES = 526, + AREA_SPIRIT_HEALER_IN_RANGE = 527, + AREA_SPIRIT_HEALER_OUT_OF_RANGE = 528, + CONFIRM_PET_UNLEARN = 529, + PLAYTIME_CHANGED = 530, + UPDATE_LFG_TYPES = 531, + UPDATE_LFG_LIST = 532, + CHAT_MSG_COMBAT_FACTION_CHANGE = 533, + START_MINIGAME = 534, + MINIGAME_UPDATE = 535, + READY_CHECK = 536, + RAID_TARGET_UPDATE = 537, + GMSURVEY_DISPLAY = 538, + UPDATE_INSTANCE_INFO = 539, + CHAT_MSG_RAID_BOSS_EMOTE = 541, + COMBAT_TEXT_UPDATE = 542, + LOTTERY_SHOW = 543, + CHAT_MSG_FILTERED = 544, + QUEST_WATCH_UPDATE = 545, + CHAT_MSG_BATTLEGROUND = 546, + CHAT_MSG_BATTLEGROUND_LEADER = 547, + LOTTERY_ITEM_UPDATE = 548, + + // ADDED EVENTS + SPELL_QUEUE_EVENT = 369, + SPELL_CAST_EVENT = 540, + SPELL_DAMAGE_EVENT_SELF = 549, + SPELL_DAMAGE_EVENT_OTHER = 550, + }; + + enum TypeMask { + TYPEMASK_OBJECT = 0x1, + TYPEMASK_ITEM = 0x2, + TYPEMASK_CONTAINER = 0x4, + TYPEMASK_UNIT = 0x8, + TYPEMASK_PLAYER = 0x10, + TYPEMASK_GAMEOBJECT = 0x20, + TYPEMASK_DYNAMICOBJECT = 0x40, + TYPEMASK_CORPSE = 0x80, + }; + + enum SpellModOp { + SPELLMOD_DAMAGE = 0, + SPELLMOD_DURATION = 1, + SPELLMOD_THREAT = 2, + SPELLMOD_ATTACK_POWER = 3, + SPELLMOD_CHARGES = 4, + SPELLMOD_RANGE = 5, + SPELLMOD_RADIUS = 6, + SPELLMOD_CRITICAL_CHANCE = 7, + SPELLMOD_ALL_EFFECTS = 8, + SPELLMOD_NOT_LOSE_CASTING_TIME = 9, + SPELLMOD_CASTING_TIME = 10, + SPELLMOD_COOLDOWN = 11, + SPELLMOD_SPEED = 12, + SPELLMOD_COST = 14, + SPELLMOD_CRIT_DAMAGE_BONUS = 15, + SPELLMOD_RESIST_MISS_CHANCE = 16, + SPELLMOD_JUMP_TARGETS = 17, + SPELLMOD_CHANCE_OF_SUCCESS = 18, // Only used with SPELL_AURA_ADD_FLAT_MODIFIER and affects proc spells + SPELLMOD_ACTIVATION_TIME = 19, + SPELLMOD_EFFECT_PAST_FIRST = 20, + SPELLMOD_CASTING_TIME_OLD = 21, + SPELLMOD_DOT = 22, + SPELLMOD_HASTE = 23, + SPELLMOD_SPELL_BONUS_DAMAGE = 24, + SPELLMOD_MULTIPLE_VALUE = 27, + SPELLMOD_RESIST_DISPEL_CHANCE = 28, + MAX_SPELLMOD = 29, + }; + + + enum OBJECT_TYPE_ID : __int32 { + ID_OBJECT = 0x0, + ID_ITEM = 0x1, + ID_CONTAINER = 0x2, + ID_UNIT = 0x3, + ID_PLAYER = 0x4, + ID_GAMEOBJECT = 0x5, + ID_DYNAMICOBJECT = 0x6, + ID_CORPSE = 0x7, + NUM_CLIENT_OBJECT_TYPES = 0x8, + ID_AIGROUP = 0x8, + ID_AREATRIGGER = 0x9, + NUM_OBJECT_TYPES = 0xA, + }; + + class CDuration { + public: + char m_DurationIndex; //0x0000 + __int32 m_Duration; //0x0004 + char unknown[4]; //0x0008 + __int32 m_Duration2; //0x000C + + __int32 GetDuration() { + return ((m_Duration / 1000) / 60); + } + };//Size=0x0010 + + class CSpellCastingTime { + public: + __int32 m_CastingTimeIndex; //0x0000 + __int32 m_CastTime; //0x0004 + char m_0x0008[4]; //0x0008 + __int32 m_CastTime2; //0x000C + + };//Size=0x0010 + + typedef struct UnitFields { + uint64_t charm; // Size:2 + uint64_t summon; // Size:2 + uint64_t charmedBy; // Size:2 + uint64_t summonedBy; // Size:2 + uint64_t createdBy; // Size:2 + uint64_t target; // Size:2 + uint64_t persuaded; // Size:2 + uint64_t channelObject; // Size:2 + uint32_t health; // Size:1 + uint32_t power1; // Size:1 + uint32_t power2; // Size:1 + uint32_t power3; // Size:1 + uint32_t power4; // Size:1 + uint32_t power5; // Size:1 + uint32_t maxHealth; // Size:1 + uint32_t maxPower1; // Size:1 + uint32_t maxPower2; // Size:1 + uint32_t maxPower3; // Size:1 + uint32_t maxPower4; // Size:1 + uint32_t maxPower5; // Size:1 + uint32_t level; // Size:1 + uint32_t factionTemplate; // Size:1 + uint32_t bytes0; // Size:1 + uint32_t virtualItemDisplay[3]; // Size:3 + uint32_t virtualItemInfo[6]; // Size:6 + uint32_t flags; // Size:1 + uint32_t aura[48]; // Size:48 + uint32_t auraFlags[6]; // Size:6 + uint32_t auraLevels[12]; // Size:12 + uint32_t auraApplications[12]; // Size:12 + uint32_t auraState; // Size:1 + uint32_t baseAttackTime; // Size:1 + uint32_t offhandAttackTime; // Size:1 + uint32_t rangedAttackTime; // Size:1 + float boundingRadius; // Size:1 + float combatReach; // Size:1 + uint32_t displayId; // Size:1 + uint32_t nativeDisplayId; // Size:1 + uint32_t mountDisplayId; // Size:1 + float minDamage; // Size:1 + float maxDamage; // Size:1 + float minOffhandDamage; // Size:1 + float maxOffhandDamage; // Size:1 + uint32_t bytes1; // Size:1 + uint32_t petNumber; // Size:1 + uint32_t petNameTimestamp; // Size:1 + uint32_t petExperience; // Size:1 + uint32_t petNextLevelExp; // Size:1 + uint32_t dynamicFlags; // Size:1 + uint32_t channelSpell; // Size:1 + float modCastSpeed; // Size:1 (Float in 1.12+) + uint32_t createdBySpell; // Size:1 + uint32_t npcFlags; // Size:1 + uint32_t npcEmoteState; // Size:1 + uint32_t trainingPoints; // Size:1 + uint32_t stat0; // Size:1 + uint32_t stat1; // Size:1 + uint32_t stat2; // Size:1 + uint32_t stat3; // Size:1 + uint32_t stat4; // Size:1 + uint32_t resistances[7]; // Size:7 + uint32_t baseMana; // Size:1 + uint32_t baseHealth; // Size:1 + uint32_t bytes2; // Size:1 + uint32_t attackPower; // Size:1 + uint32_t attackPowerMods; // Size:1 + float attackPowerMultiplier; // Size:1 + uint32_t rangedAttackPower; // Size:1 + uint32_t rangedAttackPowerMods; // Size:1 + float rangedAttackPowerMultiplier; // Size:1 + float minRangedDamage; // Size:1 + float maxRangedDamage; // Size:1 + float powerCostModifier[7]; // Size:7 + float powerCostMultiplier[7]; // Size:7 + } UnitFields; + + uintptr_t *GetObjectPtr(std::uint64_t guid); + + std::uint32_t GetCastTime(void *unit, uint32_t spellId); + + CDuration *GetDurationObject(uint32_t durationIndex); + + int GetSpellDuration(const SpellRec *spellRec, bool ignoreModifiers); + + int GetSpellModifier(const SpellRec *spellRec, SpellModOp spellMod); + + const SpellRec *GetSpellInfo(uint32_t spellId); + + uint32_t GetItemId(CGItem_C *item); + + const char *GetSpellName(uint32_t spellId); + + std::uint64_t ClntObjMgrGetActivePlayerGuid(); + + std::uint64_t GetCurrentTargetGuid(); + + uintptr_t *ClntObjMgrObjectPtr(TypeMask typeMask, std::uint64_t guid); + + uint64_t UnitGetGuid(uintptr_t *unit); + + uint64_t UnitGetTargetGuid(uintptr_t *unit); + +} \ No newline at end of file diff --git a/nampower/helper.cpp b/nampower/helper.cpp new file mode 100644 index 0000000..12dd307 --- /dev/null +++ b/nampower/helper.cpp @@ -0,0 +1,125 @@ +// +// Created by pmacc on 9/21/2024. +// + +#include "helper.hpp" +#include "offsets.hpp" +#include "main.hpp" + +namespace Nampower { + bool SpellIsOnGcd(const game::SpellRec *spell) { + if (spell->Id == 51714) { + // power overwhelming gcd removed but client not updated + return false; + } + + return spell->StartRecoveryCategory == 133; + } + + bool SpellIsChanneling(const game::SpellRec *spell) { + return spell->AttributesEx & game::SPELL_ATTR_EX_IS_CHANNELED || + spell->AttributesEx & game::SPELL_ATTR_EX_IS_SELF_CHANNELED; + } + + bool SpellIsTargeting(const game::SpellRec *spell) { + return spell->Targets == game::SpellTarget::TARGET_LOCATION_UNIT_POSITION; + } + + bool SpellIsOnSwing(const game::SpellRec *spell) { + return spell->Attributes & game::SPELL_ATTR_ON_NEXT_SWING_1; + } + + bool SpellIsAttackTradeskillOrEnchant(const game::SpellRec *spell) { + return ( + spell->Effect[0] == game::SpellEffects::SPELL_EFFECT_ATTACK || + spell->Attributes & game::SpellAttributes::SPELL_ATTR_TRADESPELL || + spell->Effect[0] == game::SpellEffects::SPELL_EFFECT_TRADE_SKILL || + spell->Effect[0] == game::SpellEffects::SPELL_EFFECT_TRANS_DOOR || + spell->Effect[0] == game::SpellEffects::SPELL_EFFECT_ENCHANT_ITEM || + spell->Effect[0] == game::SpellEffects::SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY || + spell->Effect[0] == game::SpellEffects::SPELL_EFFECT_CREATE_ITEM || + spell->Effect[0] == game::SpellEffects::SPELL_EFFECT_OPEN_LOCK || + spell->Effect[0] == game::SpellEffects::SPELL_EFFECT_OPEN_LOCK_ITEM); + } + + // if the spell is off cooldown, this will return the gcd, otherwise the cooldown + uint32_t GetGcdOrCooldownForSpell(uint32_t spellId) { + uint32_t duration; + uint64_t startTime; + uint32_t enable; + + auto const getSpellCooldown = reinterpret_cast(Offsets::Spell_C_GetSpellCooldown); + getSpellCooldown(spellId, 0, &duration, &startTime, &enable); + + if (spellId == 51714 && duration == 1500) { + // power overwhelming gcd removed but client not updated + return 0; + } + + return duration; + } + + uint32_t GetRemainingGcdOrCooldownForSpell(uint32_t spellId) { + uint32_t duration; + uint64_t startTime; + uint32_t enable; + + auto const getSpellCooldown = reinterpret_cast(Offsets::Spell_C_GetSpellCooldown); + getSpellCooldown(spellId, 0, &duration, &startTime, &enable); + startTime = startTime & 0XFFFFFFFF; // only look at same bits that lua does + + if (startTime != 0) { + auto currentLuaTime = GetWowTimeMs() & 0XFFFFFFFF; + auto remaining = (startTime + duration) - currentLuaTime; + return uint32_t(remaining); + } + + return 0; + } + + uint32_t GetRemainingCooldownForSpell(uint32_t spellId) { + uint32_t duration; + uint64_t startTime; + uint32_t enable; + + auto const getSpellCooldown = reinterpret_cast(Offsets::Spell_C_GetSpellCooldown); + getSpellCooldown(spellId, 0, &duration, &startTime, &enable); + + startTime = startTime & 0XFFFFFFFF; // only look at same bits that lua does + + // ignore gcd cooldown by looking for duration > 1.5 + if (startTime != 0 && duration > 1.5) { + auto currentLuaTime = GetWowTimeMs() & 0XFFFFFFFF; + auto remaining = (startTime + duration) - currentLuaTime; + return uint32_t(remaining); + } + + return 0; + } + + bool IsSpellOnCooldown(uint32_t spellId) { + uint32_t duration; + uint64_t startTime; + uint32_t enable; + + auto const getSpellCooldown = reinterpret_cast(Offsets::Spell_C_GetSpellCooldown); + getSpellCooldown(spellId, 0, &duration, &startTime, &enable); + + return startTime != 0 && duration > 1.5; + } + + char *ConvertGuidToString(uint64_t guid) { + char *guidStr = new char[21]; // 2 for 0x prefix, 18 for the number, and 1 for '\0' + std::snprintf(guidStr, 21, "0x%016llX", static_cast(guid)); + return guidStr; + } + + float GetNameplateDistance() { + auto const distanceSquared = *reinterpret_cast(Offsets::NameplateDistance); + return sqrtf(distanceSquared); + } + + void SetNameplateDistance(float distance) { + *reinterpret_cast(Offsets::NameplateDistance) = distance * distance; + } +} \ No newline at end of file diff --git a/nampower/helper.hpp b/nampower/helper.hpp new file mode 100644 index 0000000..91eee88 --- /dev/null +++ b/nampower/helper.hpp @@ -0,0 +1,34 @@ +// +// Created by pmacc on 9/21/2024. +// + +#pragma once + +#include "game.hpp" + +namespace Nampower { + + bool SpellIsOnGcd(const game::SpellRec *spell); + + bool SpellIsChanneling(const game::SpellRec *spell); + + bool SpellIsTargeting(const game::SpellRec *spell); + + bool SpellIsOnSwing(const game::SpellRec *spell); + + bool SpellIsAttackTradeskillOrEnchant(const game::SpellRec *spell); + + uint32_t GetGcdOrCooldownForSpell(uint32_t spellId); + + uint32_t GetRemainingGcdOrCooldownForSpell(uint32_t spellId); + + uint32_t GetRemainingCooldownForSpell(uint32_t spellId); + + bool IsSpellOnCooldown(uint32_t spellId); + + char *ConvertGuidToString(uint64_t guid); + + float GetNameplateDistance(); + + void SetNameplateDistance(float distance); +} \ No newline at end of file diff --git a/nampower/logging.cpp b/nampower/logging.cpp new file mode 100644 index 0000000..6f2b19b --- /dev/null +++ b/nampower/logging.cpp @@ -0,0 +1,7 @@ + +#include + +namespace Nampower { + std::ofstream debugLogFile; + uint32_t gStartTime; +} \ No newline at end of file diff --git a/nampower/logging.hpp b/nampower/logging.hpp new file mode 100644 index 0000000..6b1eb46 --- /dev/null +++ b/nampower/logging.hpp @@ -0,0 +1,33 @@ +// +// Created by pmacc on 9/29/2024. +// + +#pragma once + +#include +#include +#include + +namespace Nampower { + extern std::ofstream debugLogFile; + + extern uint32_t gStartTime; + extern uint32_t GetTime(); + extern std::string GetHumanReadableTime(); + +#ifndef DEBUG_LOG_H +#define DEBUG_LOG_H + +// TODO uncomment once ready for release +//#ifdef _DEBUG +//std::ofstream debugLogFile("nampower_debug.log"); +//#define DEBUG_LOG(msg) debugLogFile << "[DEBUG]" << GetTime() << ": " << msg << std::endl +//#else +//#define DEBUG_LOG(msg) // No-op in release mode +//#endif + +#define DEBUG_LOG(msg) debugLogFile << "[DEBUG]" << GetHumanReadableTime() << ": " << msg << std::endl +#define DEBUG_LOG2(msg) debugLogFile << msg + +#endif // DEBUG_LOG_H +} \ No newline at end of file diff --git a/nampower/main.cpp b/nampower/main.cpp new file mode 100644 index 0000000..e6c2dec --- /dev/null +++ b/nampower/main.cpp @@ -0,0 +1,1124 @@ +/* + Copyright (c) 2017-2023, namreeb (legal@namreeb.org) + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The views and conclusions contained in the software and documentation are those + of the authors and should not be interpreted as representing official policies, + either expressed or implied, of the FreeBSD Project. +*/ + +#include "logging.hpp" +#include "offsets.hpp" +#include "game.hpp" +#include "main.hpp" +#include "spellevents.hpp" +#include "spellcast.hpp" +#include "scripts.hpp" +#include "spellchannel.hpp" +#include "helper.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +BOOL WINAPI DllMain(HINSTANCE, uint32_t, void *); + +namespace Nampower { + uint32_t gLastErrorTimeMs; + uint32_t gLastBufferIncreaseTimeMs; + uint32_t gLastBufferDecreaseTimeMs; + + uint32_t gBufferTimeMs; // adjusts dynamically depending on errors + + bool gForceQueueCast; + bool gNoQueueCast; + + bool lastCastUsedServerDelay; + + uint64_t gNextCastId = 1; + + uint32_t gRunningAverageLatencyMs; + uint32_t gLastServerSpellDelayMs; + + hadesmem::PatchDetourBase *castSpellDetour; + + UserSettings gUserSettings; + + LastCastData gLastCastData; + CastData gCastData; + + CastSpellParams gLastNormalCastParams; + CastSpellParams gLastOnSwingCastParams; + + CastQueue gNonGcdCastQueue = CastQueue(6); + + CastQueue gCastHistory = CastQueue(30); + + + std::unique_ptr> gSpellVisualsInitDetour; + std::unique_ptr> gLoadScriptFunctionsDetour; + std::unique_ptr> gCreateEventsDetour; + + std::unique_ptr> gSetCVarDetour; + std::unique_ptr> gCastDetour; + std::unique_ptr> gSendCastDetour; + std::unique_ptr> gCancelSpellDetour; + std::unique_ptr> gSignalEventDetour; + std::unique_ptr> gSpellFailedDetour; + std::unique_ptr gCastbarPatch; + std::unique_ptr> gIEndSceneDetour; + std::unique_ptr> gSpell_C_GetAutoRepeatingSpellDetour; + std::unique_ptr> gSpell_C_CooldownEventTriggeredDetour; + std::unique_ptr> gSpellGoDetour; + std::unique_ptr> gSpellTargetUnitDetour; + std::unique_ptr> gSpellStopCastingDetour; + std::unique_ptr> gCastSpellByNameNoQueueDetour; + std::unique_ptr> gQueueSpellByNameDetour; + std::unique_ptr> qQueueScriptDetour; + std::unique_ptr> gIsSpellInRangeDetour; + std::unique_ptr> gIsSpellUsableDetour; + std::unique_ptr> gGetCurrentCastingInfoDetour; + std::unique_ptr> gGetSpellIdForNameDetour; + std::unique_ptr> gGetSpellNameAndRankForIdDetour; + std::unique_ptr> gGetSpellSlotAndTypeForNameDetour; + std::unique_ptr> gChannelStopCastingNextTickDetour; + std::unique_ptr> gOnSpriteRightClickDetour; + std::unique_ptr> gSpell_C_HandleSpriteClickDetour; + std::unique_ptr> gSpell_C_TargetSpellDetour; + + std::unique_ptr> gGetNampowerVersionDetour; + std::unique_ptr> gGetItemLevelDetour; + + std::unique_ptr> gSpellCooldownDetour; + std::unique_ptr> gSpellDelayedDetour; + std::unique_ptr> gCastResultHandlerDetour; + std::unique_ptr> gSpellFailedHandlerDetour; + std::unique_ptr> gSpellChannelStartHandlerDetour; + std::unique_ptr> gSpellChannelUpdateHandlerDetour; + std::unique_ptr> gPlaySpellVisualHandlerDetour; + + std::unique_ptr> gSpellStartHandlerDetour; + std::unique_ptr> gPeriodicAuraLogHandlerDetour; + std::unique_ptr> gSpellNonMeleeDmgLogHandlerDetour; + + uint32_t GetTime() { + return static_cast(std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch()).count()) - gStartTime; + } + + std::string GetHumanReadableTime() { + auto now = std::chrono::system_clock::now(); + auto in_time_t = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast(now.time_since_epoch()) % 1000; + + std::tm buf; + localtime_s(&buf, &in_time_t); + + std::stringstream ss; + ss << std::put_time(&buf, "%Y-%m-%d %X"); + ss << '.' << std::setfill('0') << std::setw(3) << ms.count(); + return ss.str(); + } + + uint64_t GetWowTimeMs() { + auto const osGetAsyncTimeMs = reinterpret_cast(Offsets::OsGetAsyncTimeMs); + return osGetAsyncTimeMs(); + } + + void LuaCall(const char *code) { + auto function = (LuaCallT) Offsets::lua_call; + function(code, "Unused"); + } + + uintptr_t* GetLuaStatePtr() { + typedef uintptr_t* (__fastcall* GETCONTEXT)(void); + static auto p_GetContext = reinterpret_cast(0x7040D0); + return p_GetContext(); + } + + + void RegisterLuaFunction(char *name, uintptr_t *func) { + DEBUG_LOG("Registering " << name << " to " << func); + auto const registerFunction = reinterpret_cast(Offsets::FrameScript_RegisterFunction); + registerFunction(name, func); + } + + // when the game first launches there won't be a cached latency value for 10-20 seconds and this will return 0 + uint32_t GetLatencyMs() { + auto const getConnection = reinterpret_cast(Offsets::GetClientConnection); + auto const getNetStats = reinterpret_cast(Offsets::GetNetStats); + + auto connectionPtr = getConnection(); + + float bytesIn, bytesOut; + uint32_t latency; + + getNetStats(connectionPtr, &bytesIn, &bytesOut, &latency); + + // update running average + if (latency > 0) { + if (gRunningAverageLatencyMs == 0) { + // first time + gRunningAverageLatencyMs = latency; + // ignore big spikes + } else if (latency < gRunningAverageLatencyMs * 2) { + gRunningAverageLatencyMs = (gRunningAverageLatencyMs * 9 + latency) / 10; + } + } + + return gRunningAverageLatencyMs; + } + + uint32_t GetServerDelayMs() { + // if we have a gLastServerSpellDelayMs that seems reasonable, use it + // occasionally the server will take a long time to respond to a spell cast, in which case default to gBufferTimeMs + // if gLastServerSpellDelayMs == 0, then we don't have a valid value for the last cast + if (gUserSettings.optimizeBufferUsingPacketTimings && !lastCastUsedServerDelay) { + if (gLastServerSpellDelayMs > 0 && gLastServerSpellDelayMs < 200) { + lastCastUsedServerDelay = true; + return gLastServerSpellDelayMs; + } + } + + lastCastUsedServerDelay = false; + + return gBufferTimeMs; + } + + bool InSpellQueueWindow(uint32_t remainingCastTime, uint32_t remainingGcd, bool spellIsTargeting) { + auto currentTime = GetTime(); + + uint32_t queueWindow = 0; + + if (gCastData.channeling) { + if (gUserSettings.queueChannelingSpells) { + if (gCastData.cancelChannelNextTick) { + return true; + } + + auto const remainingChannelTime = (gCastData.channelEndMs > currentTime) ? gCastData.channelEndMs - + currentTime : 0; + return remainingChannelTime < gUserSettings.channelQueueWindowMs; + } + } else if (spellIsTargeting) { + queueWindow = gUserSettings.targetingQueueWindowMs; + } else { + queueWindow = gUserSettings.spellQueueWindowMs; + } + + if (remainingCastTime > 0) { + return remainingCastTime < queueWindow || gForceQueueCast; + } + + if (remainingGcd > 0) { + return remainingGcd < queueWindow || gForceQueueCast; + } + + return false; + } + + bool IsNonSwingSpellQueued() { + return gCastData.nonGcdSpellQueued || gCastData.normalSpellQueued; + } + + uint32_t EffectiveCastEndMs() { + if (gCastData.channeling && gUserSettings.queueChannelingSpells) { + if (gUserSettings.interruptChannelsOutsideQueueWindow) { + auto currentTime = GetTime(); + auto const remainingChannelTime = (gCastData.channelEndMs > currentTime) ? gCastData.channelEndMs - + currentTime : 0; + if (remainingChannelTime < gUserSettings.channelQueueWindowMs) { + return gCastData.channelEndMs; + } + } else { + return gCastData.channelEndMs; + } + } + + return max(gCastData.castEndMs, gCastData.delayEndMs); + } + + void ResetChannelingFlags() { + gCastData.cancelChannelNextTick = false; + + gCastData.channeling = false; + gCastData.channelStartMs = 0; + gCastData.channelEndMs = 0; + gCastData.channelSpellId = 0; + + gCastData.channelTickTimeMs = 0; + gCastData.channelNumTicks = 0; + + } + + void ResetCastFlags() { + // don't reset delayEndMs + gCastData.castEndMs = 0; + gCastData.gcdEndMs = 0; + ResetChannelingFlags(); + } + + void ResetOnSwingFlags() { + gCastData.onSwingQueued = false; + gCastData.onSwingSpellId = 0; + } + + void ClearQueuedSpells() { + if (gCastData.normalSpellQueued || gCastData.cooldownNormalSpellQueued) { + TriggerSpellQueuedEvent(NORMAL_QUEUE_POPPED, gLastNormalCastParams.spellId); + gCastData.normalSpellQueued = false; + gCastData.cooldownNormalSpellQueued = false; + } + + if (gCastData.nonGcdSpellQueued || gCastData.cooldownNonGcdSpellQueued) { + while (!gNonGcdCastQueue.isEmpty()) { + auto castParams = gNonGcdCastQueue.pop(); + TriggerSpellQueuedEvent(NON_GCD_QUEUE_POPPED, castParams.spellId); + } + gCastData.nonGcdSpellQueued = false; + gCastData.cooldownNonGcdSpellQueued = false; + } + } + + void checkForStopChanneling() { + if (gUserSettings.queueChannelingSpells && IsNonSwingSpellQueued()) { + // for channels just end channeling + auto const currentTime = GetTime(); + auto const elapsed = currentTime - gLastCastData.channelStartTimeMs; + + auto remainingChannelTime = (gCastData.channelEndMs > currentTime) ? gCastData.channelEndMs - currentTime + : 0; + + auto const currentLatency = GetLatencyMs(); + uint32_t latencyReduction = 0; + if (currentLatency > 0 && gUserSettings.channelLatencyReductionPercentage != 0) { + latencyReduction = ((uint32_t) currentLatency * gUserSettings.channelLatencyReductionPercentage) / 100; + + if (remainingChannelTime > latencyReduction) { + remainingChannelTime -= latencyReduction; + } else { + remainingChannelTime = 0; + } + } + + + if (remainingChannelTime <= 0) { + DEBUG_LOG("Ending channel [" << elapsed << " elapsed > " + << gCastData.channelDuration << " original duration " + << " latency reduction " << latencyReduction << "]" + << " triggering queued spells"); + + ResetChannelingFlags(); + } else if (gCastData.cancelChannelNextTick && + gCastData.channelStartMs > 0 && + currentTime - gCastData.channelStartMs < 60000) { + auto nextTickTimeMs = gCastData.channelStartMs; + + // find the next tick time but don't choose the next tick until gBufferTimeMs has passed after a tick + while (nextTickTimeMs < currentTime - gBufferTimeMs) { + nextTickTimeMs += gCastData.channelTickTimeMs; + } + + uint32_t remainingTickTime = 0; + if (nextTickTimeMs > currentTime) { + remainingTickTime = nextTickTimeMs - currentTime; + } + + if (remainingTickTime > 0) { + if (currentLatency > 0 && gUserSettings.channelLatencyReductionPercentage != 0) { + if (remainingTickTime > latencyReduction) { + remainingTickTime -= latencyReduction; + } else { + remainingTickTime = 0; + } + } else { + // default to reducing by gBufferTimeMs + if (remainingTickTime > gBufferTimeMs) { + remainingTickTime -= gBufferTimeMs; + } else { + remainingTickTime = 0; + } + } + } + + if (remainingTickTime <= 0) { + DEBUG_LOG("Ending channel due to cancelChannelNextTick. " + << "Remaining tick time: " << nextTickTimeMs - currentTime + << " latency reduction: " << latencyReduction); + ResetChannelingFlags(); + } + } + } + } + + bool processQueues() { + if (!gCastData.channeling) { + // check for high priority script + if (RunQueuedScript(1)) { + // script ran, stop processing + return true; + } + + // check for non gcd spell + if (gCastData.nonGcdSpellQueued) { + auto currentTime = GetTime(); + + if (EffectiveCastEndMs() <= currentTime) { + CastQueuedNonGcdSpell(); + return true; + } + } + + // check for cooldown non gcd spell + if (gCastData.cooldownNonGcdSpellQueued) { + auto currentTime = GetTime(); + + if (gCastData.cooldownNonGcdEndMs <= currentTime) { + DEBUG_LOG("Non gcd spell cooldown up, casting queued spell"); + // trigger the regular non gcd queuing and turn off cooldownNonGcdSpellQueued + gCastData.cooldownNonGcdSpellQueued = false; + gCastData.nonGcdSpellQueued = true; + CastQueuedNonGcdSpell(); + return true; + } + } + + // check for medium priority script + if (RunQueuedScript(2)) { + // script ran, stop processing + return true; + } + + // check for normal spell + if (gCastData.normalSpellQueued) { + auto currentTime = GetTime(); + + auto effectiveCastEndMs = EffectiveCastEndMs(); + // get max of cooldown and gcd + auto delay = effectiveCastEndMs > gCastData.gcdEndMs ? effectiveCastEndMs : gCastData.gcdEndMs; + + if (delay <= currentTime) { + // if more than MAX_TIME_SINCE_LAST_CAST_FOR_QUEUE seconds have passed since the last cast, ignore + if (currentTime - gLastCastData.startTimeMs < MAX_TIME_SINCE_LAST_CAST_FOR_QUEUE) { + CastQueuedNormalSpell(); + return true; + } else { + DEBUG_LOG("Ignoring queued cast of " << game::GetSpellName(gLastNormalCastParams.spellId) + << " due to max time since last cast"); + TriggerSpellQueuedEvent(NORMAL_QUEUE_POPPED, gLastNormalCastParams.spellId); + gCastData.normalSpellQueued = false; + } + } + } + + // check for cooldown normal spell + if (gCastData.cooldownNormalSpellQueued) { + auto currentTime = GetTime(); + + if (gCastData.cooldownNormalEndMs <= currentTime) { + DEBUG_LOG("Spell cooldown up, casting queued spell"); + // trigger the regular non gcd queuing and turn off cooldownNonGcdSpellQueued + gCastData.cooldownNormalSpellQueued = false; + gCastData.normalSpellQueued = true; + CastQueuedNormalSpell(); + return true; + } + } + + if (RunQueuedScript(3)) { + // script ran, stop processing + return true; + } + } + + return false; + } + + int OnSpriteRightClickHook(hadesmem::PatchDetourBase *detour, uint64_t objectGUID) { + auto const onSpriteRightClick = detour->GetTrampolineT(); + + auto const currentTargetGuid = game::GetCurrentTargetGuid(); + + // if we have a target that is not the right click target, ignore the right click + if (gUserSettings.preventRightClickTargetChange && currentTargetGuid && + currentTargetGuid != objectGUID) { + + auto unitOrPlayer = game::ClntObjMgrObjectPtr( + static_cast(game::TYPEMASK_PLAYER | game::TYPEMASK_UNIT), objectGUID); + + // only prevent right click if guid is a unit/player + if (unitOrPlayer) { + auto GetUnitFromName = reinterpret_cast(Offsets::GetUnitFromName); + auto const unit = GetUnitFromName("player"); + + // combat check from Script_UnitAffectingCombat + // only prevent right click if in combat + if (unit && ((*(uint32_t *) (*(int32_t *) (unit + 0x110) + 0xa0) >> 0x13 & 1) != 0)) { + return 1; + } + } + } + + return onSpriteRightClick(objectGUID); + } + + int *ISceneEndHook(hadesmem::PatchDetourBase *detour, uintptr_t *ptr) { + auto const iSceneEnd = detour->GetTrampolineT(); + + // check if it's time to end channeling + if (gCastData.channeling) { + checkForStopChanneling(); + } + + // process any queued spells/scripts + processQueues(); + + return iSceneEnd(ptr); + } + + void updateFromCvar(const char *cvar, const char *value) { + if (strcmp(cvar, "NP_QueueCastTimeSpells") == 0) { + gUserSettings.queueCastTimeSpells = atoi(value) != 0; + DEBUG_LOG("Set NP_QueueCastTimeSpells to " << gUserSettings.queueCastTimeSpells); + } else if (strcmp(cvar, "NP_QueueInstantSpells") == 0) { + gUserSettings.queueInstantSpells = atoi(value) != 0; + DEBUG_LOG("Set NP_QueueInstantSpells to " << gUserSettings.queueInstantSpells); + } else if (strcmp(cvar, "NP_QueueOnSwingSpells") == 0) { + gUserSettings.queueOnSwingSpells = atoi(value) != 0; + DEBUG_LOG("Set NP_QueueOnSwingSpells to " << gUserSettings.queueOnSwingSpells); + } else if (strcmp(cvar, "NP_QueueChannelingSpells") == 0) { + gUserSettings.queueChannelingSpells = atoi(value) != 0; + DEBUG_LOG("Set NP_QueueChannelingSpells to " << gUserSettings.queueChannelingSpells); + } else if (strcmp(cvar, "NP_QueueTargetingSpells") == 0) { + gUserSettings.queueTargetingSpells = atoi(value) != 0; + DEBUG_LOG("Set NP_QueueTargetingSpells to " << gUserSettings.queueTargetingSpells); + } else if (strcmp(cvar, "NP_QueueSpellsOnCooldown") == 0) { + gUserSettings.queueSpellsOnCooldown = atoi(value) != 0; + DEBUG_LOG("Set NP_QueueSpellsOnCooldown to " << gUserSettings.queueSpellsOnCooldown); + + } else if (strcmp(cvar, "NP_InterruptChannelsOutsideQueueWindow") == 0) { + gUserSettings.interruptChannelsOutsideQueueWindow = atoi(value) != 0; + DEBUG_LOG("Set NP_InterruptChannelsOutsideQueueWindow to " + << gUserSettings.interruptChannelsOutsideQueueWindow); + + } else if ((strcmp(cvar, "NP_RetryServerRejectedSpells") == 0)) { + gUserSettings.retryServerRejectedSpells = atoi(value) != 0; + DEBUG_LOG("Set NP_RetryServerRejectedSpells to " << gUserSettings.retryServerRejectedSpells); + } else if (strcmp(cvar, "NP_QuickcastTargetingSpells") == 0) { + gUserSettings.quickcastTargetingSpells = atoi(value) != 0; + DEBUG_LOG("Set NP_QuickcastTargetingSpells to " << gUserSettings.quickcastTargetingSpells); + } else if (strcmp(cvar, "NP_ReplaceMatchingNonGcdCategory") == 0) { + gUserSettings.replaceMatchingNonGcdCategory = atoi(value) != 0; + DEBUG_LOG("Set NP_ReplaceMatchingNonGcdCategory to " << gUserSettings.replaceMatchingNonGcdCategory); + } else if (strcmp(cvar, "NP_OptimizeBufferUsingPacketTimings") == 0) { + gUserSettings.optimizeBufferUsingPacketTimings = atoi(value) != 0; + DEBUG_LOG("Set NP_OptimizeBufferUsingPacketTimings to " << gUserSettings.optimizeBufferUsingPacketTimings); + + } else if (strcmp(cvar, "NP_PreventRightClickTargetChange") == 0) { + gUserSettings.preventRightClickTargetChange = atoi(value) != 0; + DEBUG_LOG("Set NP_PreventRightClickTargetChange to " << gUserSettings.preventRightClickTargetChange); + + } else if (strcmp(cvar, "NP_DoubleCastToEndChannelEarly") == 0) { + gUserSettings.doubleCastToEndChannelEarly = atoi(value) != 0; + DEBUG_LOG("Set NP_DoubleCastToEndChannelEarly to " << gUserSettings.doubleCastToEndChannelEarly); + + } else if (strcmp(cvar, "NP_MinBufferTimeMs") == 0) { + gUserSettings.minBufferTimeMs = atoi(value); + DEBUG_LOG("Set NP_MinBufferTimeMs and current buffer to " << gUserSettings.minBufferTimeMs); + gBufferTimeMs = gUserSettings.minBufferTimeMs; + } else if (strcmp(cvar, "NP_NonGcdBufferTimeMs") == 0) { + gUserSettings.nonGcdBufferTimeMs = atoi(value); + DEBUG_LOG("Set NP_NonGcdBufferTimeMs to " << gUserSettings.nonGcdBufferTimeMs); + } else if (strcmp(cvar, "NP_MaxBufferIncreaseMs") == 0) { + gUserSettings.maxBufferIncreaseMs = atoi(value); + DEBUG_LOG("Set NP_MaxBufferIncreaseMs to " << gUserSettings.maxBufferIncreaseMs); + + } else if (strcmp(cvar, "NP_SpellQueueWindowMs") == 0) { + gUserSettings.spellQueueWindowMs = atoi(value); + DEBUG_LOG("Set NP_SpellQueueWindowMs to " << gUserSettings.spellQueueWindowMs); + } else if (strcmp(cvar, "NP_OnSwingBufferCooldownMs") == 0) { + gUserSettings.onSwingBufferCooldownMs = atoi(value); + DEBUG_LOG("Set NP_OnSwingBufferCooldownMs to " << gUserSettings.onSwingBufferCooldownMs); + } else if (strcmp(cvar, "NP_ChannelQueueWindowMs") == 0) { + gUserSettings.channelQueueWindowMs = atoi(value); + DEBUG_LOG("Set NP_ChannelQueueWindowMs to " << gUserSettings.channelQueueWindowMs); + } else if (strcmp(cvar, "NP_TargetingQueueWindowMs") == 0) { + gUserSettings.targetingQueueWindowMs = atoi(value); + DEBUG_LOG("Set NP_TargetingQueueWindowMs to " << gUserSettings.targetingQueueWindowMs); + } else if (strcmp(cvar, "NP_CooldownQueueWindowMs") == 0) { + gUserSettings.cooldownQueueWindowMs = atoi(value); + DEBUG_LOG("Set NP_CooldownQueueWindowMs to " << gUserSettings.cooldownQueueWindowMs); + + } else if (strcmp(cvar, "NP_ChannelLatencyReductionPercentage") == 0) { + gUserSettings.channelLatencyReductionPercentage = atoi(value); + DEBUG_LOG( + "Set NP_ChannelLatencyReductionPercentage to " << gUserSettings.channelLatencyReductionPercentage); + + } else if (strcmp(cvar, "NP_NameplateDistance") == 0) { + auto distance = std::stof(value); + SetNameplateDistance(distance); + DEBUG_LOG( + "Set NP_NameplateDistance to " << distance); + } + } + + int Script_SetCVarHook(hadesmem::PatchDetourBase *detour, uintptr_t *luaPtr) { + auto const cvarSetOrig = gSetCVarDetour->GetTrampolineT(); + + auto const lua_isstring = reinterpret_cast(Offsets::lua_isstring); + if (lua_isstring(luaPtr, 1)) { + auto const lua_tostring = reinterpret_cast(Offsets::lua_tostring); + auto const cVarName = lua_tostring(luaPtr, 1); + auto const cVarValue = lua_tostring(luaPtr, 2); + // if cvar starts with "NP_", then we need to handle it + if (strncmp(cVarName, "NP_", 3) == 0) { + updateFromCvar(cVarName, cVarValue); + } + } // original function handles errors + + return cvarSetOrig(luaPtr); + } + + int *getCvar(const char *cvar) { + auto const cvarLookup = hadesmem::detail::AliasCast(Offsets::CVarLookup); + uintptr_t *cvarPtr = cvarLookup(cvar); + + if (cvarPtr) { + return reinterpret_cast(cvarPtr + + 10); // get intValue from CVar which is consistent, strValue more complicated + } + return nullptr; + } + + void loadUserVar(const char *cvar) { + int *value = getCvar(cvar); + if (value) { + updateFromCvar(cvar, std::to_string(*value).c_str()); + } else { + DEBUG_LOG("Using default value for " << cvar); + } + } + + void loadConfig() { + gStartTime = static_cast(std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch()).count()); + + try { + // remove/rename previous logs + remove("nampower_debug.log.3"); + rename("nampower_debug.log.2", "nampower_debug.log.3"); + rename("nampower_debug.log.1", "nampower_debug.log.2"); + rename("nampower_debug.log", "nampower_debug.log.1"); + } catch (...) { + // ignore any exceptions during log rotation + } + + + // open new log file + debugLogFile.open("nampower_debug.log"); + + DEBUG_LOG("Loading nampower v" << MAJOR_VERSION << "." << MINOR_VERSION << "." << PATCH_VERSION); + + // default values + gUserSettings.queueCastTimeSpells = true; + gUserSettings.queueInstantSpells = true; + gUserSettings.queueChannelingSpells = true; + gUserSettings.queueTargetingSpells = true; + gUserSettings.queueOnSwingSpells = false; + gUserSettings.queueSpellsOnCooldown = true; + + gUserSettings.interruptChannelsOutsideQueueWindow = false; + + gUserSettings.retryServerRejectedSpells = true; + gUserSettings.quickcastTargetingSpells = false; + gUserSettings.replaceMatchingNonGcdCategory = false; + gUserSettings.optimizeBufferUsingPacketTimings = false; + + gUserSettings.preventRightClickTargetChange = false; + + gUserSettings.doubleCastToEndChannelEarly = false; + + gUserSettings.minBufferTimeMs = 55; // time in ms to buffer cast to minimize server failure + gUserSettings.nonGcdBufferTimeMs = 100; // time in ms to buffer non-GCD spells to minimize server failure + gUserSettings.maxBufferIncreaseMs = 30; + + gUserSettings.spellQueueWindowMs = 500; // time in ms before cast to allow queuing spells + gUserSettings.onSwingBufferCooldownMs = 500; // time in ms to wait before queuing on swing spell after a swing + gUserSettings.channelQueueWindowMs = 1500; // time in ms before channel ends to allow queuing spells + gUserSettings.targetingQueueWindowMs = 500; // time in ms before cast to allow targeting + gUserSettings.cooldownQueueWindowMs = 250; // time in ms before cooldown is up to allow queuing spells + + gUserSettings.channelLatencyReductionPercentage = 75; // percent of latency to reduce channel time by + + char defaultTrue[] = "1"; + char defaultFalse[] = "0"; + + DEBUG_LOG("Registering/Loading CVars"); + + // register cvars + auto const CVarRegister = hadesmem::detail::AliasCast(Offsets::RegisterCVar); + + char NP_QueueCastTimeSpells[] = "NP_QueueCastTimeSpells"; + CVarRegister(NP_QueueCastTimeSpells, // name + nullptr, // help + 0, // unk1 + gUserSettings.queueCastTimeSpells ? defaultTrue : defaultFalse, // default value address + nullptr, // callback + 5, // category + 0, // unk2 + 0); // unk3 + + char NP_QueueInstantSpells[] = "NP_QueueInstantSpells"; + CVarRegister(NP_QueueInstantSpells, // name + nullptr, // help + 0, // unk1 + gUserSettings.queueInstantSpells ? defaultTrue : defaultFalse, // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_QueueChannelingSpells[] = "NP_QueueChannelingSpells"; + CVarRegister(NP_QueueChannelingSpells, // name + nullptr, // help + 0, // unk1 + gUserSettings.queueChannelingSpells ? defaultTrue : defaultFalse, // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_QueueTargetingSpells[] = "NP_QueueTargetingSpells"; + CVarRegister(NP_QueueTargetingSpells, // name + nullptr, // help + 0, // unk1 + gUserSettings.queueTargetingSpells ? defaultTrue : defaultFalse, // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_QueueOnSwingSpells[] = "NP_QueueOnSwingSpells"; + CVarRegister(NP_QueueOnSwingSpells, // name + nullptr, // help + 0, // unk1 + gUserSettings.queueOnSwingSpells ? defaultTrue : defaultFalse, // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_QueueSpellsOnCooldown[] = "NP_QueueSpellsOnCooldown"; + CVarRegister(NP_QueueSpellsOnCooldown, // name + nullptr, // help + 0, // unk1 + gUserSettings.queueSpellsOnCooldown ? defaultTrue : defaultFalse, // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_InterruptChannelsOutsideQueueWindow[] = "NP_InterruptChannelsOutsideQueueWindow"; + CVarRegister(NP_InterruptChannelsOutsideQueueWindow, // name + nullptr, // help + 0, // unk1 + gUserSettings.interruptChannelsOutsideQueueWindow ? defaultTrue + : defaultFalse, // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_RetryServerRejectedSpells[] = "NP_RetryServerRejectedSpells"; + CVarRegister(NP_RetryServerRejectedSpells, // name + nullptr, // help + 0, // unk1 + gUserSettings.retryServerRejectedSpells ? defaultTrue : defaultFalse, // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_QuickcastTargetingSpells[] = "NP_QuickcastTargetingSpells"; + CVarRegister(NP_QuickcastTargetingSpells, // name + nullptr, // help + 0, // unk1 + gUserSettings.quickcastTargetingSpells ? defaultTrue : defaultFalse, // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_MinBufferTimeMs[] = "NP_MinBufferTimeMs"; + CVarRegister(NP_MinBufferTimeMs, // name + nullptr, // help + 0, // unk1 + std::to_string(gUserSettings.minBufferTimeMs).c_str(), // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_NonGcdBufferTimeMs[] = "NP_NonGcdBufferTimeMs"; + CVarRegister(NP_NonGcdBufferTimeMs, // name + nullptr, // help + 0, // unk1 + std::to_string(gUserSettings.nonGcdBufferTimeMs).c_str(), // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_MaxBufferIncreaseMs[] = "NP_MaxBufferIncreaseMs"; + CVarRegister(NP_MaxBufferIncreaseMs, // name + nullptr, // help + 0, // unk1 + std::to_string(gUserSettings.maxBufferIncreaseMs).c_str(), // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_SpellQueueWindowMs[] = "NP_SpellQueueWindowMs"; + CVarRegister(NP_SpellQueueWindowMs, // name + nullptr, // help + 0, // unk1 + std::to_string(gUserSettings.spellQueueWindowMs).c_str(), // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_ChannelQueueWindowMs[] = "NP_ChannelQueueWindowMs"; + CVarRegister(NP_ChannelQueueWindowMs, // name + nullptr, // help + 0, // unk1 + std::to_string(gUserSettings.channelQueueWindowMs).c_str(), // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_TargetingQueueWindowMs[] = "NP_TargetingQueueWindowMs"; + CVarRegister(NP_TargetingQueueWindowMs, // name + nullptr, // help + 0, // unk1 + std::to_string(gUserSettings.targetingQueueWindowMs).c_str(), // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_CooldownQueueWindowMs[] = "NP_CooldownQueueWindowMs"; + CVarRegister(NP_CooldownQueueWindowMs, // name + nullptr, // help + 0, // unk1 + std::to_string(gUserSettings.cooldownQueueWindowMs).c_str(), // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_OnSwingBufferCooldownMs[] = "NP_OnSwingBufferCooldownMs"; + CVarRegister(NP_OnSwingBufferCooldownMs, // name + nullptr, // help + 0, // unk1 + std::to_string(gUserSettings.onSwingBufferCooldownMs).c_str(), // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_ReplaceMatchingNonGcdCategory[] = "NP_ReplaceMatchingNonGcdCategory"; + CVarRegister(NP_ReplaceMatchingNonGcdCategory, // name + nullptr, // help + 0, // unk1 + gUserSettings.replaceMatchingNonGcdCategory ? defaultTrue : defaultFalse, // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_OptimizeBufferUsingPacketTimings[] = "NP_OptimizeBufferUsingPacketTimings"; + CVarRegister(NP_OptimizeBufferUsingPacketTimings, // name + nullptr, // help + 0, // unk1 + gUserSettings.optimizeBufferUsingPacketTimings ? defaultTrue + : defaultFalse, // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_PreventRightClickTargetChange[] = "NP_PreventRightClickTargetChange"; + CVarRegister(NP_PreventRightClickTargetChange, // name + nullptr, // help + 0, // unk1 + gUserSettings.preventRightClickTargetChange ? defaultTrue : defaultFalse, // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_DoubleCastToEndChannelEarly[] = "NP_DoubleCastToEndChannelEarly"; + CVarRegister(NP_DoubleCastToEndChannelEarly, // name + nullptr, // help + 0, // unk1 + gUserSettings.doubleCastToEndChannelEarly ? defaultTrue : defaultFalse, // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_ChannelLatencyReductionPercentage[] = "NP_ChannelLatencyReductionPercentage"; + CVarRegister(NP_ChannelLatencyReductionPercentage, // name + nullptr, // help + 0, // unk1 + std::to_string(gUserSettings.channelLatencyReductionPercentage).c_str(), // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + char NP_NameplateDistance[] = "NP_NameplateDistance"; + CVarRegister(NP_NameplateDistance, // name + nullptr, // help + 0, // unk1 + std::to_string(GetNameplateDistance()).c_str(), // use the game's DAT value as the default + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + + // update from cvars + loadUserVar("NP_QueueCastTimeSpells"); + loadUserVar("NP_QueueInstantSpells"); + loadUserVar("NP_QueueOnSwingSpells"); + loadUserVar("NP_QueueChannelingSpells"); + loadUserVar("NP_QueueTargetingSpells"); + loadUserVar("NP_QueueSpellsOnCooldown"); + + loadUserVar("NP_InterruptChannelsOutsideQueueWindow"); + + loadUserVar("NP_RetryServerRejectedSpells"); + loadUserVar("NP_QuickcastTargetingSpells"); + loadUserVar("NP_ReplaceMatchingNonGcdCategory"); + loadUserVar("NP_OptimizeBufferUsingPacketTimings"); + + loadUserVar("NP_PreventRightClickTargetChange"); + + loadUserVar("NP_DoubleCastToEndChannelEarly"); + + loadUserVar("NP_MinBufferTimeMs"); + loadUserVar("NP_NonGcdBufferTimeMs"); + loadUserVar("NP_MaxBufferIncreaseMs"); + + loadUserVar("NP_SpellQueueWindowMs"); + loadUserVar("NP_ChannelQueueWindowMs"); + loadUserVar("NP_TargetingQueueWindowMs"); + loadUserVar("NP_OnSwingBufferCooldownMs"); + loadUserVar("NP_CooldownQueueWindowMs"); + + loadUserVar("NP_ChannelLatencyReductionPercentage"); + + loadUserVar("NP_NameplateDistance"); + + gBufferTimeMs = gUserSettings.minBufferTimeMs; + } + + void initCustomEvents() { + auto strPtr = reinterpret_cast(Offsets::QueueEventStringPtr); + const char *SPELL_QUEUE_EVENT = "SPELL_QUEUE_EVENT"; + // Make 0x00BE175C which is the unused event string ptr point to SPELL_QUEUE_EVENT (369) + *strPtr = reinterpret_cast(SPELL_QUEUE_EVENT); + + strPtr = reinterpret_cast(Offsets::CastEventStringPtr); + const char *SPELL_CAST_EVENT = "SPELL_CAST_EVENT"; + // Make 0X00BE1A08 which is the unused event string ptr point to SPELL_CAST_EVENT (540) + *strPtr = reinterpret_cast(SPELL_CAST_EVENT); + + strPtr = reinterpret_cast(Offsets::SpellDamageEventSelfStringPtr); + const char *SPELL_DAMAGE_EVENT_SELF = "SPELL_DAMAGE_EVENT_SELF"; + // Make 0X00BE1A2C which is the unused event string ptr point to SPELL_DAMAGE_EVENT_SELF (549) + *strPtr = reinterpret_cast(SPELL_DAMAGE_EVENT_SELF); + + strPtr = reinterpret_cast(Offsets::SpellDamageEventOtherStringPtr); + const char *SPELL_DAMAGE_EVENT_OTHER = "SPELL_DAMAGE_EVENT_OTHER"; + // Make 0X00BE1A30 which is the unused event string ptr point to SPELL_DAMAGE_EVENT_OTHER (550) + *strPtr = reinterpret_cast(SPELL_DAMAGE_EVENT_OTHER); + } + + // Template function to simplify hook initialization with specific storage + template + std::unique_ptr> createHook(const hadesmem::Process& process, Offsets offset, HookT hookFunc) { + auto const originalFunc = hadesmem::detail::AliasCast(offset); + auto detour = std::make_unique>(process, originalFunc, hookFunc); + detour->Apply(); + return detour; + } + + void initHooks() { + const hadesmem::Process process(::GetCurrentProcessId()); + + initCustomEvents(); + + gSetCVarDetour = createHook(process, Offsets::Script_SetCVar, &Script_SetCVarHook); + gCastDetour = createHook(process, Offsets::Spell_C_CastSpell, &Spell_C_CastSpellHook); + gSendCastDetour = createHook(process, Offsets::SendCast, &SendCastHook); + gCancelSpellDetour = createHook(process, Offsets::CancelSpell, &CancelSpellHook); + gCastResultHandlerDetour = createHook(process, Offsets::CastResultHandler, &CastResultHandlerHook); + gSpellStartHandlerDetour = createHook(process, Offsets::SpellStartHandler, &SpellStartHandlerHook); + gPeriodicAuraLogHandlerDetour = createHook(process, Offsets::PeriodicAuraLogHandler, &PeriodicAuraLogHandlerHook); + gSpellNonMeleeDmgLogHandlerDetour = createHook(process, Offsets::SpellNonMeleeDmgLogHandler, &SpellNonMeleeDmgLogHandlerHook); + gSpellChannelStartHandlerDetour = createHook(process, Offsets::SpellChannelStartHandler, &SpellChannelStartHandlerHook); + gSpellChannelUpdateHandlerDetour = createHook(process, Offsets::SpellChannelUpdateHandler, &SpellChannelUpdateHandlerHook); + gSpellFailedDetour = createHook(process, Offsets::Spell_C_SpellFailed, &Spell_C_SpellFailedHook); + gSpellGoDetour = createHook(process, Offsets::SpellGo, &SpellGoHook); + gSpellDelayedDetour = createHook(process, Offsets::SpellDelayed, &SpellDelayedHook); + gSpellTargetUnitDetour = createHook(process, Offsets::Script_SpellTargetUnit, &Script_SpellTargetUnitHook); + gSpellStopCastingDetour = createHook(process, Offsets::Script_SpellStopCasting, &Script_SpellStopCastingHook); + gSpell_C_TargetSpellDetour = createHook(process, Offsets::Spell_C_TargetSpell, &Spell_C_TargetSpellHook); + gCastSpellByNameNoQueueDetour = createHook(process, Offsets::Script_CastSpellByNameNoQueue, Script_CastSpellByNameNoQueue); + gQueueSpellByNameDetour = createHook(process, Offsets::Script_QueueSpellByName, Script_QueueSpellByName); + qQueueScriptDetour = createHook(process, Offsets::Script_QueueScript, Script_QueueScript); + gIsSpellInRangeDetour = createHook(process, Offsets::Script_IsSpellInRange, Script_IsSpellInRange); + gIsSpellUsableDetour = createHook(process, Offsets::Script_IsSpellUsable, Script_IsSpellUsable); + gGetCurrentCastingInfoDetour = createHook(process, Offsets::Script_GetCurrentCastingInfo, Script_GetCurrentCastingInfo); + gGetSpellIdForNameDetour = createHook(process, Offsets::Script_GetSpellIdForName, Script_GetSpellIdForName); + gGetSpellNameAndRankForIdDetour = createHook(process, Offsets::Script_GetSpellNameAndRankForId, Script_GetSpellNameAndRankForId); + gGetSpellSlotAndTypeForNameDetour = createHook(process, Offsets::Script_GetSpellSlotTypeIdForName, Script_GetSpellSlotTypeIdForName); + gOnSpriteRightClickDetour = createHook(process, Offsets::OnSpriteRightClick, OnSpriteRightClickHook); + gChannelStopCastingNextTickDetour = createHook(process, Offsets::Script_ChannelStopCastingNextTick, Script_ChannelStopCastingNextTick); + gGetNampowerVersionDetour = createHook(process, Offsets::Script_GetNampowerVersion, Script_GetNampowerVersion); + gGetItemLevelDetour = createHook(process, Offsets::Script_GetItemLevel, Script_GetItemLevel); + gIEndSceneDetour = createHook(process, Offsets::ISceneEndPtr, &ISceneEndHook); + } + + void SpellVisualsInitializeHook(hadesmem::PatchDetourBase *detour) { + auto const spellVisualsInitialize = detour->GetTrampolineT(); + spellVisualsInitialize(); + loadConfig(); + initHooks(); + } + + void FrameScript_CreateEventsHook(hadesmem::PatchDetourBase *detour, int param_1, uint32_t maxEventId) { + auto const createEvents = detour->GetTrampolineT(); + + if (maxEventId == 549) { + maxEventId = 551; // add two more events + } + + createEvents(param_1, maxEventId); + } + + void LoadScriptFunctionsHook(hadesmem::PatchDetourBase *detour) { + auto const loadScriptFunctions = detour->GetTrampolineT(); + loadScriptFunctions(); + + // register our own lua functions + DEBUG_LOG("Registering Custom Lua functions"); + char queueSpellByName[] = "QueueSpellByName"; + RegisterLuaFunction(queueSpellByName, reinterpret_cast(Offsets::Script_QueueSpellByName)); + + char castSpellByNameNoQueue[] = "CastSpellByNameNoQueue"; + RegisterLuaFunction(castSpellByNameNoQueue, + reinterpret_cast(Offsets::Script_CastSpellByNameNoQueue)); + + char queueScript[] = "QueueScript"; + RegisterLuaFunction(queueScript, reinterpret_cast(Offsets::Script_QueueScript)); + + char isSpellInRange[] = "IsSpellInRange"; + RegisterLuaFunction(isSpellInRange, reinterpret_cast(Offsets::Script_IsSpellInRange)); + + char isSpellUsable[] = "IsSpellUsable"; + RegisterLuaFunction(isSpellUsable, reinterpret_cast(Offsets::Script_IsSpellUsable)); + + char getCurrentCastingInfo[] = "GetCurrentCastingInfo"; + RegisterLuaFunction(getCurrentCastingInfo, + reinterpret_cast(Offsets::Script_GetCurrentCastingInfo)); + + char getSpellIdForName[] = "GetSpellIdForName"; + RegisterLuaFunction(getSpellIdForName, + reinterpret_cast(Offsets::Script_GetSpellIdForName)); + + char getSpellNameAndRankForId[] = "GetSpellNameAndRankForId"; + RegisterLuaFunction(getSpellNameAndRankForId, + reinterpret_cast(Offsets::Script_GetSpellNameAndRankForId)); + + char getSpellSlotTypeIdForName[] = "GetSpellSlotTypeIdForName"; + RegisterLuaFunction(getSpellSlotTypeIdForName, + reinterpret_cast(Offsets::Script_GetSpellSlotTypeIdForName)); + + char channelStopCastingNextTick[] = "ChannelStopCastingNextTick"; + RegisterLuaFunction(channelStopCastingNextTick, + reinterpret_cast(Offsets::Script_ChannelStopCastingNextTick)); + + char getNampowerVersion[] = "GetNampowerVersion"; + RegisterLuaFunction(getNampowerVersion, reinterpret_cast(Offsets::Script_GetNampowerVersion)); + + char getItemILevel[] = "GetItemLevel"; + RegisterLuaFunction(getItemILevel, reinterpret_cast(Offsets::Script_GetItemLevel)); + } + + std::once_flag loadFlag; + + void load() { + std::call_once(loadFlag, []() { + // hook spell visuals initialize + const hadesmem::Process process(::GetCurrentProcessId()); + + auto const spellVisualsInitOrig = hadesmem::detail::AliasCast( + Offsets::SpellVisualsInitialize); + gSpellVisualsInitDetour = std::make_unique>(process, + spellVisualsInitOrig, + &SpellVisualsInitializeHook); + gSpellVisualsInitDetour->Apply(); + + auto const loadScriptFunctionsOrig = hadesmem::detail::AliasCast( + Offsets::LoadScriptFunctions); + gLoadScriptFunctionsDetour = std::make_unique>(process, + loadScriptFunctionsOrig, + &LoadScriptFunctionsHook); + gLoadScriptFunctionsDetour->Apply(); + + auto const createEventsOrig = hadesmem::detail::AliasCast( + Offsets::FrameScript_CreateEvents); + gCreateEventsDetour = std::make_unique>(process, + createEventsOrig, + &FrameScript_CreateEventsHook); + gCreateEventsDetour->Apply(); + } + ); + } + +} + +extern "C" __declspec(dllexport) uint32_t Load() { + Nampower::load(); + return EXIT_SUCCESS; +} diff --git a/nampower/main.hpp b/nampower/main.hpp new file mode 100644 index 0000000..c9fb88c --- /dev/null +++ b/nampower/main.hpp @@ -0,0 +1,172 @@ +// +// Created by pmacc on 9/21/2024. +// + +#pragma once + +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include "game.hpp" +#include "types.h" +#include "castqueue.h" +#include "cdatastore.hpp" + +namespace Nampower { + constexpr uint32_t MAX_TIME_SINCE_LAST_CAST_FOR_QUEUE = 10000; // time limit in ms after which queued casts are ignored in errors + constexpr uint32_t DYNAMIC_BUFFER_INCREMENT = 5; // amount to adjust buffer in ms on errors/lack of errors + constexpr uint32_t BUFFER_INCREASE_FREQUENCY = 5000; // time in ms between changes to raise buffer + constexpr uint32_t BUFFER_DECREASE_FREQUENCY = 10000; // time in ms between changes to lower buffer + + constexpr uint32_t MAJOR_VERSION = 2; + constexpr uint32_t MINOR_VERSION = 10; + constexpr uint32_t PATCH_VERSION = 16; + + constexpr int32_t LUA_REGISTRYINDEX = -10000; + constexpr int32_t LUA_GLOBALSINDEX = -10001; + + extern uint32_t gLastErrorTimeMs; + extern uint32_t gLastBufferIncreaseTimeMs; + extern uint32_t gLastBufferDecreaseTimeMs; + + extern uint32_t gBufferTimeMs; // adjusts dynamically depending on errors + + extern bool gForceQueueCast; + extern bool gNoQueueCast; + + extern uint64_t gNextCastId; + + extern uint32_t gRunningAverageLatencyMs; + extern uint32_t gLastServerSpellDelayMs; + + extern hadesmem::PatchDetourBase *castSpellDetour; + + /* Configurable settings set by user */ + extern UserSettings gUserSettings; + + extern LastCastData gLastCastData; + extern CastData gCastData; + + extern CastSpellParams gLastNormalCastParams; + extern CastSpellParams gLastOnSwingCastParams; + + extern CastQueue gNonGcdCastQueue; + extern CastQueue gCastHistory; + + extern bool gScriptQueued; + + using RangeCheckSelectedT = bool (__fastcall *)(uintptr_t *playerUnit, const game::SpellRec *, + std::uint64_t targetGuid, char ignoreErrors); + using CastSpellT = bool (__fastcall *)(uintptr_t *playerUnit, uint32_t spellId, uintptr_t *item, + std::uint64_t targetGuid); + using SendCastT = void (__fastcall *)(game::SpellCast *, char unk); + using CancelSpellT = void (__fastcall *)(bool, bool, game::SpellCastResult); + using CancelAutoRepeatSpellT = void (__stdcall *)(); + using SignalEventT = void (__fastcall *)(game::Events); + using PacketHandlerT = int (__stdcall *)(uint32_t *opCode, CDataStore *packet); + using FastCallPacketHandlerT = int (__fastcall *)(uint32_t unk, uint32_t opCode, uint32_t unk2, CDataStore *packet); + using ISceneEndT = int *(__fastcall *)(uintptr_t *unk); + using EndSceneT = int (__fastcall *)(uintptr_t *unk); + using OnSpriteRightClickT = int (__fastcall *)(uint64_t objectGUID); + using CGGameUI_TargetT = void (__stdcall *)(uint64_t objectGUID); + using Spell_C_SpellFailedT = void (__fastcall *)(uint32_t, game::SpellCastResult, int, int, char unk3); + using Spell_C_GetAutoRepeatingSpellT = int (__cdecl *)(); + using SpellGoT = void (__fastcall *)(uint64_t *, uint64_t *, uint32_t, CDataStore *); + using Spell_C_HandleSpriteClickT = bool (__fastcall *)(game::CSpriteClickEvent *event); + using Spell_C_TargetSpellT = bool (__fastcall *)( + uint32_t *player, + uint32_t *spellId, + uint32_t unk3, + float unk4); + using Spell_C_GetCastTimeT = uint32_t (__fastcall *)(uint32_t spellId, uint64_t *casterGuid, int avoidRounding); + using Spell_C_GetSpellCooldownT = int (__fastcall *)(uint32_t spellId, uint32_t isPetSpell, + uint32_t *duration, uint64_t *startTime, uint32_t *enable); + using Spell_C_IsSpellUsableT = int (__fastcall *)(const game::SpellRec *spellRec, uint32_t *usesManaReturn); + + using GetSpellSlotAndTypeT = int (__fastcall *)(const char *, uint32_t *); + using GetTimeMsT = uint64_t (__stdcall *)(); + + using GetClientConnectionT = uintptr_t *(__stdcall *)(); + using GetNetStatsT = void (__thiscall *)(uintptr_t *connection, float *param_1, float *param_2, uint32_t *param_3); + using ClientServices_SendT = void (__fastcall *)(CDataStore *param_1); + + using LoadScriptFunctionsT = void (__stdcall *)(); + using FrameScript_RegisterFunctionT = void (__fastcall *)(char *name, uintptr_t *func); + using FrameScript_CreateEventsT = void (__fastcall *)(int param_1, uint32_t maxEventId); + + using LuaGetContextT = uintptr_t *(__fastcall *)(void); + using LuaGetTableT = void (__fastcall *)(uintptr_t *luaState, int globalsIndex); + using LuaCallT = void (__fastcall *)(const char *code, const char *unused); + using LuaScriptT = uint32_t (__fastcall *)(uintptr_t *luaState); + using GetGUIDFromNameT = std::uint64_t (__fastcall *)(const char *); + using GetUnitFromNameT = uintptr_t (__fastcall *)(const char *); + using lua_gettableT = void (__fastcall *)(uintptr_t *luaState, int globalsIndex); + using lua_isstringT = bool (__fastcall *)(uintptr_t *, int); + using lua_isnumberT = bool (__fastcall *)(uintptr_t *, int); + using lua_tostringT = char *(__fastcall *)(uintptr_t *, int); + using lua_tonumberT = double (__fastcall *)(uintptr_t *, int); + using lua_pushnumberT = void (__fastcall *)(uintptr_t *, double); + using lua_pushstringT = void (__fastcall *)(uintptr_t *, char *); + using lua_pcallT = int (__fastcall *)(uintptr_t *, int nArgs, int nResults, int errFunction); + using lua_pushnilT = void (__fastcall *)(uintptr_t *); + using lua_errorT = void (__cdecl *)(uintptr_t *, const char *); + using lua_settopT = void (__fastcall *)(uintptr_t *, int); + + using Spell_C_CooldownEventTriggeredT = void (__fastcall *)(uint32_t spellId, + uint64_t *targetGUID, + int param_3, + int clearCooldowns); + + using SpellVisualsInitializeT = void (__stdcall *)(void); + + using PlaySpellVisual = void (__stdcall *)(int **param_1, void *param_2, int param_3, void **param_4); + using CGUnit_C_ClearCastingSpellT = void (__thiscall *)(uintptr_t *unit, uint32_t param_1, int param_2, int param_3); + using CGUnit_C_ClearSpellEffectT = void (__thiscall *)(uintptr_t *unit, uint32_t param_1, int param_2); + + using GetBuffByIndexT = uintptr_t *(__fastcall *)(int index); + + using CVarLookupT = uintptr_t *(__fastcall *)(const char *); + using SetCVarT = int (__fastcall *)(uintptr_t *luaPtr); + using CVarRegisterT = int *(__fastcall *)(char *name, char *help, int unk1, const char *defaultValuePtr, + void *callbackPtr, + int category, char unk2, int unk3); + + void RegisterLuaFunction(char *, uintptr_t *func); + + void LuaCall(const char *code); + + uintptr_t *GetLuaStatePtr(); + + uint64_t GetWowTimeMs(); + + uint32_t GetLatencyMs(); + + uint32_t GetServerDelayMs(); + + bool InSpellQueueWindow(uint32_t remainingCastTime, uint32_t remainingGcd, bool spellIsTargeting); + + bool IsNonSwingSpellQueued(); + + void ResetChannelingFlags(); + + void ResetCastFlags(); + + void ResetOnSwingFlags(); + + void ClearQueuedSpells(); + + bool processQueues(); + + uint32_t EffectiveCastEndMs(); +} \ No newline at end of file diff --git a/nampower/offsets.hpp b/nampower/offsets.hpp new file mode 100644 index 0000000..11e330a --- /dev/null +++ b/nampower/offsets.hpp @@ -0,0 +1,177 @@ +/* + Copyright (c) 2017-2023, namreeb (legal@namreeb.org) + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The views and conclusions contained in the software and documentation are those + of the authors and should not be interpreted as representing official policies, + either expressed or implied, of the FreeBSD Project. +*/ + +#pragma once + +#include + +enum class Offsets : std::uint32_t { + StartSpellVisualCheck = 0X006E79B5, + + ClntObjMgrObjectPtr = 0x00468460, + GetObjectPtr = 0x464870, + GetActivePlayer = 0x468550, + GetUnitFromName = 0x00515940, + GetDurationObject = 0x00C0D828, + GetCastingTimeIndex = 0x2D, + Language = 0xC0E080, + SpellDb = 0xC0D780, + CursorMode = 0xBE2C4C, + SpellIsTargeting = 0xCECAC0, + CastingItemIdPtr = 0X00CECAB0, + CastingSpellId = 0xCECA88, + AutoRepeatingSpellId = 0xCEAC30, + VisualSpellId = 0X00CEAC58, + GetSpellSlotAndType = 0X004B3950, + OsGetAsyncTimeMs = 0X0042B790, + ChannelTargetGuid = 0xC4D980, + NameplateDistance = 0xC4D988, // float containing the distance squared ready to be pythagorean'd + DBCacheGetRecord = 0X0055BA30, + + ItemDBCache = 0xC0E2A0, + + CGSpellBook_mKnownSpells = 0xB700F0, + CGSpellBook_mKnownPetSpells = 0XB6F098, + + IsSpellInRangeOfUnit = 0X004E56F0, + CancelSpell = 0x6E4940, + CancelAutoRepeatSpell = 0X006EA080, + SpellDelayed = 0x6E74F0, + SignalEvent = 0x703E50, + SignalEventParam = 0x703F50, + ISceneEndPtr = 0x5A17A0, + SpellStart = 0X006E7700, + SpellGo = 0x006E7A70, + CooldownEvent = 0x006E9670, + SendCast = 0x6E54F0, + CreateCastbar = 0x6E7A53, + CheckAndReportSpellInhibitFlags = 0x006094f0, + + LockedTargetGuid = 0x00B4E2D8, + OnSpriteRightClick = 0x00492820, + CGGameUI_Target = 0X00493540, + + SpellVisualsHandleCastStart = 0X006EC220, + PlaySpellVisualHandler = 0X006E98D0, + PlaySpellVisual = 0X0060EDF0, + + SpellChannelStartHandler = 0x006E7550, + SpellChannelUpdateHandler = 0x006E75F0, + SpellFailedHandler = 0x006E8D80, + SpellFailedOtherHandler = 0X006E8E40, + CastResultHandler = 0x006E7330, + SpellStartHandler = 0x006E7640, + SpellCooldownHandler = 0X006E9460, + PeriodicAuraLogHandler = 0X00626DD0, + SpellNonMeleeDmgLogHandler = 0X005E85E0, + + Spell_C_GetCastTime = 0X006E3340, + Spell_C_CastSpell = 0x6E4B60, + Spell_C_CooldownEventTriggered = 0X006E3050, + Spell_C_SpellFailed = 0x006E1A00, + Spell_C_CastSpellByID = 0x6E5A90, + Spell_C_GetAutoRepeatingSpell = 0x006E9FD0, + Spell_C_HandleSpriteClick = 0x006E5B10, + Spell_C_TargetSpell = 0x006E5250, + Spell_C_GetSpellCooldown = 0X006E2EA0, + Spell_C_IsSpellUsable = 0X006E3D60, + Spell_C_GetDuration = 0X006EA000, + Spell_C_GetSpellModifiers = 0x006e6af0, + + CVarLookup = 0x0063DEC0, + RegisterCVar = 0X0063DB90, + + GetClientConnection = 0X005AB490, + GetNetStats = 0X00537F20, + ClientServices_Send = 0X005AB630, + + LoadScriptFunctions = 0x00490250, + FrameScript_RegisterFunction = 0x00704120, + FrameScript_CreateEvents = 0X00703D90, + + // Existing script functions + GetGUIDFromName = 0X00515970, + Script_CastSpellByName = 0x004B4AB0, + Script_SpellTargetUnit = 0x006E6D90, + Script_SetCVar = 0x00488C10, + Script_RunScript = 0x0048B980, + Script_SpellStopCasting = 0x006E6E80, + + // Added script functions + Script_QueueSpellByName = 0x004B4B38, // unused address at the end of Script_CastSpellByName. Need to be < 0x7FEDAC to avoid Invalid Function Pointer + Script_CastSpellByNameNoQueue = 0X004B4B64, + Script_QueueScript = 0X0048B968, // unused address at the end of Script_StopCinematic + Script_IsSpellInRange = 0x004E76D8, + Script_IsSpellUsable = 0X004E77A4, + Script_GetCurrentCastingInfo = 0X004E77F8, + Script_GetSpellIdForName = 0X004E7828, + Script_GetSpellNameAndRankForId = 0X004E7844, + Script_GetSpellSlotTypeIdForName = 0X004E784A, + Script_ChannelStopCastingNextTick = 0X004E7858, + Script_GetNampowerVersion = 0X004E7874, + Script_GetItemLevel = 0X004E787A, + + lua_state_ptr = 0x7040D0, + + lua_isstring = 0x006F3510, + lua_isnumber = 0X006F34D0, + lua_tostring = 0x006F3690, + lua_tonumber = 0X006F3620, + lua_pushnumber = 0X006F3810, + lua_gettable = 0X6F3A40, + lua_pushstring = 0X006F3890, + lua_pushnil = 0X006F37F0, + lua_call = 0x00704CD0, + lua_pcall = 0x006F41A0, + lua_error = 0X006F4940, + lua_settop = 0X006F3080, + + + CGInputControlGetActive = 0XBE1148, + LastHardwareAction = 0X00CF0BC8, + CGInputControlSetReleaseAction = 0x514810, + CGInputControlSetControlBit = 0x515090, + + RangeCheckSelected = 0x6E4440, + + SpellVisualsInitialize = 0x006ec0e0, + + IntIntParamFormat = 0X00843342, + StringIntParamFormat = 0X00847FBC, + + QueueEventStringPtr = 0X00BE175C, // unused event 369 0x171 + CastEventStringPtr = 0X00BE1A08, // unused event 540 0x21C + SpellDamageEventSelfStringPtr = 0X00BE1A2C, // unused event 549 0x225 + SpellDamageEventOtherStringPtr = 0X00BE1A30, // unused event 550 0x226 + + GetBuffByIndex = 0X004E4430, + + CGUnit_C_ClearCastingSpell = 0x0060d040, + CGUnit_C_ClearSpellEffect = 0x00614150, +}; diff --git a/nampower/scripts.cpp b/nampower/scripts.cpp new file mode 100644 index 0000000..d1989ab --- /dev/null +++ b/nampower/scripts.cpp @@ -0,0 +1,443 @@ +// +// Created by pmacc on 1/8/2025. +// + +#include "scripts.hpp" +#include "offsets.hpp" + +namespace Nampower { + auto const lua_error = reinterpret_cast(Offsets::lua_error); + + auto const lua_isstring = reinterpret_cast(Offsets::lua_isstring); + auto const lua_isnumber = reinterpret_cast(Offsets::lua_isnumber); + + auto const lua_tostring = reinterpret_cast(Offsets::lua_tostring); + auto const lua_tonumber = reinterpret_cast(Offsets::lua_tonumber); + + auto const lua_pushnumber = reinterpret_cast(Offsets::lua_pushnumber); + auto const lua_pushstring = reinterpret_cast(Offsets::lua_pushstring); + + bool gScriptQueued; + int gScriptPriority = 1; + char *queuedScript; + + uint32_t GetSpellSlotAndTypeForName(const char *spellName, uint32_t *spellType) { + auto const getSpellSlotAndType = reinterpret_cast(Offsets::GetSpellSlotAndType); + return getSpellSlotAndType(spellName, spellType); + } + + + uint32_t GetSpellIdFromSpellName(const char *spellName) { + uint32_t bookType; + uint32_t spellSlot = GetSpellSlotAndTypeForName(spellName, &bookType); + + uint32_t spellId = 0; + if (spellSlot < 1024) { + if (bookType == 0) { + spellId = *reinterpret_cast(uint32_t(Offsets::CGSpellBook_mKnownSpells) + + spellSlot * 4); + } else { + spellId = *reinterpret_cast(uint32_t(Offsets::CGSpellBook_mKnownPetSpells) + + spellSlot * 4); + } + } + + return spellId; + } + + uint32_t Script_CastSpellByNameNoQueue(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + DEBUG_LOG("Casting next spell without queuing"); + // turn on forceQueue and then call regular CastSpellByName + gNoQueueCast = true; + auto const Script_CastSpellByName = reinterpret_cast(Offsets::Script_CastSpellByName); + auto result = Script_CastSpellByName(luaState); + gNoQueueCast = false; + + return result; + } + + uint32_t Script_QueueSpellByName(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + DEBUG_LOG("Force queuing next cast spell"); + // turn on forceQueue and then call regular CastSpellByName + gForceQueueCast = true; + auto const Script_CastSpellByName = reinterpret_cast(Offsets::Script_CastSpellByName); + auto result = Script_CastSpellByName(luaState); + gForceQueueCast = false; + + return result; + } + + uint32_t Script_SpellStopCastingHook(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + DEBUG_LOG("SpellStopCasting called"); + + ClearQueuedSpells(); + ResetCastFlags(); + ResetChannelingFlags(); + + auto const spellStopCasting = detour->GetTrampolineT(); + return spellStopCasting(luaState); + } + + uint32_t Script_IsSpellInRange(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + auto param1IsString = lua_isstring(luaState, 1); + auto param1IsNumber = lua_isnumber(luaState, 1); + if (param1IsString || param1IsNumber) { + uint32_t spellId = 0; + + if (param1IsNumber) { + spellId = uint32_t(lua_tonumber(luaState, 1)); + + if (spellId == 0) { + lua_error(luaState, "Unable to parse spell id"); + return 0; + } + } else { + auto const spellName = lua_tostring(luaState, 1); + + spellId = GetSpellIdFromSpellName(spellName); + if (spellId == 0) { + lua_error(luaState, + "Unable to determine spell id from spell name, possibly because it isn't in your spell book. Try IsSpellInRange(SPELL_ID) instead"); + return 0; + } + } + + auto spell = game::GetSpellInfo(spellId); + if (spell) { + std::set validTargetTypes = {5, 6, 21, 25}; + if (sizeof spell->EffectImplicitTargetA == 0 || + validTargetTypes.count(spell->EffectImplicitTargetA[0]) == 0) { + lua_pushnumber(luaState, -1.0); + return 1; + } + + char *target; + + if (lua_isstring(luaState, 2)) { + target = lua_tostring(luaState, 2); + } else { + char defaultTarget[] = "target"; + target = defaultTarget; + } + + uint64_t targetGUID; + if (strncmp(target, "0x", 2) == 0 || strncmp(target, "0X", 2) == 0) { + // already a guid + targetGUID = std::stoull(target, nullptr, 16); + } else { + auto const getGUIDFromName = reinterpret_cast(Offsets::GetGUIDFromName); + targetGUID = getGUIDFromName(target); + } + + auto playerUnit = game::GetObjectPtr(game::ClntObjMgrGetActivePlayerGuid()); + + auto const RangeCheckSelected = reinterpret_cast(Offsets::RangeCheckSelected); + auto const result = RangeCheckSelected(playerUnit, spell, targetGUID, '\0'); + + if (result != 0) { + lua_pushnumber(luaState, 1.0); + } else { + lua_pushnumber(luaState, 0); + } + return 1; + } else { + lua_error(luaState, "Spell not found"); + } + } else { + lua_error(luaState, "Usage: IsSpellInRange(spellName)"); + } + + return 0; + } + + uint32_t Script_IsSpellUsable(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + auto param1IsString = lua_isstring(luaState, 1); + auto param1IsNumber = lua_isnumber(luaState, 1); + if (param1IsString || param1IsNumber) { + uint32_t spellId = 0; + + if (param1IsNumber) { + spellId = uint32_t(lua_tonumber(luaState, 1)); + + if (spellId == 0) { + lua_error(luaState, "Unable to parse spell id"); + return 0; + } + } else { + auto const spellName = lua_tostring(luaState, 1); + + spellId = GetSpellIdFromSpellName(spellName); + if (spellId == 0) { + lua_error(luaState, + "Unable to determine spell id from spell name, possibly because it isn't in your spell book. Try IsSpellUsable(SPELL_ID) instead"); + return 0; + } + } + + auto spell = game::GetSpellInfo(spellId); + if (spell) { + auto const IsSpellUsable = reinterpret_cast(Offsets::Spell_C_IsSpellUsable); + + uint32_t outOfMana = 0; + auto const result = IsSpellUsable(spell, &outOfMana) & 0xFF; + + if (result != 0) { + lua_pushnumber(luaState, 1.0); + } else { + lua_pushnumber(luaState, 0); + } + + if (outOfMana) { + lua_pushnumber(luaState, 1.0); + } else { + lua_pushnumber(luaState, 0); + } + + return 2; + } else { + lua_error(luaState, "Spell not found"); + } + } else { + lua_error(luaState, "Usage: IsSpellUsable(spellName)"); + } + + return 0; + } + + uint32_t Script_GetCurrentCastingInfo(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + auto const castingSpellId = reinterpret_cast(Offsets::CastingSpellId); + lua_pushnumber(luaState, *castingSpellId); + + auto const isCasting = gCastData.castEndMs > GetTime(); + auto const isChanneling = gCastData.channeling; + + auto const visualSpellId = reinterpret_cast(Offsets::VisualSpellId); + lua_pushnumber(luaState, *visualSpellId); + + auto const autoRepeatingSpellId = reinterpret_cast(Offsets::AutoRepeatingSpellId); + lua_pushnumber(luaState, *autoRepeatingSpellId); + + auto playerUnit = game::GetObjectPtr(game::ClntObjMgrGetActivePlayerGuid()); + if (isCasting) { + lua_pushnumber(luaState, 1); + } else { + lua_pushnumber(luaState, 0); + } + + if (isChanneling) { + lua_pushnumber(luaState, 1); + } else { + lua_pushnumber(luaState, 0); + } + + if (gCastData.pendingOnSwingCast) { + lua_pushnumber(luaState, 1); + } else { + lua_pushnumber(luaState, 0); + } + + auto const attackPtr = playerUnit + 0x312; // auto attacking + if (attackPtr && *reinterpret_cast(attackPtr) > 0) { + lua_pushnumber(luaState, 1); + } else { + lua_pushnumber(luaState, 0); + } + + return 7; + } + + uint32_t Script_GetSpellIdForName(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + if (lua_isstring(luaState, 1)) { + auto const spellName = lua_tostring(luaState, 1); + auto const spellId = GetSpellIdFromSpellName(spellName); + lua_pushnumber(luaState, spellId); + return 1; + } else { + lua_error(luaState, "Usage: GetSpellIdForName(spellName)"); + } + + return 0; + } + + uint32_t Script_GetSpellNameAndRankForId(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + if (lua_isnumber(luaState, 1)) { + auto const spellId = uint32_t(lua_tonumber(luaState, 1)); + auto const spell = game::GetSpellInfo(spellId); + + if (spell) { + auto const language = *reinterpret_cast(Offsets::Language); + lua_pushstring(luaState, (char *) spell->SpellName[language]); + lua_pushstring(luaState, (char *) spell->Rank[language]); + return 2; + } else { + DEBUG_LOG("Spell not found for id: " << spellId); + lua_error(luaState, "Spell not found"); + } + } else { + lua_error(luaState, "Usage: GetSpellNameAndRankForId(spellId)"); + } + + return 0; + } + + uint32_t Script_GetSpellSlotTypeIdForName(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + if (lua_isstring(luaState, 1)) { + auto const spellName = lua_tostring(luaState, 1); + uint32_t bookType; + auto spellSlot = GetSpellSlotAndTypeForName(spellName, &bookType); + + // returns large number if spell not found + if (spellSlot > 100000) { + lua_pushnumber(luaState, 0); + spellSlot = 0; + bookType = 999; + + lua_pushnumber(luaState, spellSlot); + } else { + lua_pushnumber(luaState, spellSlot + 1); // lua is 1 indexed + } + + if (bookType == 0) { + char spell[] = "spell"; + lua_pushstring(luaState, spell); + } else if (bookType == 1) { + char pet[] = "pet"; + lua_pushstring(luaState, pet); + } else { + char unknown[] = "unknown"; + lua_pushstring(luaState, unknown); + } + + if (spellSlot > 0 && spellSlot < 1024) { + uint32_t spellId = 0; + if (bookType == 0) { + spellId = *reinterpret_cast(uint32_t(Offsets::CGSpellBook_mKnownSpells) + + spellSlot * 4); + } else { + spellId = *reinterpret_cast(uint32_t(Offsets::CGSpellBook_mKnownPetSpells) + + spellSlot * 4); + } + lua_pushnumber(luaState, spellId); + } else { + lua_pushnumber(luaState, 0); + } + + return 3; + } else { + lua_error(luaState, "Usage: GetSpellSlotTypeIdForName(spellName)"); + } + + return 0; + } + + uint32_t Script_ChannelStopCastingNextTick(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + if (gCastData.channeling) { + DEBUG_LOG("ChannelStopCastingNextTick activated, canceling next tick"); + gCastData.cancelChannelNextTick = true; + } + + return 0; + } + + uint32_t Script_GetNampowerVersion(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + lua_pushnumber(luaState, MAJOR_VERSION); + lua_pushnumber(luaState, MINOR_VERSION); + lua_pushnumber(luaState, PATCH_VERSION); + + return 3; + } + + uint32_t Script_GetItemLevel(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + if (lua_isnumber(luaState, 1)) { + auto const itemId = static_cast(lua_tonumber(luaState, 1)); + + // Pointer to ItemDBCache + void *itemDbCache = reinterpret_cast(Offsets::ItemDBCache); + + // Parameters for the DBCache<>::GetRecord function + int **param2 = nullptr; + int *param3 = nullptr; + int *param4 = nullptr; + char param5 = 0; + + // Call the DBCache<>::GetRecord function + auto getRecord = reinterpret_cast( + Offsets::DBCacheGetRecord + ); + uintptr_t *itemObject = getRecord(itemDbCache, itemId, param2, param3, param4, param5); + if (itemObject == nullptr) { + lua_error(luaState, "Item not found in DBCache"); + return 0; + } + + uint32_t itemLevel = *reinterpret_cast(itemObject + 14); + lua_pushnumber(luaState, itemLevel); + return 1; + } else { + lua_error(luaState, "Usage: GetItemLevel(itemId)"); + } + + return 0; + } + + bool Script_QueueScript(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + DEBUG_LOG("Trying to queue script"); + + auto const currentTime = GetTime(); + auto effectiveCastEndMs = EffectiveCastEndMs(); + auto remainingEffectiveCastTime = (effectiveCastEndMs > currentTime) ? effectiveCastEndMs - currentTime : 0; + auto remainingGcd = (gCastData.gcdEndMs > currentTime) ? gCastData.gcdEndMs - currentTime : 0; + auto inSpellQueueWindow = InSpellQueueWindow(remainingEffectiveCastTime, remainingGcd, false); + + if (inSpellQueueWindow) { + // check if valid string + if (lua_isstring(luaState, 1)) { + auto script = lua_tostring(luaState, 1); + + if (script != nullptr && strlen(script) > 0) { + // save the script to be run later + queuedScript = script; + gScriptQueued = true; + + // check if priority is set + if (lua_isnumber(luaState, 2)) { + gScriptPriority = (int) lua_tonumber(luaState, 2); + + DEBUG_LOG("Queuing script priority " << gScriptPriority << ": " << script); + } else { + DEBUG_LOG("Queuing script: " << script); + } + } + } else { + DEBUG_LOG("Invalid script"); + lua_error(luaState, "Usage: QueueScript(\"script\", (optional)priority)"); + } + } else { + // just call regular runscript + auto const runScript = reinterpret_cast(Offsets::Script_RunScript); + return runScript(luaState); + } + + return false; + } + + bool RunQueuedScript(int priority) { + if (gScriptQueued && gScriptPriority == priority) { + auto currentTime = GetTime(); + + auto effectiveCastEndMs = EffectiveCastEndMs(); + // get max of cooldown and gcd + auto delay = effectiveCastEndMs > gCastData.gcdEndMs ? effectiveCastEndMs : gCastData.gcdEndMs; + + if (delay <= currentTime) { + DEBUG_LOG("Running queued script priority " << gScriptPriority << ": " << queuedScript); + LuaCall(queuedScript); + gScriptQueued = false; + gScriptPriority = 1; + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/nampower/scripts.hpp b/nampower/scripts.hpp new file mode 100644 index 0000000..ab18b8e --- /dev/null +++ b/nampower/scripts.hpp @@ -0,0 +1,38 @@ +// +// Created by pmacc on 1/8/2025. +// + +#pragma once + +#include +#include "main.hpp" + +namespace Nampower { + uint32_t Script_CastSpellByNameNoQueue(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + uint32_t Script_QueueSpellByName(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + uint32_t Script_IsSpellInRange(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + uint32_t Script_IsSpellUsable(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + uint32_t Script_SpellStopCastingHook(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + uint32_t Script_GetCurrentCastingInfo(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + uint32_t Script_GetSpellIdForName(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + uint32_t Script_GetSpellNameAndRankForId(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + uint32_t Script_GetSpellSlotTypeIdForName(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + uint32_t Script_ChannelStopCastingNextTick(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + uint32_t Script_GetNampowerVersion(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + uint32_t Script_GetItemLevel(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + bool Script_QueueScript(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + bool RunQueuedScript(int priority); +} \ No newline at end of file diff --git a/nampower/spellcast.cpp b/nampower/spellcast.cpp new file mode 100644 index 0000000..7d19aca --- /dev/null +++ b/nampower/spellcast.cpp @@ -0,0 +1,744 @@ +// +// Created by pmacc on 9/21/2024. +// + +#include "spellcast.hpp" +#include "helper.hpp" +#include "offsets.hpp" +#include "logging.hpp" + +namespace Nampower { + auto const APPLY_BUFFER_TO_GCD = true; // only necessary until the gcd issue is fixed again + + uint32_t GetChannelBaseDuration(const game::SpellRec *spell) { + auto const duration = game::GetDurationObject(spell->DurationIndex); + if (duration == nullptr) { + DEBUG_LOG("GetChannelBaseDuration: Duration object is null for spell " << game::GetSpellName(spell->Id)); + return 0; + } + return duration->m_Duration; + } + + void BeginCast(uint32_t castTime, const game::SpellRec *spell, const game::SpellCast *cast) { + if (cast != nullptr && cast->itemTarget == 0 && cast->caster != game::ClntObjMgrGetActivePlayerGuid()) { + DEBUG_LOG("Ignoring non active player begin cast of spell " << game::GetSpellName(cast->spellId) << " " + << cast->spellId); + return; + } + + auto currentTime = GetTime(); + + gLastCastData.castTimeMs = castTime; + + gCastData.channeling = SpellIsChanneling(spell); + gLastCastData.wasQueued = false; // reset the last spell queued flag + + if (gCastData.channeling) { + gCastData.channelDuration = GetChannelBaseDuration(spell); + gCastData.channelEndMs = currentTime + gCastData.channelDuration; + } + + auto const spellOnGcd = SpellIsOnGcd(spell); + gLastCastData.wasOnGcd = spellOnGcd; + + auto lastCastParams = gCastHistory.peek(); + uint64_t lastCastId = 0; + if (lastCastParams != nullptr) { + gLastCastData.wasItem = lastCastParams->item != nullptr; + lastCastParams->castResult = CastResult::WAITING_FOR_SERVER; + lastCastId = lastCastParams->castId; + } + + auto bufferMs = GetServerDelayMs(); + // Reset the server delay + gLastServerSpellDelayMs = 0; + + if (spellOnGcd) { + auto gcdTime = GetGcdOrCooldownForSpell(spell->Id); + if (gcdTime > 1500) { + gcdTime = 1500; // items with spells on gcd will return their item gcd, make sure not to use that + } + + if (!APPLY_BUFFER_TO_GCD) { + if (castTime < gcdTime - 50) { + bufferMs = 0; // no longer need to buffer spells with cast time 50ms < gcd + } else if (castTime < gcdTime) { + auto const diff = gcdTime - castTime; + if (bufferMs > diff) { + bufferMs -= diff; // subtract the difference from the buffer + } else { + bufferMs = 0; // if the buffer is less than the difference, set it to 0 + } + } + } + + gCastData.gcdEndMs = currentTime + gcdTime + bufferMs; + DEBUG_LOG("BeginCast #" << lastCastId + << " " << game::GetSpellName(spell->Id) + << "(" << spell->Id << ")" + << " cast time: " << castTime + << " buffer: " << bufferMs + << " Gcd: " << gcdTime + << " latency: " << GetLatencyMs() + << " time since last cast " << currentTime - gLastCastData.startTimeMs); + } else { + gCastData.delayEndMs = currentTime + + gUserSettings.nonGcdBufferTimeMs; // set small "cast time" to avoid attempting next spell too fast + DEBUG_LOG("BeginCast #" << lastCastId + << " " << game::GetSpellName(spell->Id) + << "(" << spell->Id << ")" + << " cast time: " << castTime + << " buffer: " << bufferMs + << " NO Gcd" + << " latency: " << GetLatencyMs() + << " time since last cast " << currentTime - gLastCastData.startTimeMs); + } + + gCastData.castEndMs = castTime ? currentTime + castTime + bufferMs : 0; + gCastData.bufferMs = bufferMs; + + // check if we can lower buffers + if (currentTime - gLastBufferDecreaseTimeMs > BUFFER_DECREASE_FREQUENCY) { + if (gBufferTimeMs > gUserSettings.minBufferTimeMs) { + gBufferTimeMs -= DYNAMIC_BUFFER_INCREMENT; + DEBUG_LOG("Decreasing default buffer to " << gBufferTimeMs); + } + + gLastBufferDecreaseTimeMs = currentTime; // update the last error time to prevent lowering buffer too often + } + + gLastCastData.startTimeMs = currentTime; + + // if queued cast, simulate spellcast start + } + + void CastQueuedNonGcdSpell() { + if (gCastData.nonGcdSpellQueued) { + auto nonGcdCastParams = gNonGcdCastQueue.pop(); + if (nonGcdCastParams.spellId > 0) { + DEBUG_LOG("Triggering queued non gcd cast of " << game::GetSpellName(nonGcdCastParams.spellId)); + + gCastData.castingQueuedSpell = true; + gCastData.numRetries = nonGcdCastParams.numRetries; + Spell_C_CastSpellHook(castSpellDetour, nonGcdCastParams.playerUnit, nonGcdCastParams.spellId, + nonGcdCastParams.item, nonGcdCastParams.guid); + gLastCastData.wasQueued = true; + } else { + DEBUG_LOG("Ignoring queued non gcd cast, no spell id"); + gLastCastData.wasQueued = false; + } + TriggerSpellQueuedEvent(NON_GCD_QUEUE_POPPED, nonGcdCastParams.spellId); + gCastData.nonGcdSpellQueued = !gNonGcdCastQueue.isEmpty(); + gCastData.castingQueuedSpell = false; + gCastData.numRetries = 0; + } + } + + void CastQueuedNormalSpell() { + if (gCastData.normalSpellQueued) { + if (gLastNormalCastParams.spellId > 0) { + DEBUG_LOG("Triggering queued cast of " << game::GetSpellName(gLastNormalCastParams.spellId)); + gCastData.castingQueuedSpell = true; + gCastData.numRetries = gLastNormalCastParams.numRetries; + Spell_C_CastSpellHook(castSpellDetour, gLastNormalCastParams.playerUnit, gLastNormalCastParams.spellId, + gLastNormalCastParams.item, gLastNormalCastParams.guid); + gLastCastData.wasQueued = true; + } else { + DEBUG_LOG("Ignoring queued cast, no spell id"); + gLastCastData.wasQueued = false; + } + TriggerSpellQueuedEvent(NORMAL_QUEUE_POPPED, gLastNormalCastParams.spellId); + gCastData.normalSpellQueued = false; + gCastData.castingQueuedSpell = false; + gCastData.numRetries = 0; + } + } + + void CastQueuedSpells() { + if (gCastData.nonGcdSpellQueued) { + CastQueuedNonGcdSpell(); + } else { + CastQueuedNormalSpell(); + } + } + + void SaveCastParams(CastSpellParams *params, + uint32_t *playerUnit, + uint32_t spellId, + uintptr_t *item, + std::uint64_t guid, + uint32_t gcDCategory, + uint32_t castTimeMs, + uint32_t castStartTimeMs, + CastType castType, + uint32_t numRetries) { + params->playerUnit = playerUnit; + params->spellId = spellId; + params->item = item; + params->guid = guid; + + params->gcDCategory = gcDCategory; + params->castTimeMs = castTimeMs; + params->castStartTimeMs = castStartTimeMs; + params->castType = castType; + params->numRetries = numRetries; + } + + void TriggerSpellQueuedEvent(QueueEvents queueEventCode, uint32_t spellId) { + ((int (__cdecl *)(int, char *, uint32_t, uint32_t)) Offsets::SignalEventParam)( + game::SPELL_QUEUE_EVENT, // SPELL_QUEUE_EVENT event we are adding + (char *) Offsets::IntIntParamFormat, + queueEventCode, + spellId); + } + + void + TriggerSpellCastEvent(bool result, uint32_t spellId, CastType castType, std::uint64_t guid, uint32_t itemId) { + char format[] = "%d%d%d%s%d"; + char *guidStr = new char[21]; // 2 for 0x prefix, 18 for the number, and 1 for '\0' + std::snprintf(guidStr, 21, "0x%016llX", static_cast(guid)); + + ((int (__cdecl *)(int, char *, uint32_t, uint32_t, uint32_t, char *, uint32_t)) Offsets::SignalEventParam)( + game::SPELL_CAST_EVENT, // SPELL_CAST_EVENT event we are adding + format, + result, + spellId, + castType, + guidStr, + itemId); + } + + void clearCastingSpell() { + // clearing current casting spell id if needed + // this prevents client from failing to cast spells without a casttime + // due to not receiving spell result yet + auto const castingSpellId = reinterpret_cast(Offsets::CastingSpellId); + if (*castingSpellId > 0) { + *castingSpellId = 0; + } + } + + void setSelectionTarget(uint64_t target) { + auto dataStore = CDataStore(); + uint32_t opcode = 317; // CMSG_SET_SELECTION + dataStore.Put(opcode); + dataStore.Put(target); + + dataStore.Finalize(); + + auto const clientServicesSend = reinterpret_cast(Offsets::ClientServices_Send); + clientServicesSend(&dataStore); + } + + bool + Spell_C_CastSpellHook(hadesmem::PatchDetourBase *detour, uint32_t *casterUnit, uint32_t spellId, uintptr_t *item, + std::uint64_t guid) { + // save the detour to allow quickly calling this hook + castSpellDetour = detour; + + if (item == nullptr && casterUnit != game::GetObjectPtr(game::ClntObjMgrGetActivePlayerGuid())) { + DEBUG_LOG("Ignoring non active player cast of spell " << game::GetSpellName(spellId) << " " << spellId); + // just call original function if caster is not the active player + // happens with Doomguard rain of fire + auto const castSpell = detour->GetTrampolineT(); + return castSpell(casterUnit, spellId, item, guid); + } + + auto const spell = game::GetSpellInfo(spellId); + auto const spellIsOnSwing = SpellIsOnSwing(spell); + auto const spellName = game::GetSpellName(spellId); + auto currentTime = GetTime(); + auto const castTime = game::GetCastTime(casterUnit, spellId); + gCastData.attemptedCastTimeMs = castTime; + + auto const spellOnGcd = SpellIsOnGcd(spell); + auto const spellIsChanneling = SpellIsChanneling(spell); + auto const spellIsTargeting = SpellIsTargeting(spell); + auto const isSpecialSpell = SpellIsAttackTradeskillOrEnchant(spell); + auto const currentTargetGuid = game::GetCurrentTargetGuid(); + + if(spellIsChanneling) { + auto casterGuid = game::UnitGetGuid(casterUnit); + if (casterGuid == game::ClntObjMgrGetActivePlayerGuid()) { + // check that locked target guid matches our unit target guid + auto unitTargetGuid = game::UnitGetTargetGuid(casterUnit); + if (unitTargetGuid != currentTargetGuid) { + DEBUG_LOG("Updating selection target to " << currentTargetGuid << " from " << unitTargetGuid); + setSelectionTarget(currentTargetGuid); + } + } + } + + // check for double press to interrupt channeling early + if (gCastData.channeling && !gCastData.cancelChannelNextTick && + gCastData.numRetries == 0 && + gUserSettings.doubleCastToEndChannelEarly && + gCastData.channelStartMs > 0) { + // wait 500ms after the start of a channel before allowing double cast to end it early + if (currentTime - gCastData.channelStartMs > 500) { + // check if same spell is being cast again within 350ms + if (gLastCastData.attemptSpellId == spellId && currentTime - gLastCastData.attemptTimeMs < 350) { + DEBUG_LOG("Double cast detected for " << spellName << ", ending channel early"); + gCastData.cancelChannelNextTick = true; + } + } + } + + if (gCastData.nonGcdSpellQueued && spellOnGcd) { + // it is possible when spamming to attempt to cast before non gcd spells are processed, process queue first + if (processQueues()) { + return false; + } + } + + gLastCastData.attemptTimeMs = currentTime; + gLastCastData.attemptSpellId = spellId; + + uint32_t itemId = 0; + if (item) { + itemId = game::GetItemId((game::CGItem_C *) item); + } + + DEBUG_LOG("Attempt cast " << spellName << " item " << item << " on guid " << guid << " target " << currentTargetGuid + << ", time since last cast " << currentTime - gLastCastData.startTimeMs); + + // clear cooldown queue if we are casting a spell + if (spellOnGcd && gCastData.cooldownNormalSpellQueued) { + gCastData.cooldownNormalSpellQueued = false; + TriggerSpellQueuedEvent(NORMAL_QUEUE_POPPED, gLastNormalCastParams.spellId); + } else if (gCastData.cooldownNonGcdSpellQueued) { + gCastData.cooldownNonGcdSpellQueued = false; + // pop the params + auto nonGcdParams = gNonGcdCastQueue.pop(); + TriggerSpellQueuedEvent(NON_GCD_QUEUE_POPPED, nonGcdParams.spellId); + } + + // on swing spells are independent of cast bar / gcd, handle them separately + if (spellIsOnSwing) { + SaveCastParams(&gLastOnSwingCastParams, casterUnit, spellId, item, guid, spell->StartRecoveryCategory, + castTime, + currentTime, ON_SWING, 0); + + gCastHistory.pushFront({gNextCastId, casterUnit, spellId, item, guid, + spell->StartRecoveryCategory, + castTime, + currentTime, + ON_SWING, + gCastData.numRetries, + CastResult::WAITING_FOR_CAST}); + gNextCastId++; + + // try to cast the spell + auto const castSpell = detour->GetTrampolineT(); + auto ret = castSpell(casterUnit, spellId, item, guid); + + TriggerSpellCastEvent(ret, spellId, ON_SWING, guid, itemId); + + if (ret) { + gCastData.pendingOnSwingCast = true; + gCastData.onSwingSpellId = spellId; + } + + if (!ret && gUserSettings.queueOnSwingSpells && !gNoQueueCast) { + // if not in cooldown window + if (currentTime - gLastCastData.onSwingStartTimeMs > gUserSettings.onSwingBufferCooldownMs) { + DEBUG_LOG("Queuing on swing spell " << spellName); + TriggerSpellQueuedEvent(ON_SWING_QUEUED, spellId); + gCastData.onSwingQueued = true; + } + } else { + gLastCastData.onSwingStartTimeMs = GetTime(); + DEBUG_LOG("Successful on swing spell " << spellName); + } + + return ret; + } + + auto const castSpell = detour->GetTrampolineT(); + + auto effectiveCastEndMs = EffectiveCastEndMs(); + auto remainingEffectiveCastTime = (effectiveCastEndMs > currentTime) ? effectiveCastEndMs - currentTime : 0; + auto remainingGcd = (gCastData.gcdEndMs > currentTime) ? gCastData.gcdEndMs - currentTime : 0; + + auto remainingCD = (remainingEffectiveCastTime > remainingGcd) ? remainingEffectiveCastTime : remainingGcd; + + auto inSpellQueueWindow = InSpellQueueWindow(remainingEffectiveCastTime, remainingGcd, spellIsTargeting); + + // don't queue trade skills or enchants + if (isSpecialSpell) { + inSpellQueueWindow = false; + } + + if (spellOnGcd) { + auto castType = NORMAL; + if (spellIsTargeting) { + castType = TARGETING; + } else if (spellIsChanneling) { + castType = CHANNEL; + } + + SaveCastParams(&gLastNormalCastParams, casterUnit, spellId, item, guid, + spell->StartRecoveryCategory, + castTime, + currentTime, castType, 0); + } + + // skip queueing if gNoQueueCast is set + // skip queueing if spellIsChanneling and gUserSettings.queueChannelingSpells is false + if (!gNoQueueCast && (!spellIsChanneling || gUserSettings.queueChannelingSpells)) { + if (spellIsTargeting) { + if (gUserSettings.queueTargetingSpells) { + if (castTime > 0 && inSpellQueueWindow) { + if (gUserSettings.queueCastTimeSpells) { + DEBUG_LOG("Queuing targeting for after cast/gcd: " << remainingCD << "ms " << spellName); + TriggerSpellQueuedEvent(NORMAL_QUEUED, spellId); + gCastData.normalSpellQueued = true; + return false; + } + } else if (inSpellQueueWindow) { + if (gUserSettings.queueInstantSpells) { + if (spellOnGcd) { + DEBUG_LOG("Queuing instant cast targeting for after cast/gcd: " << remainingCD << "ms " + << spellName); + TriggerSpellQueuedEvent(NORMAL_QUEUED, spellId); + gCastData.normalSpellQueued = true; + return false; + } else if (remainingEffectiveCastTime > 0) { + auto castParams = gNonGcdCastQueue.findSpellId(spellId); + if (castParams) { + DEBUG_LOG("Updating instant cast non GCD targeting params for " << spellName); + castParams->guid = guid; + return false; + } else { + DEBUG_LOG("Queuing instant cast non GCD targeting for after cast/gcd: " + << remainingEffectiveCastTime << "ms " << spellName + << " gcd category " + << spell->StartRecoveryCategory); + + gNonGcdCastQueue.push({0, casterUnit, spellId, item, guid, + spell->StartRecoveryCategory, + castTime, + 0, + ::NON_GCD, + false}, gUserSettings.replaceMatchingNonGcdCategory); + TriggerSpellQueuedEvent(NON_GCD_QUEUED, spellId); + gCastData.nonGcdSpellQueued = true; + return false; + } + } + } + } + } + } else if (castTime > 0 && inSpellQueueWindow) { + if (gUserSettings.queueCastTimeSpells) { + if (spellOnGcd) { + DEBUG_LOG("Queuing for after cast/gcd: " << remainingCD << "ms " << spellName); + TriggerSpellQueuedEvent(NORMAL_QUEUED, spellId); + gCastData.normalSpellQueued = true; + return false; + } else if (remainingEffectiveCastTime > 0) { + auto castParams = gNonGcdCastQueue.findSpellId(spellId); + if (castParams) { + DEBUG_LOG("Updating non GCD params for " << spellName); + castParams->guid = guid; + return false; + } else { + DEBUG_LOG("Queuing non GCD for after cast/gcd: " + << remainingEffectiveCastTime << "ms " << spellName << " gcd category " + << spell->StartRecoveryCategory); + + gNonGcdCastQueue.push({0, casterUnit, spellId, item, guid, + spell->StartRecoveryCategory, + castTime, + 0, + ::NON_GCD, + false}, gUserSettings.replaceMatchingNonGcdCategory); + TriggerSpellQueuedEvent(NON_GCD_QUEUED, spellId); + gCastData.nonGcdSpellQueued = true; + return false; + } + } + } + } else if (inSpellQueueWindow) { + if ((spellIsChanneling && gUserSettings.queueChannelingSpells) || + (!spellIsChanneling && gUserSettings.queueInstantSpells)) { + + auto desc = "instant cast"; + if (spellIsChanneling) { + desc = "channeling"; + } + + if (spellOnGcd) { + DEBUG_LOG("Queuing " << desc << " for after cast/gcd: " << remainingCD << "ms " << spellName); + TriggerSpellQueuedEvent(NORMAL_QUEUED, spellId); + gCastData.normalSpellQueued = true; + return false; + } else if (remainingEffectiveCastTime > 0) { + auto castParams = gNonGcdCastQueue.findSpellId(spellId); + if (castParams) { + DEBUG_LOG("Updating " << desc << " non GCD params for " << spellName); + castParams->guid = guid; + return false; + } else { + DEBUG_LOG("Queuing " << desc << " non GCD for after cast/gcd: " + << remainingEffectiveCastTime << "ms " << spellName << " gcd category " + << spell->StartRecoveryCategory); + + gNonGcdCastQueue.push({0, casterUnit, spellId, item, guid, + spell->StartRecoveryCategory, + castTime, + 0, + ::NON_GCD, + false}, gUserSettings.replaceMatchingNonGcdCategory); + TriggerSpellQueuedEvent(NON_GCD_QUEUED, spellId); + gCastData.nonGcdSpellQueued = true; + return false; + } + } + } + } + } + + if (!isSpecialSpell) { + // is there a cast? (ignore for on swing spells) + if (remainingEffectiveCastTime) { + DEBUG_LOG("Cast or delay active " << remainingEffectiveCastTime << "ms remaining"); + return false; + } else { + gCastData.castEndMs = 0; + } + + // is there a Gcd? + if (spellOnGcd && remainingGcd) { + DEBUG_LOG("Gcd active " << remainingGcd << "ms remaining"); + return false; + } else { + gCastData.gcdEndMs = 0; + } + } + + // prevent casting instant cast spells and spells with SPELL_ATTR_DISABLED_WHILE_ACTIVE + // if cast in the last second and still waiting for server result or succeeded + // otherwise can break cooldown in the client and cause unnecessary errors + if (castTime == 0 || spell->Attributes & game::SPELL_ATTR_DISABLED_WHILE_ACTIVE) { + auto castParams = gCastHistory.findNewestWaitingForServerSpellId(spellId); + if (castParams && + currentTime - castParams->castStartTimeMs < 500) { + DEBUG_LOG("Ignoring " << spellName + << " cast still waiting for server result for the same spell"); + return false; + } else { + castParams = gCastHistory.findNewestSuccessfulSpellId(spellId); + if (castParams && + castParams->guid == guid && + currentTime - castParams->castStartTimeMs < 500) { + DEBUG_LOG("Ignoring " << spellName + << " cast recently succeeded for the same spell and target"); + return false; + } + } + } + + // try clearing current casting spell id if + // not using tradeskill or enchant + // no on swing spell queued (will interrupt them) + if (!isSpecialSpell && !gCastData.pendingOnSwingCast) { + clearCastingSpell(); + } + + // add to cast history + auto castType = CastType::NORMAL; + if (spellIsChanneling) { + castType = CastType::CHANNEL; + } else if (spellIsTargeting) { + if (spellOnGcd) { + castType = CastType::TARGETING; + } else { + castType = CastType::TARGETING_NON_GCD; + } + } else if (!spellOnGcd) { + castType = CastType::NON_GCD; + } + + gCastHistory.pushFront({gNextCastId, casterUnit, spellId, item, guid, + spell->StartRecoveryCategory, + castTime, + currentTime, + castType, + gCastData.numRetries, + CastResult::WAITING_FOR_CAST}); + gNextCastId++; + + auto ret = castSpell(casterUnit, spellId, item, guid); + + // if this is a trade skill or item enchant, do nothing further + if (isSpecialSpell) { + TriggerSpellCastEvent(ret, spellId, castType, guid, itemId); + return ret; + } + + // haven't gotten spell result from the previous cast yet, probably due to latency. + // simulate a cancel to clear the cast bar but only when there should be a cast time + // mining/herbing have cast time but aren't on Gcd, don't cancel them + if (!ret && gLastCastData.castTimeMs > 0 && gLastCastData.wasOnGcd) { + if (*reinterpret_cast(Offsets::SpellIsTargeting) == 0 && !gCastData.pendingOnSwingCast && + !IsSpellOnCooldown(spellId)) { + DEBUG_LOG("Canceling spell cast due to previous spell having cast time of " + << gLastCastData.castTimeMs); + + //JT: Suggest replacing CancelSpell with InterruptSpell (the API called when moving during casting). + // The address of InterruptSpell needs to be dug out. It could possibly fix the sometimes broken animations. + gCastData.cancellingSpell = true; + + auto const cancelSpell = reinterpret_cast(Offsets::CancelSpell); + cancelSpell(false, false, game::SPELL_FAILED_ERROR); + + gCastData.cancellingSpell = false; + + clearCastingSpell(); + + // try again now that cast bar is gone + ret = castSpell(casterUnit, spellId, item, guid); + + auto const cursorMode = *reinterpret_cast(Offsets::CursorMode); + if (!ret && !(spell->Attributes & game::SPELL_ATTR_RANGED) && cursorMode != 2) { + DEBUG_LOG("Retry cast after cancel still failed"); + } + } else { + DEBUG_LOG("Initial cast failed but not canceling spell cast"); + } + } + + TriggerSpellCastEvent(ret, spellId, castType, guid, itemId); + + return ret; + } + + void + SpellGoHook(hadesmem::PatchDetourBase *detour, uint64_t *casterGUID, uint64_t *targetGUID, uint32_t spellId, + CDataStore *spellData) { + auto const spellGo = detour->GetTrampolineT(); + spellGo(casterGUID, targetGUID, spellId, spellData); + + auto const castByActivePlayer = game::ClntObjMgrGetActivePlayerGuid() == *casterGUID; + + if (castByActivePlayer) { + auto const currentTime = GetTime(); + // only care about our own casts + if (!gCastData.channeling) { + // check if spell is on swing + auto const spell = game::GetSpellInfo(spellId); + if (spell->Attributes & game::SPELL_ATTR_ON_NEXT_SWING_1) { + gLastCastData.onSwingStartTimeMs = currentTime; + + gCastData.pendingOnSwingCast = false; + + if (gCastData.onSwingQueued) { + DEBUG_LOG("On swing spell " << game::GetSpellName(spellId) << + " resolved, casting queued on swing spell " + << game::GetSpellName(gLastOnSwingCastParams.spellId)); + + + TriggerSpellQueuedEvent(ON_SWING_QUEUE_POPPED, gLastOnSwingCastParams.spellId); + Spell_C_CastSpellHook(castSpellDetour, gLastOnSwingCastParams.playerUnit, + gLastOnSwingCastParams.spellId, + gLastOnSwingCastParams.item, gLastOnSwingCastParams.guid); + gCastData.onSwingQueued = false; + } + } + } + } + } + + void SetReleaseAction(uint32_t input) { + uint32_t activeControl = *reinterpret_cast(Offsets::CGInputControlGetActive); + + typedef void(__thiscall *SetReleaseActionT)(uint32_t, uint32_t); + auto SetReleaseAction = reinterpret_cast(Offsets::CGInputControlSetReleaseAction); + SetReleaseAction(activeControl, input); + } + + void SetControlBit(uint32_t input) { + uint32_t activeControl = *reinterpret_cast(Offsets::CGInputControlGetActive); + auto *LastHardwareAction = reinterpret_cast(Offsets::LastHardwareAction); + + typedef void(__thiscall *SetControlBitT)(uint32_t, uint32_t, uint32_t, uintptr_t *, int); + auto SetControlBit = reinterpret_cast(Offsets::CGInputControlSetControlBit); + SetControlBit(activeControl, 2, input, LastHardwareAction, 0); + } + + + void CameraOrSelectOrMoveStart() { + SetReleaseAction(1); + SetControlBit(1); + } + + void CameraOrSelectOrMoveStop() { + SetControlBit(0); + } + + bool Spell_C_TargetSpellHook(hadesmem::PatchDetourBase *detour, + uint32_t *player, + uint32_t *spellId, + uint32_t unk3, + float unk4) { + auto const spellTarget = detour->GetTrampolineT(); + auto result = spellTarget(player, spellId, unk3, unk4); + + if (!result) { + auto const spellName = game::GetSpellName(*spellId); + auto const spell = game::GetSpellInfo(*spellId); + + if (spell->Targets == game::SpellTarget::TARGET_LOCATION_UNIT_POSITION && + spell->Effect[0] != game::SPELL_EFFECT_SUMMON_GUARDIAN) { + // if quickcast is on instantly trigger all casts + // otherwise if this is a queued cast, trigger it instant cast + if (gUserSettings.quickcastTargetingSpells || + (gUserSettings.queueTargetingSpells && gCastData.castingQueuedSpell)) { + DEBUG_LOG("Quickcasting terrain spell " << spellName + << " quickcast: " + << gUserSettings.quickcastTargetingSpells + << " queuetrigger: " << gCastData.castingQueuedSpell); + + // store the current target + auto const targetGuid = game::GetCurrentTargetGuid(); + + CameraOrSelectOrMoveStart(); + CameraOrSelectOrMoveStop(); + + // check if target changed + if (targetGuid != game::GetCurrentTargetGuid()) { + DEBUG_LOG("Target changed during quick cast, restoring previous target " << targetGuid); + auto const targetUnit = reinterpret_cast(Offsets::CGGameUI_Target); + targetUnit(targetGuid); + } + } + } + } + return result; + } + + void + CancelSpellHook(hadesmem::PatchDetourBase *detour, bool failed, bool notifyServer, + game::SpellCastResult reason) { + // triggered by us, reset the cast bar + if (notifyServer) { + ResetCastFlags(); + } else if (failed) { + DEBUG_LOG("Cancel spell cast failed:" << failed << + " notifyServer:" << notifyServer << " reason:" << int(reason)); + } + + auto const cancelSpell = detour->GetTrampolineT(); + return cancelSpell(failed, notifyServer, reason); + } + + void SendCastHook(hadesmem::PatchDetourBase *detour, game::SpellCast *cast, char unk) { + auto const sendCast = detour->GetTrampolineT(); + sendCast(cast, unk); + + auto const spell = game::GetSpellInfo(cast->spellId); + BeginCast(gCastData.attemptedCastTimeMs, spell, cast); + } + +} \ No newline at end of file diff --git a/nampower/spellcast.hpp b/nampower/spellcast.hpp new file mode 100644 index 0000000..88e4048 --- /dev/null +++ b/nampower/spellcast.hpp @@ -0,0 +1,36 @@ +// +// Created by pmacc on 9/21/2024. +// + +#pragma once + +#include "game.hpp" +#include +#include "main.hpp" + +namespace Nampower { + void CastQueuedNonGcdSpell(); + + void CastQueuedNormalSpell(); + + void CastQueuedSpells(); + + void TriggerSpellQueuedEvent(QueueEvents queueEventCode, uint32_t spellId); + + bool Spell_C_CastSpellHook(hadesmem::PatchDetourBase *detour, uint32_t *casterUnit, uint32_t spellId, uintptr_t *item, + std::uint64_t guid); + + void + CancelSpellHook(hadesmem::PatchDetourBase *detour, bool failed, bool notifyServer, game::SpellCastResult reason); + + void SpellGoHook(hadesmem::PatchDetourBase *detour, uint64_t *casterGUID, uint64_t *targetGUID, uint32_t spellId, + CDataStore *spellData); + + bool Spell_C_TargetSpellHook(hadesmem::PatchDetourBase *detour, + uint32_t *player, + uint32_t *spellId, + uint32_t unk3, + float unk4); + + void SendCastHook(hadesmem::PatchDetourBase *detour, game::SpellCast *cast, char unk); +} \ No newline at end of file diff --git a/nampower/spellchannel.cpp b/nampower/spellchannel.cpp new file mode 100644 index 0000000..3f4b12d --- /dev/null +++ b/nampower/spellchannel.cpp @@ -0,0 +1,125 @@ +// +// Created by pmacc on 9/21/2024. +// + +#include "spellchannel.hpp" +#include "spellcast.hpp" +#include "logging.hpp" +#include "offsets.hpp" + +namespace Nampower { + int SpellChannelStartHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet) { + auto const rpos = packet->m_read; + + uint32_t spellId; + packet->Get(spellId); + + uint32_t duration; + packet->Get(duration); + + packet->m_read = rpos; + + DEBUG_LOG("Channel start: " << game::GetSpellName(spellId) << " duration " << duration); + + if (duration > 0) { + // check if spell has "Far sight" flag which is used on mind control style abilities + auto spell = game::GetSpellInfo(spellId); + if (spell && !(spell->AttributesEx & game::SPELL_ATTR_EX_TOGGLE_FARSIGHT || + spellId == 19832 || // bwl orb spell + spellId == 23014 || // bwl orb spell + spellId == 13180) // mind control cap + ) { + auto currentTimeMs = GetTime(); + gCastData.channeling = true; + gCastData.channelStartMs = currentTimeMs; + gCastData.channelEndMs = currentTimeMs + duration; + gCastData.channelSpellId = spellId; + gCastData.channelDuration = duration; + + auto originalDuration = game::GetDurationObject(spell->DurationIndex)->m_Duration; + + float durationReduction = 1.0f; + + // if this was an Arcane Missiles cast, we need to check if we have the +1 aura + if (spellId == 5143 || + spellId == 5144 || + spellId == 5145 || + spellId == 8416 || + spellId == 8417 || + spellId == 10211 || + spellId == 10212 || + spellId == 25345) { + // check if we have duration mod > 0 + auto durationModifier = game::GetSpellModifier(spell, game::SPELLMOD_DURATION); + if (durationModifier > 0) { + DEBUG_LOG("Adding 1 second to Arcane Missiles duration due to duration mod " << durationModifier); + originalDuration += 1000; + } + } + + if (originalDuration > 0 && originalDuration < 1000000) { + durationReduction = float(duration) / float(originalDuration); + } else { + DEBUG_LOG("Invalid originalDuration of " << originalDuration << " for " + << game::GetSpellName(spellId)); + } + + uint32_t amplitude = spell->EffectAmplitude[0]; + if (amplitude <= 0 || amplitude > duration) { + amplitude = spell->EffectAmplitude[1]; + } + if (amplitude <= 0 || amplitude > duration) { + amplitude = spell->EffectAmplitude[2]; + } + + gCastData.channelTickTimeMs = uint32_t(float(amplitude) * + durationReduction); // the base tick time is usually on the first effect, scale it based on the duration reduction due to haste mechanics + + DEBUG_LOG("Original tick time " << amplitude << " scaled tick time " + << gCastData.channelTickTimeMs); + + if (gCastData.channelTickTimeMs <= 0 || gCastData.channelTickTimeMs > duration) { + DEBUG_LOG("Invalid channel tick time for " << game::GetSpellName(spellId) + << ", defaulting to duration"); + gCastData.channelTickTimeMs = duration; + } + + gCastData.channelNumTicks = 0; + + gLastCastData.channelStartTimeMs = currentTimeMs; + } else { + DEBUG_LOG("Ignoring/resetting channeling for " << game::GetSpellName(spellId)); + ResetChannelingFlags(); + } + } else { + ResetChannelingFlags(); + } + + auto const spellChannelStartHandler = detour->GetTrampolineT(); + return spellChannelStartHandler(opCode, packet); + } + + // this gets called on damage and when channel ends with the end time of the channel + int SpellChannelUpdateHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet) { + auto const rpos = packet->m_read; + + uint32_t channelRemainingTime; + packet->Get(channelRemainingTime); + + packet->m_read = rpos; + + if (channelRemainingTime <= 0) { + DEBUG_LOG("Channel done: " << game::GetSpellName(gCastData.channelSpellId) + << " elapsed " << (GetTime() - gLastCastData.channelStartTimeMs) + << " original duration " << gCastData.channelDuration); + ResetChannelingFlags(); + } else { + DEBUG_LOG("Channel update: " << game::GetSpellName(gCastData.channelSpellId) << " remaining " + << channelRemainingTime); + gCastData.channelEndMs = GetTime() + channelRemainingTime; + } + + auto const spellChannelUpdateHandler = detour->GetTrampolineT(); + return spellChannelUpdateHandler(opCode, packet); + } +} \ No newline at end of file diff --git a/nampower/spellchannel.hpp b/nampower/spellchannel.hpp new file mode 100644 index 0000000..b7db752 --- /dev/null +++ b/nampower/spellchannel.hpp @@ -0,0 +1,13 @@ +// +// Created by pmacc on 9/21/2024. +// + +#pragma once + +#include "main.hpp" + +namespace Nampower { + int SpellChannelStartHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet); + + int SpellChannelUpdateHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet); +} \ No newline at end of file diff --git a/nampower/spellevents.cpp b/nampower/spellevents.cpp new file mode 100644 index 0000000..bafb6ad --- /dev/null +++ b/nampower/spellevents.cpp @@ -0,0 +1,708 @@ +// +// Created by pmacc on 9/21/2024. +// + +#include "spellevents.hpp" +#include "offsets.hpp" +#include "logging.hpp" +#include "spellcast.hpp" +#include "helper.hpp" + +namespace Nampower { + uint32_t lastCastResultTimeMs; + + void SignalEventHook(hadesmem::PatchDetourBase *detour, game::Events eventId) { + auto const signalEvent = detour->GetTrampolineT(); + signalEvent(eventId); + } + + uint32_t Script_SpellTargetUnitHook(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) { + auto const spellTargetUnit = detour->GetTrampolineT(); + + // check if valid string + auto const lua_isstring = reinterpret_cast(Offsets::lua_isstring); + if (lua_isstring(luaState, 1)) { + auto const lua_tostring = reinterpret_cast(Offsets::lua_tostring); + auto const unitName = lua_tostring(luaState, 1); + + auto const getGUIDFromName = reinterpret_cast(Offsets::GetGUIDFromName); + auto const guid = getGUIDFromName(unitName); + if (guid) { + DEBUG_LOG("Spell target unit " << unitName << " guid " << guid); + // update all cast params so we don't have to figure out which one to use + gLastNormalCastParams.guid = guid; + gLastOnSwingCastParams.guid = guid; + + auto nonGcdCastParams = gNonGcdCastQueue.peek(); + if (nonGcdCastParams) { + nonGcdCastParams->guid = guid; + } + } + } + + return spellTargetUnit(luaState); + } + + void Spell_C_SpellFailedHook(hadesmem::PatchDetourBase *detour, uint32_t spellId, + game::SpellCastResult spellResult, int unk1, int unk2, char unk3) { + auto const spellFailed = detour->GetTrampolineT(); + spellFailed(spellId, spellResult, unk1, unk2, unk3); + + // ignore fake failure (used by Unleashed Potential and not sure what else) + if (spellResult == game::SpellCastResult::SPELL_FAILED_DONT_REPORT) { + return; + } + + // ignore SPELL_FAILED_CANT_DO_THAT_YET for arcane surge gets sent all the time after success + if (spellResult == game::SpellCastResult::SPELL_FAILED_CANT_DO_THAT_YET && + (spellId == 51933 || + spellId == 51934 || + spellId == 51935 || + spellId == 51936) + ) { + return; + } + + ResetCastFlags(); + + if (spellId == gCastData.onSwingSpellId) { + ResetOnSwingFlags(); + } + + if ((spellResult == game::SpellCastResult::SPELL_FAILED_NOT_READY || + spellResult == game::SpellCastResult::SPELL_FAILED_ITEM_NOT_READY || + spellResult == game::SpellCastResult::SPELL_FAILED_SPELL_IN_PROGRESS) + ) { + auto const currentTime = GetTime(); + + if (spellResult == game::SpellCastResult::SPELL_FAILED_NOT_READY) { + // check if spell is just on cooldown still + auto const spellCooldown = GetRemainingCooldownForSpell(spellId); + if (spellCooldown > 0) { + auto castParams = gCastHistory.findSpellId(spellId); + if (castParams) { + // mark as failed so it can be cast again + castParams->castResult = CastResult::SERVER_FAILURE; + } + + // check if we should do cooldown queuing + if (gUserSettings.queueSpellsOnCooldown && spellCooldown < gUserSettings.cooldownQueueWindowMs) { + if (castParams) { + if (castParams->castType == CastType::NON_GCD || + castParams->castType == CastType::TARGETING_NON_GCD) { + DEBUG_LOG("Non gcd spell " << game::GetSpellName(spellId) << " is still on cooldown " + << spellCooldown + << " queuing retry"); + gCastData.cooldownNonGcdSpellQueued = true; + gCastData.cooldownNonGcdEndMs = spellCooldown + currentTime; + + TriggerSpellQueuedEvent(QueueEvents::NON_GCD_QUEUED, spellId); + gNonGcdCastQueue.push(*castParams, gUserSettings.replaceMatchingNonGcdCategory); + } else { + DEBUG_LOG("Spell " << game::GetSpellName(spellId) << " is still on cooldown " + << spellCooldown + << " queuing retry"); + gCastData.cooldownNormalSpellQueued = true; + gCastData.cooldownNormalEndMs = spellCooldown + currentTime; + + TriggerSpellQueuedEvent(QueueEvents::NORMAL_QUEUED, spellId); + gLastNormalCastParams = *castParams; + } + return; + } + } else { + DEBUG_LOG("Spell " << game::GetSpellName(spellId) << " is still on cooldown " << spellCooldown); + return; + } + } + } + + if (!gUserSettings.retryServerRejectedSpells) { + DEBUG_LOG("Cast failed for " << game::GetSpellName(spellId) + << " code " << int(spellResult) + << " not queuing retry due to retryServerRejectedSpells=false"); + return; + } + gLastErrorTimeMs = currentTime; + + uint64_t lastCastId = 0; + + // otherwise see if we should retry the cast + auto castParams = gCastHistory.findSpellId(spellId); + if (castParams) { + lastCastId = castParams->castId; + // if we find non retried cast params and the original cast time is within the last 500ms, retry the cast + if (castParams->castStartTimeMs > currentTime - 500) { + // allow 3 retries + if (castParams->numRetries < 3) { + castParams->numRetries++; // mark as retried + + if (castParams->castType == CastType::NON_GCD || + castParams->castType == CastType::TARGETING_NON_GCD) { + + // see if we have a recent successful cast of this spell + auto successfulCastParams = gCastHistory.findNewestSuccessfulSpellId(spellId); + if (successfulCastParams && successfulCastParams->castStartTimeMs > currentTime - 1000) { + // we found a recent successful cast of this spell, ignore the failure that was the result + // of spamming the cast + DEBUG_LOG("Cast failed for #" << lastCastId << " " << game::GetSpellName(spellId) << " " + << " non gcd code " << int(spellResult) + << ", but a recent cast succeeded, not retrying"); + return; + } + + DEBUG_LOG("Cast failed for #" << lastCastId << " " << game::GetSpellName(spellId) << " " + << " non gcd code " << int(spellResult) + << ", retry " << castParams->numRetries + << " result " << castParams->castResult); + gCastData.delayEndMs = currentTime + gBufferTimeMs; // retry after buffer delay + TriggerSpellQueuedEvent(QueueEvents::NON_GCD_QUEUED, spellId); + gCastData.nonGcdSpellQueued = true; + gNonGcdCastQueue.push(*castParams, gUserSettings.replaceMatchingNonGcdCategory); + } else { + // if gcd is active, do nothing + if (gCastData.gcdEndMs > currentTime) { + DEBUG_LOG("Cast failed for #" << lastCastId << " " << game::GetSpellName(spellId) << " " + << " code " << int(spellResult) + << ", gcd active not retrying"); + } else { + DEBUG_LOG("Cast failed for #" << lastCastId << " " << game::GetSpellName(spellId) << " " + << " code " << int(spellResult) + << ", retry " << castParams->numRetries + << " result " << castParams->castResult); + + TriggerSpellQueuedEvent(QueueEvents::NORMAL_QUEUED, spellId); + gCastData.normalSpellQueued = true; + gLastNormalCastParams = *castParams; + } + } + } else { + DEBUG_LOG("Cast failed for #" << lastCastId << " " << game::GetSpellName(spellId) << " " + << " code " << int(spellResult) + << ", not retrying as it has already been retried 3 times"); + } + } else { + DEBUG_LOG("Cast failed for #" << lastCastId << " " << game::GetSpellName(spellId) << " " + << " code " << int(spellResult) + << ", no recent cast params found in history"); + } + + + // check if we should increase the buffer time + if (currentTime - gLastBufferIncreaseTimeMs > BUFFER_INCREASE_FREQUENCY) { + // check if gBufferTimeMs is already at max + if (gBufferTimeMs - gUserSettings.minBufferTimeMs < gUserSettings.maxBufferIncreaseMs) { + gBufferTimeMs += DYNAMIC_BUFFER_INCREMENT; + DEBUG_LOG("Increasing buffer to " << gBufferTimeMs); + gLastBufferIncreaseTimeMs = currentTime; + } + } + } + } else if (gCastData.normalSpellQueued) { + DEBUG_LOG("Cast failed for " << game::GetSpellName(spellId) + << " spell queued + ignored code " << int(spellResult)); + } else { + DEBUG_LOG("Cast failed for " << game::GetSpellName(spellId) + << " ignored code " << int(spellResult)); + } + } + + int SpellCooldownHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet) { + auto const spellCooldownHandler = detour->GetTrampolineT(); + + auto const rpos = packet->m_read; + + if (packet->m_size == 16) { + uint64_t targetGuid; + packet->Get(targetGuid); + + uint32_t spellId; + packet->Get(spellId); + + uint32_t cooldown; + packet->Get(cooldown); + + packet->m_read = rpos; + + DEBUG_LOG("Spell cooldown opcode:" << opCode << " for " << game::GetSpellName(spellId) << " cooldown " + << cooldown); + } + + return spellCooldownHandler(opCode, packet); + } + + void Spell_C_CooldownEventTriggeredHook(hadesmem::PatchDetourBase *detour, + uint32_t spellId, + uint64_t *targetGUID, + int param_3, + int clearCooldowns) { + DEBUG_LOG("Cooldown event triggered for " << game::GetSpellName(spellId) << " " << + targetGUID << " " << param_3 << " " << clearCooldowns); + + auto const cooldownEventTriggered = detour->GetTrampolineT(); + cooldownEventTriggered(spellId, targetGUID, param_3, clearCooldowns); + } + + int SpellDelayedHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet) { + auto const spellDelayed = detour->GetTrampolineT(); + + auto const rpos = packet->m_read; + + uint64_t guid; + packet->Get(guid); + + uint32_t delay; + packet->Get(delay); + + packet->m_read = rpos; + + auto const activePlayer = game::ClntObjMgrGetActivePlayerGuid(); + + if (guid == activePlayer) { + auto const currentTime = GetTime(); + + // if we are casting a spell, and it was delayed, update our own state so we do not allow a cast too soon + if (currentTime < gCastData.castEndMs) { + gCastData.castEndMs += delay; + + auto lastCastParams = gCastHistory.peek(); + if (lastCastParams != nullptr) { + DEBUG_LOG("Spell delayed by " << delay << " for cast #" << lastCastParams->castId << " " + << game::GetSpellName(lastCastParams->spellId) + << " cast end " << gCastData.castEndMs); + } + } + } + + return spellDelayed(opCode, packet); + } + + + int CastResultHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet) { + auto const castResultHandler = detour->GetTrampolineT(); + + auto const rpos = packet->m_read; + + uint32_t spellId; + packet->Get(spellId); + + uint8_t status; + packet->Get(status); + + uint8_t spellCastResult; + + if (status != 0) { + packet->Get(spellCastResult); + } else { + spellCastResult = 0; + } + + packet->Get(spellCastResult); + + packet->m_read = rpos; + + auto const currentLatency = GetLatencyMs(); + auto currentTime = GetTime(); + + // Reset the server delay in case we aren't able to calculate it + gLastServerSpellDelayMs = 0; + + // if the cast was successful and the spell cast result was successful, and we have a latency + // attempt to calculate the server delay + if (status == 0 && spellCastResult == 0 && currentLatency > 0) { + // try to find the cast in the history + auto maxStartTime = currentTime - currentLatency; + auto castParams = gCastHistory.findOldestWaitingForServerSpellId(spellId); + + if (castParams) { + // running spellResponseTimeMs average + auto const lastCastTime = castParams->castStartTimeMs; + auto const spellResponseTimeMs = int32_t(currentTime - lastCastTime); + + auto const serverDelay = spellResponseTimeMs - int32_t(currentLatency + castParams->castTimeMs); + + if (serverDelay > 0) { + gLastServerSpellDelayMs = serverDelay + 15; + } else { + DEBUG_LOG("Negative server delay using 1 ms " << serverDelay); + gLastServerSpellDelayMs = 1; + } + } + } + + uint64_t matchingCastId = 0; + + // update cast history + if (status == 0) { + auto castParams = gCastHistory.findOldestWaitingForServerSpellId(spellId); + if (castParams) { + // successes normally for the oldest cast + castParams->castResult = CastResult::SERVER_SUCCESS; + matchingCastId = castParams->castId; + } + } else { + auto castParams = gCastHistory.findNewestWaitingForServerSpellId(spellId); + if (castParams) { + // failures normally caused by the latest cast + castParams->castResult = CastResult::SERVER_FAILURE; + matchingCastId = castParams->castId; + } + } + + if (gLastServerSpellDelayMs > 0) { + DEBUG_LOG("Cast result for #" << matchingCastId << " " + << game::GetSpellName(spellId) + << "(" << spellId << ")" + << " status " << int(status) + << " result " << int(spellCastResult) << " latency " << currentLatency + << " server delay " << gLastServerSpellDelayMs + << " since last cast result " << currentTime - lastCastResultTimeMs); + } else { + DEBUG_LOG("Cast result for #" << matchingCastId << " " + << game::GetSpellName(spellId) + << "(" << spellId << ")" + << " status " << int(status) + << " result " << int(spellCastResult) << " latency " << currentLatency + << " since last cast result " << currentTime - lastCastResultTimeMs); + } + + lastCastResultTimeMs = currentTime; + + return castResultHandler(opCode, packet); + } + + int SpellFailedHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet) { + auto const spellFailedHandler = detour->GetTrampolineT(); + + auto const rpos = packet->m_read; + + uint64_t guid; + packet->Get(guid); + + uint32_t spellId; + packet->Get(spellId); + + packet->m_read = rpos; + + DEBUG_LOG("Spell failed opcode:" << opCode << " for " << game::GetSpellName(spellId) << " guid " << guid); + + return spellFailedHandler(opCode, packet); + } + + int SpellStartHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t unk, uint32_t opCode, uint32_t unk2, + CDataStore *packet) { + auto const spellStartHandler = detour->GetTrampolineT(); + + auto const rpos = packet->m_read; + + uint32_t previousVisualSpellId = 0; + + if (opCode == 0x131) { + // 8 + 8 + 4 + 2 + 4 but first 2 guids are packed + uint64_t itemGuid; + packet->GetPackedGuid(itemGuid); + + uint64_t casterGuid; + packet->GetPackedGuid(casterGuid); + + if (casterGuid == game::ClntObjMgrGetActivePlayerGuid()) { + uint32_t spellId; + packet->Get(spellId); + + uint16_t castFlags; + packet->Get(castFlags); + + uint32_t castTime; + packet->Get(castTime); + + bool isAutoRepeat = false; + + auto spellInfo = game::GetSpellInfo(spellId); + if (spellInfo) { + isAutoRepeat = spellInfo->AttributesEx2 & game::SpellAttributesEx2::SPELL_ATTR_EX2_AUTO_REPEAT; + } + + auto castParams = gCastHistory.findSpellId(spellId); + + // check if cast time differed from what we expected, ignore haste rounding errors + if (castParams && castParams->spellId == spellId) { + if (!isAutoRepeat && castParams->castTimeMs < castTime) { + // server cast time increased + auto castTimeDifference = castTime - castParams->castTimeMs; + + if (castTimeDifference > 5) { + gCastData.castEndMs = castParams->castStartTimeMs + castTime; + if (castTime > 0) { + gCastData.castEndMs += gBufferTimeMs; + } + + castParams->castTimeMs = castTime; + DEBUG_LOG("Server cast time for " << game::GetSpellName(spellId) << " increased by " + << castTimeDifference << "ms. Updated cast end time to " + << gCastData.castEndMs); + } + } else if (!isAutoRepeat && castParams->castTimeMs > castTime) { + // server cast time decreased + auto castTimeDifference = castParams->castTimeMs - castTime; + + if (castTimeDifference > 5) { + auto gcdTime = GetGcdOrCooldownForSpell(spellId); + if (gcdTime > 1500) { + gcdTime = 1500; // items with spells on gcd will return their item gcd, make sure not to use that + } + + if (gcdTime > castTime) { + DEBUG_LOG("Server cast time " << castTime << " was reduced below gcd of " << gcdTime + << " using gcd instead"); + castTime = gcdTime; + gCastData.castEndMs = castParams->castStartTimeMs + gcdTime; + } else { + gCastData.castEndMs = castParams->castStartTimeMs + castTime + gBufferTimeMs; + + } + + castParams->castTimeMs = castTime; + DEBUG_LOG("Server cast time for " << game::GetSpellName(spellId) << " decreased by " + << castTimeDifference << "ms. Updated cast end time to " + << gCastData.castEndMs); + } + } + + // spell start successful, reset castParams->numRetries + castParams->numRetries = 0; + } + + // spell start successful, reset gLastNormalCastParams.numRetries + gLastNormalCastParams.numRetries = 0; + + // avoid clearing visual spell id as much as possible as it can cause weird sound/animation issues + // only do it for normal gcd spells with a cast time + if (castTime > 0 && gLastCastData.wasQueued && gLastCastData.wasOnGcd && !gLastCastData.wasItem) { + auto visualSpellId = reinterpret_cast(Offsets::VisualSpellId); + // only clear if the current visual spell id is the same as the spell we are casting + // as that seems to be when cast animation breaks + if (*visualSpellId == spellId) { + *visualSpellId = 0; + + previousVisualSpellId = spellId; + } + } + } + } + + packet->m_read = rpos; + + auto result = spellStartHandler(unk, opCode, unk2, packet); + + if (previousVisualSpellId > 0) { + auto currentSpellId = reinterpret_cast(Offsets::VisualSpellId); + *currentSpellId = previousVisualSpellId; + } + + return result; + } + + void + TriggerSpellDamageEvent(uint64_t targetGuid, + uint64_t casterGuid, + uint32_t spellId, + uint32_t amount, + uint32_t spellSchool, + uint32_t absorb, + uint32_t blocked, + int32_t resist, + uint32_t auraType, + uint32_t hitInfo) { + char format[] = "%s%s%d%d%s%d%d%s"; + + char *targetGuidStr = ConvertGuidToString(targetGuid); + + char *casterGuidStr = ConvertGuidToString(casterGuid); + + auto event = game::SPELL_DAMAGE_EVENT_OTHER; + + if (casterGuid == game::ClntObjMgrGetActivePlayerGuid()) { + event = game::SPELL_DAMAGE_EVENT_SELF; + + if (gCastData.channeling && gCastData.channelSpellId == spellId) { + gCastData.channelNumTicks++; + } + } + + auto spell = game::GetSpellInfo(spellId); + + std::ostringstream mitigationStream; + mitigationStream << absorb << "," << blocked << "," << resist; + + std::ostringstream effectsAuraTypeStream; + if (spell) { + effectsAuraTypeStream << spell->Effect[0] << "," << spell->Effect[1] << "," << spell->Effect[2]; + } else { + effectsAuraTypeStream << "0,0,0"; + } + // add aura type to the end + effectsAuraTypeStream << "," << auraType; + + ((int (__cdecl *)(int eventCode, + char *format, + char *targetGuid, + char *casterGuid, + uint32_t spellId, + uint32_t amount, + const char *mitigationStr, + uint32_t hitInfo, + uint32_t spellSchool, + const char *effectAuraStr)) Offsets::SignalEventParam)( + event, + format, + targetGuidStr, + casterGuidStr, + spellId, + amount, + mitigationStream.str().c_str(), + hitInfo, + spellSchool, + effectsAuraTypeStream.str().c_str()); + } + + int PeriodicAuraLogHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t unk, uint32_t opCode, uint32_t unk2, + CDataStore *packet) { + auto const rpos = packet->m_read; + + uint64_t targetGuid; + packet->GetPackedGuid(targetGuid); + + uint64_t casterGuid; + packet->GetPackedGuid(casterGuid); + + uint32_t spellId; + packet->Get(spellId); + + uint32_t count; + packet->Get(count); + + uint32_t auraType; + packet->Get(auraType); + + uint32_t amount; + uint32_t powerType; + + switch (auraType) { + case 3: // SPELL_AURA_PERIODIC_DAMAGE + case 89: // SPELL_AURA_PERIODIC_DAMAGE_PERCENT + packet->Get(amount); // damage amount + + uint32_t spellSchool; + packet->Get(spellSchool); + + uint32_t absorb; + packet->Get(absorb); + + int32_t resist; + packet->Get(resist); + + TriggerSpellDamageEvent(targetGuid, casterGuid, spellId, amount, spellSchool, absorb, 0, resist, + auraType, + 0); + break; + case 8: // SPELL_AURA_PERIODIC_HEAL + case 20: // SPELL_AURA_OBS_MOD_HEALTH + packet->Get(amount); // heal amount + + // No custom event for these for now + break; + case 21: // SPELL_AURA_OBS_MOD_MANA + case 24: // SPELL_AURA_PERIODIC_ENERGIZE + packet->Get(powerType); + + packet->Get(amount); // power type amount + + // No custom event for these for now + break; + case 64: // SPELL_AURA_PERIODIC_MANA_LEECH + packet->Get(powerType); + packet->Get(amount); // power type amount + + uint32_t multiplier; + packet->Get(multiplier); // gain multiplier + + // No custom event for these for now + break; + default: + break; + } + + packet->m_read = rpos; + + auto const periodicAuraLogHandler = detour->GetTrampolineT(); + return periodicAuraLogHandler(unk, opCode, unk2, packet); + } + + int SpellNonMeleeDmgLogHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t unk, uint32_t opCode, uint32_t unk2, + CDataStore *packet) { + auto const rpos = packet->m_read; + + uint64_t targetGuid; + packet->GetPackedGuid(targetGuid); + + uint64_t casterGuid; + packet->GetPackedGuid(casterGuid); + + uint32_t spellId; + packet->Get(spellId); + + uint32_t damage; + packet->Get(damage); + + uint8_t school; + packet->Get(school); + + uint32_t absorb; + packet->Get(absorb); + + int32_t resist; + packet->Get(resist); + + uint8_t periodicLog; + packet->Get(periodicLog); + + uint8_t unused; + packet->Get(unused); + + uint32_t blocked; + packet->Get(blocked); + + uint32_t hitInfo; + packet->Get(hitInfo); + + uint8_t extendData; + packet->Get(extendData); + + packet->m_read = rpos; + + TriggerSpellDamageEvent(targetGuid, casterGuid, spellId, damage, school, absorb, blocked, resist, 0, hitInfo); + + auto const spellNonMeleeDmgLogHandler = detour->GetTrampolineT(); + return spellNonMeleeDmgLogHandler(unk, opCode, unk2, packet); + } + + int PlaySpellVisualHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet) { + auto const playSpellVisualHandler = detour->GetTrampolineT(); + + auto const rpos = packet->m_read; + + uint64_t targetGuid; + packet->GetPackedGuid(targetGuid); + + uint32_t spellVisualKitIndex; + packet->Get(spellVisualKitIndex); + + packet->m_read = rpos; + + DEBUG_LOG("Play spell visual id " << spellVisualKitIndex << " for guid " << targetGuid); + +// return playSpellVisualHandler(opCode, packet); + + return 1; + } +} \ No newline at end of file diff --git a/nampower/spellevents.hpp b/nampower/spellevents.hpp new file mode 100644 index 0000000..44fed0c --- /dev/null +++ b/nampower/spellevents.hpp @@ -0,0 +1,38 @@ +// +// Created by pmacc on 9/21/2024. +// + +#pragma once + +#include "main.hpp" + +namespace Nampower { + void SignalEventHook(hadesmem::PatchDetourBase *detour, game::Events eventId); + + int SpellDelayedHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet); + + uint32_t Script_SpellTargetUnitHook(hadesmem::PatchDetourBase *detour, uintptr_t *luaState); + + void Spell_C_SpellFailedHook(hadesmem::PatchDetourBase *detour, uint32_t spellId, + game::SpellCastResult spellResult, int unk1, int unk2, char unk3); + + int CastResultHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet); + + int SpellFailedHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet); + + int SpellStartHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t unk, uint32_t opCode, uint32_t unk2, + CDataStore *packet); + + void Spell_C_CooldownEventTriggeredHook(hadesmem::PatchDetourBase *detour, uint32_t spellId, uint64_t *targetGUID, + int param_3, int clearCooldowns); + + int SpellCooldownHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet); + + int PeriodicAuraLogHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t unk, uint32_t opCode, uint32_t unk2, + CDataStore *packet); + + int SpellNonMeleeDmgLogHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t unk, uint32_t opCode, uint32_t unk2, + CDataStore *packet); + + int PlaySpellVisualHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet); +} \ No newline at end of file diff --git a/nampower/types.h b/nampower/types.h new file mode 100644 index 0000000..c3a6942 --- /dev/null +++ b/nampower/types.h @@ -0,0 +1,135 @@ +// +// Created by pmacc on 9/25/2024. +// + +#pragma once + +#include +#include + +struct UserSettings { + bool queueCastTimeSpells; + bool queueInstantSpells; + bool queueOnSwingSpells; + bool queueChannelingSpells; + bool queueTargetingSpells; + bool queueSpellsOnCooldown; + + bool interruptChannelsOutsideQueueWindow; + bool retryServerRejectedSpells; + bool quickcastTargetingSpells; + bool replaceMatchingNonGcdCategory; + bool optimizeBufferUsingPacketTimings; + + bool preventRightClickTargetChange; + + bool doubleCastToEndChannelEarly; + + uint32_t spellQueueWindowMs; + uint32_t onSwingBufferCooldownMs; + uint32_t channelQueueWindowMs; + uint32_t targetingQueueWindowMs; + uint32_t cooldownQueueWindowMs; + + uint32_t minBufferTimeMs; + uint32_t maxBufferIncreaseMs; + uint32_t nonGcdBufferTimeMs; + + int32_t channelLatencyReductionPercentage; +}; + +enum CastType { + NORMAL, + NON_GCD, + ON_SWING, + CHANNEL, + TARGETING, + TARGETING_NON_GCD +}; + +enum QueueEvents { + ON_SWING_QUEUED, + ON_SWING_QUEUE_POPPED, + NORMAL_QUEUED, + NORMAL_QUEUE_POPPED, + NON_GCD_QUEUED, + NON_GCD_QUEUE_POPPED, + QUEUE_EVENT_COUNT // Keep track of the number of events +}; + +enum CastResult { + WAITING_FOR_CAST, + WAITING_FOR_SERVER, + SERVER_SUCCESS, + SERVER_FAILURE +}; + +struct CastSpellParams { + /* Original cast spell function arguments */ + uint64_t castId; + + uint32_t *playerUnit; + uint32_t spellId; + uintptr_t *item; + uint64_t guid; + /* *********************** */ + + /* Additional data */ + uint32_t gcDCategory; // comes from spell->StartRecoveryCategory + uint32_t castTimeMs; // spell's cast time in ms + uint32_t castStartTimeMs; // event time in ms + CastType castType; + uint32_t numRetries; + CastResult castResult; +}; + +struct LastCastData { + uint32_t attemptTimeMs; // last cast attempt time in ms + uint32_t attemptSpellId; // last cast attempt spell id + + uint32_t castTimeMs; // spell's cast time in ms + + uint32_t startTimeMs; // event time in ms + uint32_t channelStartTimeMs; // event time in ms + uint32_t onSwingStartTimeMs; // event time in ms + + bool wasItem; + bool wasOnGcd; + bool wasQueued; +}; + +struct CastData { + uint32_t delayEndMs; // can't be reset by looking at CastingSpellId + uint32_t castEndMs; + uint32_t gcdEndMs; + uint32_t attemptedCastTimeMs; // this ignoring on swing spells as they are independent + + uint32_t bufferMs; + + bool onSwingQueued; + bool pendingOnSwingCast; + uint32_t onSwingSpellId; + + uint32_t cooldownNormalEndMs; + uint32_t cooldownNonGcdEndMs; + bool cooldownNormalSpellQueued; + bool cooldownNonGcdSpellQueued; + + bool normalSpellQueued; + bool nonGcdSpellQueued; + + bool castingQueuedSpell; + uint32_t numRetries; + + bool cancellingSpell; + + bool channeling; + bool cancelChannelNextTick; + uint32_t channelStartMs; + uint32_t channelEndMs; + uint32_t channelTickTimeMs; + uint32_t channelNumTicks; + uint32_t channelSpellId; + uint32_t channelDuration; +}; +