This commit is contained in:
avitasia
2025-09-03 12:56:59 -07:00
commit dc38b57122
35 changed files with 6920 additions and 0 deletions
+24
View File
@@ -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.
+10
View File
@@ -0,0 +1,10 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---
+20
View File
@@ -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.
+56
View File
@@ -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
+12
View File
@@ -0,0 +1,12 @@
/.vs/*
/*.sdf
/loader/*.aps
/CMakeSettings.json
/out
/.idea/
/.vscode/
/build/
/loader/RCa22240
/cmake-build*/
/nampower/cmake-build*/
.claude/
+105
View File
@@ -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}")
+26
View File
@@ -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.
+490
View File
@@ -0,0 +1,490 @@
<b> Checkout the list button above this to easily navigate the readme </b>
# 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. <b>If you have superwow and latest pfui issue is fixed</b>.
<b>Notgrid</b> mouseover needs to be updated to take advantage of superwow the default version won't work well with queuing.
<b>If you use healcomm can replace all instances of it in your addons with this version to work well with queuing </b> 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.
<b>You will need launch the game with a launcher like Vanillafixes https://github.com/hannesmann/vanillafixes or Unitxp https://github.com/allfoxwy/UnitXP_SP3</b> 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"))`<br>
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 `<Realm>` block to tell the launcher to include this tool:
```xml
<DLL Path="c:\path\to\nampower.dll" Method="Load" />
```
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)
+13
View File
@@ -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}")
Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
View File
Binary file not shown.
+106
View File
@@ -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 <iostream>
#include <vector>
#include <string>
#include <boost/filesystem.hpp>
#include <boost/exception/diagnostic_information.hpp>
#include <boost/program_options.hpp>
#include <hadesmem/process_list.hpp>
#include <hadesmem/injector.hpp>
#include <hadesmem/process.hpp>
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<std::wstring>(&dll)->default_value(L"nampower.dll", "nampower.dll"), "dll to inject into program")
#ifdef _DEBUG
("export,e", boost::program_options::value<std::string>(&exportFunc)->default_value("Load"), "export function to call upon injection")
#endif
("program,p", boost::program_options::wvalue<std::wstring>(&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<std::wstring> 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;
}
BIN
View File
Binary file not shown.
+32
View File
@@ -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}")
+164
View File
@@ -0,0 +1,164 @@
#pragma once
#include "types.h"
#include "logging.hpp"
#include <vector>
namespace Nampower {
class CastQueue {
private:
int maxSize;
std::vector<CastSpellParams> 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 &params) {
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 &params, 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;
}
};
}
+248
View File
@@ -0,0 +1,248 @@
#include "cdatastore.hpp"
#include <memory.h>
#include <cstring>
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<uint64_t>(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;
}
}
}
}
+211
View File
@@ -0,0 +1,211 @@
//
// Created by pmacc on 9/27/2024.
//
#pragma once
#include <cassert>
#include <cstdlib>
#include <cstdint>
#include <logging.hpp>
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<typename T>
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<typename T>
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<typename T>
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<typename T>
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<typename T>
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<typename T>
CDataStore &operator<<(T val) {
return Put(val);
}
template<typename T>
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 *);
};
}
+155
View File
@@ -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 <hadesmem/detail/alias_cast.hpp>
#include <cstdint>
namespace game {
uintptr_t *GetObjectPtr(std::uint64_t guid) {
uintptr_t *(__stdcall *getObjectPtr)(std::uint64_t) = hadesmem::detail::AliasCast<decltype(getObjectPtr)>(
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<ClntObjMgrObjectPtrT>(Offsets::ClntObjMgrObjectPtr);
return clntObjMgrObjectPtr(typeMask, nullptr, guid, 0);
}
std::uint32_t GetCastTime(void *unit, uint32_t spellId) {
auto const vmt = *reinterpret_cast<std::uint8_t **>(unit);
int
(__thiscall *getSpellCastingTime)(void *, uint32_t) = *reinterpret_cast<decltype(&getSpellCastingTime)>(vmt +
4 *
static_cast<std::uint32_t>(Offsets::GetCastingTimeIndex));
return getSpellCastingTime(unit, spellId);
}
CDuration *GetDurationObject(uint32_t durationIndex) {
auto const durationListPtr = *reinterpret_cast<std::uint32_t *>(Offsets::GetDurationObject);
if (durationListPtr) {
auto const durationObjectPtr = *reinterpret_cast<std::uint32_t *>(durationListPtr + durationIndex*4);
if (durationObjectPtr) {
return reinterpret_cast<CDuration *>(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<Spell_C_GetDurationT>(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<Spell_C_GetSpellModifiersT>(Offsets::Spell_C_GetSpellModifiers);
auto modificationPercentage = 0;
getModifiers(spellRec, &modificationPercentage, spellMod);
return modificationPercentage;
}
const SpellRec *GetSpellInfo(uint32_t spellId) {
auto const spellDb = reinterpret_cast<WowClientDB<SpellRec> *>(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<uint32_t *>(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<std::uint32_t *>(Offsets::Language);
return spell->SpellName[language];
}
std::uint64_t ClntObjMgrGetActivePlayerGuid() {
auto const getActivePlayer = hadesmem::detail::AliasCast<decltype(&ClntObjMgrGetActivePlayerGuid)>(
Offsets::GetActivePlayer);
return getActivePlayer();
}
std::uint64_t GetCurrentTargetGuid() {
return *reinterpret_cast<uint64_t *>(Offsets::LockedTargetGuid);
}
uint64_t UnitGetGuid(uintptr_t *unit) {
if (!unit) {
return 0;
}
uint64_t guid = *reinterpret_cast<uint64_t *>(unit + 12);
return guid;
}
uint64_t UnitGetTargetGuid(uintptr_t *unit) {
if (!unit) {
return 0;
}
auto *unitFields = *reinterpret_cast<UnitFields **>(unit + 68);
if (unitFields == nullptr) {
return 0;
}
return unitFields->target;
}
}
+1296
View File
File diff suppressed because it is too large Load Diff
+125
View File
@@ -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<Spell_C_GetSpellCooldownT>(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<Spell_C_GetSpellCooldownT>(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<Spell_C_GetSpellCooldownT>(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<Spell_C_GetSpellCooldownT>(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<unsigned long long>(guid));
return guidStr;
}
float GetNameplateDistance() {
auto const distanceSquared = *reinterpret_cast<float *>(Offsets::NameplateDistance);
return sqrtf(distanceSquared);
}
void SetNameplateDistance(float distance) {
*reinterpret_cast<float *>(Offsets::NameplateDistance) = distance * distance;
}
}
+34
View File
@@ -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);
}
+7
View File
@@ -0,0 +1,7 @@
#include <logging.hpp>
namespace Nampower {
std::ofstream debugLogFile;
uint32_t gStartTime;
}
+33
View File
@@ -0,0 +1,33 @@
//
// Created by pmacc on 9/29/2024.
//
#pragma once
#include <fstream>
#include <chrono>
#include <string>
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
}
+1124
View File
File diff suppressed because it is too large Load Diff
+172
View File
@@ -0,0 +1,172 @@
//
// Created by pmacc on 9/21/2024.
//
#pragma once
#include <hadesmem/process.hpp>
#include <hadesmem/patcher.hpp>
#include <Windows.h>
#include <cstdint>
#include <memory>
#include <atomic>
#include <chrono>
#include <thread>
#include <iostream>
#include <fstream>
#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();
}
+177
View File
@@ -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 <cstdint>
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,
};
+443
View File
@@ -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<lua_errorT>(Offsets::lua_error);
auto const lua_isstring = reinterpret_cast<lua_isstringT>(Offsets::lua_isstring);
auto const lua_isnumber = reinterpret_cast<lua_isnumberT>(Offsets::lua_isnumber);
auto const lua_tostring = reinterpret_cast<lua_tostringT>(Offsets::lua_tostring);
auto const lua_tonumber = reinterpret_cast<lua_tonumberT>(Offsets::lua_tonumber);
auto const lua_pushnumber = reinterpret_cast<lua_pushnumberT>(Offsets::lua_pushnumber);
auto const lua_pushstring = reinterpret_cast<lua_pushstringT>(Offsets::lua_pushstring);
bool gScriptQueued;
int gScriptPriority = 1;
char *queuedScript;
uint32_t GetSpellSlotAndTypeForName(const char *spellName, uint32_t *spellType) {
auto const getSpellSlotAndType = reinterpret_cast<GetSpellSlotAndTypeT>(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 *>(uint32_t(Offsets::CGSpellBook_mKnownSpells) +
spellSlot * 4);
} else {
spellId = *reinterpret_cast<uint32_t *>(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<LuaScriptT>(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<LuaScriptT>(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<LuaScriptT>();
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<uint32_t> 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<GetGUIDFromNameT>(Offsets::GetGUIDFromName);
targetGUID = getGUIDFromName(target);
}
auto playerUnit = game::GetObjectPtr(game::ClntObjMgrGetActivePlayerGuid());
auto const RangeCheckSelected = reinterpret_cast<RangeCheckSelectedT>(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<Spell_C_IsSpellUsableT>(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<uint32_t *>(Offsets::CastingSpellId);
lua_pushnumber(luaState, *castingSpellId);
auto const isCasting = gCastData.castEndMs > GetTime();
auto const isChanneling = gCastData.channeling;
auto const visualSpellId = reinterpret_cast<uint32_t *>(Offsets::VisualSpellId);
lua_pushnumber(luaState, *visualSpellId);
auto const autoRepeatingSpellId = reinterpret_cast<uint32_t *>(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<uint32_t *>(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<std::uint32_t *>(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 *>(uint32_t(Offsets::CGSpellBook_mKnownSpells) +
spellSlot * 4);
} else {
spellId = *reinterpret_cast<uint32_t *>(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<uint32_t>(lua_tonumber(luaState, 1));
// Pointer to ItemDBCache
void *itemDbCache = reinterpret_cast<void *>(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<uintptr_t *(__thiscall *)(void *, uint32_t, int **, int *, int *, char)>(
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<uint32_t *>(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<LuaScriptT >(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;
}
}
+38
View File
@@ -0,0 +1,38 @@
//
// Created by pmacc on 1/8/2025.
//
#pragma once
#include <Windows.h>
#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);
}
+744
View File
@@ -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<unsigned long long>(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<uint32_t *>(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<ClientServices_SendT>(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<CastSpellT>();
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<CastSpellT>();
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<CastSpellT>();
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<int *>(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<CancelSpellT>(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<int *>(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<SpellGoT>();
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<uint32_t *>(Offsets::CGInputControlGetActive);
typedef void(__thiscall *SetReleaseActionT)(uint32_t, uint32_t);
auto SetReleaseAction = reinterpret_cast<SetReleaseActionT>(Offsets::CGInputControlSetReleaseAction);
SetReleaseAction(activeControl, input);
}
void SetControlBit(uint32_t input) {
uint32_t activeControl = *reinterpret_cast<uint32_t *>(Offsets::CGInputControlGetActive);
auto *LastHardwareAction = reinterpret_cast<uintptr_t *>(Offsets::LastHardwareAction);
typedef void(__thiscall *SetControlBitT)(uint32_t, uint32_t, uint32_t, uintptr_t *, int);
auto SetControlBit = reinterpret_cast<SetControlBitT>(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<Spell_C_TargetSpellT>();
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<CGGameUI_TargetT>(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<CancelSpellT>();
return cancelSpell(failed, notifyServer, reason);
}
void SendCastHook(hadesmem::PatchDetourBase *detour, game::SpellCast *cast, char unk) {
auto const sendCast = detour->GetTrampolineT<SendCastT>();
sendCast(cast, unk);
auto const spell = game::GetSpellInfo(cast->spellId);
BeginCast(gCastData.attemptedCastTimeMs, spell, cast);
}
}
+36
View File
@@ -0,0 +1,36 @@
//
// Created by pmacc on 9/21/2024.
//
#pragma once
#include "game.hpp"
#include <Windows.h>
#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);
}
+125
View File
@@ -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<PacketHandlerT>();
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<PacketHandlerT>();
return spellChannelUpdateHandler(opCode, packet);
}
}
+13
View File
@@ -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);
}
+708
View File
@@ -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<SignalEventT>();
signalEvent(eventId);
}
uint32_t Script_SpellTargetUnitHook(hadesmem::PatchDetourBase *detour, uintptr_t *luaState) {
auto const spellTargetUnit = detour->GetTrampolineT<LuaScriptT>();
// check if valid string
auto const lua_isstring = reinterpret_cast<lua_isstringT>(Offsets::lua_isstring);
if (lua_isstring(luaState, 1)) {
auto const lua_tostring = reinterpret_cast<lua_tostringT>(Offsets::lua_tostring);
auto const unitName = lua_tostring(luaState, 1);
auto const getGUIDFromName = reinterpret_cast<GetGUIDFromNameT>(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<Spell_C_SpellFailedT>();
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<PacketHandlerT>();
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<Spell_C_CooldownEventTriggeredT>();
cooldownEventTriggered(spellId, targetGUID, param_3, clearCooldowns);
}
int SpellDelayedHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet) {
auto const spellDelayed = detour->GetTrampolineT<PacketHandlerT>();
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<PacketHandlerT>();
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<PacketHandlerT>();
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<FastCallPacketHandlerT>();
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<uint32_t *>(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<uint32_t *>(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<FastCallPacketHandlerT>();
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<FastCallPacketHandlerT>();
return spellNonMeleeDmgLogHandler(unk, opCode, unk2, packet);
}
int PlaySpellVisualHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet) {
auto const playSpellVisualHandler = detour->GetTrampolineT<PacketHandlerT>();
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;
}
}
+38
View File
@@ -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);
}
+135
View File
@@ -0,0 +1,135 @@
//
// Created by pmacc on 9/25/2024.
//
#pragma once
#include <minwindef.h>
#include <cstdint>
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;
};