mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 07:00:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84da9c23df | ||
|
|
6e58cb81d1 | ||
|
|
8a065481b4 | ||
|
|
2a7cbf75ca | ||
|
|
ff5ce5cdcb | ||
|
|
47b6a66b96 | ||
|
|
e78c36bb70 | ||
|
|
41fb17502d | ||
|
|
a989499cae | ||
|
|
722c257acb | ||
|
|
a6c7098106 | ||
|
|
7caee620e8 | ||
|
|
954533a170 | ||
|
|
dbe5ab40ee | ||
|
|
f3b328247c | ||
|
|
470361fbce | ||
|
|
2d266a9b76 | ||
|
|
8e3e207f83 |
+49
-15
@@ -21,8 +21,44 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
# Decide which runner the build lands on. `runs-on` is fixed before a job
|
||||
# starts and a job pointed at an offline self-hosted runner queues forever
|
||||
# instead of failing over — so we probe first and hand the label down.
|
||||
pick-runner:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
label: ${{ steps.pick.outputs.label }}
|
||||
steps:
|
||||
- name: Probe for an online self-hosted runner
|
||||
id: pick
|
||||
env:
|
||||
# Listing runners needs repo-admin access; the default GITHUB_TOKEN
|
||||
# does NOT have it. Create a PAT (classic: `repo`; fine-grained:
|
||||
# Administration → read) and store it as the RUNNER_CHECK_TOKEN
|
||||
# secret. If the secret is missing, we fall back to windows-latest.
|
||||
GH_TOKEN: ${{ secrets.RUNNER_CHECK_TOKEN }}
|
||||
run: |
|
||||
label=windows-latest
|
||||
if [ -n "$GH_TOKEN" ]; then
|
||||
online=$(gh api "repos/${{ github.repository }}/actions/runners" \
|
||||
--jq '[.runners[] | select(.status=="online") | select([.labels[].name] | index("self-hosted"))] | length' \
|
||||
2>/dev/null || echo 0)
|
||||
if [ "${online:-0}" -gt 0 ]; then
|
||||
label=self-hosted
|
||||
fi
|
||||
fi
|
||||
echo "Selected runner: $label"
|
||||
echo "label=$label" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
needs: pick-runner
|
||||
runs-on: ${{ needs.pick-runner.outputs.label }}
|
||||
# Windows PowerShell 5.1 is always present; the runner's default `pwsh`
|
||||
# (PowerShell Core) is not installed by default, and `shell: bash` on a
|
||||
# self-hosted box resolves to the WSL launcher in System32.
|
||||
defaults:
|
||||
run:
|
||||
shell: powershell
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -36,21 +72,19 @@ jobs:
|
||||
# — no source edit.
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.tag }}" ]; then
|
||||
tag="${{ inputs.tag }}"
|
||||
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
tag="${{ github.ref_name }}"
|
||||
else
|
||||
tag=""
|
||||
fi
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
if [ -n "$tag" ]; then
|
||||
echo "is_release=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_release=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
if ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch' -and $env:INPUT_TAG) {
|
||||
$tag = $env:INPUT_TAG
|
||||
} elseif ($env:GITHUB_REF -like 'refs/tags/v*') {
|
||||
$tag = $env:GITHUB_REF_NAME
|
||||
} else {
|
||||
$tag = ''
|
||||
}
|
||||
$isRelease = if ($tag) { 'true' } else { 'false' }
|
||||
Add-Content -Path $env:GITHUB_OUTPUT -Value "tag=$tag" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_OUTPUT -Value "is_release=$isRelease" -Encoding utf8
|
||||
|
||||
- name: Configure (release)
|
||||
if: steps.ver.outputs.is_release == 'true'
|
||||
|
||||
+25
@@ -13715,6 +13715,31 @@ if d then
|
||||
end
|
||||
```
|
||||
|
||||
**Filter tokens.** `filter` uses the modern AuraFilters format: tokens
|
||||
separated by `|` or spaces, each token optionally negated with a
|
||||
leading `!`. This build honors:
|
||||
|
||||
- `HELPFUL` / `HARMFUL` — buffs / debuffs. With neither token the
|
||||
query returns both (the indexed getter defaults to helpful).
|
||||
- `PLAYER` / `!PLAYER` — only auras the local player cast, or only
|
||||
auras the player did not cast. Caster data comes from casts this
|
||||
session, so an aura present before you saw it cast has no caster and
|
||||
counts as not-player.
|
||||
- `DISPELLABLE` / `!DISPELLABLE` — only auras that can be dispelled,
|
||||
purged, or stolen (dispel type Magic, Curse, Disease, or Poison), or
|
||||
only auras that cannot. This is "can it be removed at all", not "can
|
||||
you remove it".
|
||||
- `CROWD_CONTROL` / `!CROWD_CONTROL` — only auras that are a crowd-
|
||||
control effect (stun, fear, silence, root, charm, confuse, disarm, and
|
||||
movement slows), or only auras that are not. Shares the loss-of-control
|
||||
classifier with `C_LossOfControl`, plus snares (which are crowd
|
||||
control but not loss of control).
|
||||
|
||||
All other AuraFilters tokens (`RAID`, `CANCELABLE`,
|
||||
`INCLUDE_NAME_PLATE_ONLY`, `MAW`, and the rest) are accepted and
|
||||
ignored — vanilla has no data for them. Token matching is whole-token,
|
||||
so `RAID_PLAYER_DISPELLABLE` is not read as `PLAYER`.
|
||||
|
||||
### `C_UnitAuras.GetBuffDataByIndex(unit, index)` / `GetDebuffDataByIndex(unit, index)`
|
||||
|
||||
Convenience wrappers locking the filter to `HELPFUL` or `HARMFUL`
|
||||
|
||||
+53
-4
@@ -202,6 +202,19 @@ enum Offsets {
|
||||
// GO field at +0x370.
|
||||
FUN_GAMETOOLTIP_BUILD_UNIT = 0x00529FE0,
|
||||
FUN_GAMETOOLTIP_BUILD_GAMEOBJECT = 0x0052AA20,
|
||||
// Pointer to the GameTooltip CFrameScriptObject the engine's mouseover
|
||||
// setter builds into (`DAT_00b4b3c4`; the value at this address is the
|
||||
// tooltip object). `Frame::Attributes` reads it to build the unit tooltip
|
||||
// for offline/out-of-range party & raid members, which FUN_00492890 skips.
|
||||
VAR_GAMETOOLTIP_OBJECT_PTR = 0x00B4B3C4,
|
||||
// The tooltip's OnTooltipSetDefaultAnchor handler slot (the `{handler,
|
||||
// context}` pair the script resolver maps that name to). The engine
|
||||
// mouseover setter fires this before building to (re)establish the
|
||||
// tooltip owner/anchor via FrameXML's GameTooltip_SetDefaultAnchor —
|
||||
// without it a rebuild after the tooltip was hidden has no owner and
|
||||
// stays invisible. `Frame::Attributes` fires it (via the self-contained
|
||||
// FUN_FRAME_INVOKE_SCRIPT) before the offline roster build.
|
||||
OFF_TOOLTIP_SET_DEFAULT_ANCHOR_HANDLER = 0x444,
|
||||
FUN_GAMETOOLTIP_ADD_LINE = 0x00530270, // __thiscall(self, left, right, lColorBGRA*, rColorBGRA*, wrap)
|
||||
OFF_GAMETOOLTIP_NUM_LINES = 0x31C, // int — live line count (AddLine index; +0x320 is the cap)
|
||||
// The displayed item's identity is read via the existing
|
||||
@@ -5746,14 +5759,46 @@ enum Offsets {
|
||||
// VAR_SPELLMOD_*). Verified from FUN_006e6b30: SpellFamilyName at
|
||||
// +0x280 (gates whether the player's class mods apply), 64-bit
|
||||
// SpellFamilyFlags at +0x284 (selects which familyFlagBit rows to
|
||||
// sum). AttributesEx2 (+0x24) bit 0x20000000 disables spell mods.
|
||||
// sum). AttributesEx3 (+0x24) bit 0x20000000 (IGNORE_CASTER_MODIFIERS)
|
||||
// disables spell mods — the client reads [record + 0x24] in FUN_006e6b30.
|
||||
OFF_SPELL_RECORD_FAMILY_NAME = 0x280, // u32
|
||||
OFF_SPELL_RECORD_FAMILY_FLAGS = 0x284, // u64
|
||||
OFF_SPELL_RECORD_ICON_ID = 0x1D4, // u32 SpellIconID (→ SpellIcon.dbc)
|
||||
OFF_SPELL_RECORD_ATTRIBUTES = 0x18, // u32 (base Attributes)
|
||||
OFF_SPELL_RECORD_ATTRIBUTES = 0x18, // u32 (column 6, base Attributes)
|
||||
SPELL_ATTR_PASSIVE = 0x40, // bit 6 — always-on aura
|
||||
OFF_SPELL_RECORD_ATTRIBUTES_EX2 = 0x24, // u32
|
||||
SPELL_ATTR_EX2_NO_SPELL_MODS = 0x20000000,
|
||||
OFF_SPELL_RECORD_ATTRIBUTES_EX = 0x1C, // u32 (column 7)
|
||||
OFF_SPELL_RECORD_ATTRIBUTES_EX2 = 0x20, // u32 (column 8)
|
||||
OFF_SPELL_RECORD_ATTRIBUTES_EX3 = 0x24, // u32 (column 9)
|
||||
// The client's spell-mod core (FUN_006e6b30) skips all mods when this bit
|
||||
// is set: it reads [record + 0x24] & 0x20000000 — AttributesEx3 bit 29,
|
||||
// SPELL_ATTR_EX3_IGNORE_CASTER_MODIFIERS (verified via disassembly; the
|
||||
// server enum agrees). NOT AttributesEx2's bit 29 (that's CANT_CRIT).
|
||||
SPELL_ATTR_EX3_IGNORE_CASTER_MODIFIERS = 0x20000000,
|
||||
// AttributesEx channel bits (CHANNELED_1 0x4 | CHANNELED_2 0x40) and the
|
||||
// AttributesEx2 autorepeat flag (bit 5, Auto Shot / Shoot) — each shared
|
||||
// by more than one module, so kept here rather than redefined locally.
|
||||
SPELL_ATTR_EX_CHANNELED = 0x4 | 0x40,
|
||||
SPELL_ATTR_EX2_AUTOREPEAT_FLAG = 0x20,
|
||||
OFF_SPELL_RECORD_INTERRUPT_FLAGS = 0x54, // u32 (column 21)
|
||||
|
||||
// Remaining Spell.dbc record fields — the single source of truth for the
|
||||
// record layout (byte = column * 4). Modules must use these rather than
|
||||
// redefining local copies.
|
||||
OFF_SPELL_RECORD_CASTING_TIME_INDEX = 0x48, // u32 → SpellCastTimes.dbc
|
||||
OFF_SPELL_RECORD_MAX_LEVEL = 0x6C, // u32 (scaling cap level)
|
||||
OFF_SPELL_RECORD_SPELL_LEVEL = 0x74, // u32 (this rank's effective level)
|
||||
OFF_SPELL_RECORD_POWER_TYPE = 0x7C, // i32 (0=mana,1=rage,2=focus,3=energy,4=happiness)
|
||||
OFF_SPELL_RECORD_MANA_COST = 0x80, // u32 base cost
|
||||
OFF_SPELL_RECORD_RANGE_INDEX = 0x90, // u32 → SpellRange.dbc
|
||||
OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A = 0x148,// i32[3] implicit target A
|
||||
OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_B = 0x154,// i32[3] implicit target B
|
||||
OFF_SPELL_RECORD_MANA_COST_PERCENT = 0x270, // i32 % of base resource (0 = flat)
|
||||
OFF_SPELL_RECORD_RANK = 0x204, // char*[9] localized rank text
|
||||
OFF_SPELL_RECORD_TOTEM = 0xA0, // i32[2] required totem/tool item IDs
|
||||
|
||||
// SpellIcon.dbc record's path string field (char* at +0x04). Distinct
|
||||
// struct from Spell.dbc — reached via SpellIcon.dbc[Spell.SpellIconID].
|
||||
OFF_SPELLICON_PATH = 0x04,
|
||||
|
||||
// Spell.dbc Mechanic field — the spell-level SpellMechanic ID (→
|
||||
// SpellMechanic.dbc), 0 = none. Field 5 of the record
|
||||
@@ -5775,6 +5820,10 @@ enum Offsets {
|
||||
SPELL_AURA_MOD_CHARM = 6,
|
||||
SPELL_AURA_MOD_FEAR = 7,
|
||||
SPELL_AURA_MOD_STUN = 12,
|
||||
// Movement slow / snare. Not a loss-of-control (C_LossOfControl omits it),
|
||||
// but the C_UnitAuras CROWD_CONTROL filter counts it — see
|
||||
// Spell::CrowdControl::IsCrowdControl.
|
||||
SPELL_AURA_MOD_DECREASE_SPEED = 33,
|
||||
SPELL_AURA_MOD_PACIFY = 25,
|
||||
SPELL_AURA_MOD_ROOT = 26,
|
||||
SPELL_AURA_MOD_SILENCE = 27,
|
||||
|
||||
+108
-61
@@ -17,12 +17,20 @@
|
||||
// nameplate flags, etc.) are populated with vanilla-truthful
|
||||
// defaults so consumers reading those keys get sensible values.
|
||||
//
|
||||
// Filter parsing here mirrors what most modern addons actually pass:
|
||||
// "HELPFUL" / "HARMFUL" are honored, every other modern filter
|
||||
// (`PLAYER` / `RAID` / `CANCELABLE` / `INCLUDE_NAME_PLATE_ONLY`) is
|
||||
// accepted but no-ops — they'd require either source-GUID tracking
|
||||
// (`PLAYER`), class-dispel-matrix infra (`RAID`), or systems
|
||||
// vanilla doesn't have at all.
|
||||
// Filter parsing (`ParseFilters`) tokenizes the modern AuraFilters string:
|
||||
// tokens separated by `|` and/or whitespace, each optionally negated with a
|
||||
// leading `!`. Whole-token matching, so `RAID_PLAYER_DISPELLABLE` is not
|
||||
// mistaken for `PLAYER` and `!PLAYER` is a negation rather than a match.
|
||||
// Honored: `HELPFUL` / `HARMFUL` (descriptor slot ranges), `PLAYER` /
|
||||
// `!PLAYER` (caster == / != the local player, from the Aura::Source cache),
|
||||
// `DISPELLABLE` / `!DISPELLABLE` (dispel type is / isn't one a
|
||||
// dispel/purge/steal can remove — Spell.dbc Dispel ∈ Magic/Curse/Disease/
|
||||
// Poison, matching the server's DISPEL_ALL_MASK), and `CROWD_CONTROL` /
|
||||
// `!CROWD_CONTROL` (is / isn't a hard control effect, via the shared
|
||||
// `Spell::CrowdControl` classifier that also backs C_LossOfControl). Every
|
||||
// other modern token (`RAID`, `CANCELABLE`, `INCLUDE_NAME_PLATE_ONLY`, `MAW`,
|
||||
// …) is accepted and ignored — they need a class-dispel matrix or systems
|
||||
// vanilla has no data for.
|
||||
|
||||
#include "Data.h"
|
||||
|
||||
@@ -60,24 +68,72 @@ uint64_t GuidForOutOfRange(const char *token) {
|
||||
return Unit::Identity::GuidForToken(token);
|
||||
}
|
||||
|
||||
// Parses the filter string. Returns Helpful by default; Harmful if
|
||||
// the string contains the substring "HARMFUL". Case-sensitive — the
|
||||
// modern API documents the filter tokens as upper-case constants
|
||||
// (`"HELPFUL"` etc.) and addons that pass them through case-changing
|
||||
// transforms are already broken on modern too.
|
||||
Data::Filter ParseFilter(const char *filter) {
|
||||
if (filter == nullptr)
|
||||
return Data::Filter::Helpful;
|
||||
if (strstr(filter, "HARMFUL") != nullptr)
|
||||
return Data::Filter::Harmful;
|
||||
return Data::Filter::Helpful;
|
||||
// Parsed AuraFilters string. `helpful`/`harmful` record which range tokens
|
||||
// were present (neither → both, matching modern's default); `caster` carries
|
||||
// the PLAYER / !PLAYER restriction.
|
||||
struct ParsedFilter {
|
||||
bool helpful = false;
|
||||
bool harmful = false;
|
||||
Data::CasterMode caster = Data::CasterMode::Any;
|
||||
Data::DispelMode dispel = Data::DispelMode::Any;
|
||||
Data::CcMode cc = Data::CcMode::Any;
|
||||
|
||||
Data::Match ToMatch() const { return Data::Match{caster, dispel, cc}; }
|
||||
};
|
||||
|
||||
// Reduces the parsed range tokens to the single `Data::Filter` the indexed /
|
||||
// by-id / by-name getters take: Harmful when HARMFUL was given, else Helpful
|
||||
// (the modern default). HELPFUL wins ties are irrelevant here — an indexed
|
||||
// getter reads one range.
|
||||
Data::Filter RangeFilter(const ParsedFilter &pf) {
|
||||
return pf.harmful && !pf.helpful ? Data::Filter::Harmful
|
||||
: Data::Filter::Helpful;
|
||||
}
|
||||
|
||||
// The `PLAYER` filter token restricts results to auras the local player
|
||||
// cast. No other modern token contains the substring "PLAYER", so a plain
|
||||
// substring test is safe.
|
||||
bool HasPlayerFilter(const char *filter) {
|
||||
return filter != nullptr && strstr(filter, "PLAYER") != nullptr;
|
||||
// Tokenizes the filter string. Case-sensitive (modern documents the tokens as
|
||||
// upper-case constants). `HELPFUL`/`HARMFUL` are positive range selectors;
|
||||
// negating them is meaningless for our slot-based ranges, so a leading `!` is
|
||||
// ignored on those. `PLAYER` honors negation (`!PLAYER` → NotPlayer). Any
|
||||
// unrecognized token is accepted and skipped.
|
||||
ParsedFilter ParseFilters(const char *filter) {
|
||||
ParsedFilter out;
|
||||
if (filter == nullptr)
|
||||
return out;
|
||||
for (const char *p = filter; *p != '\0';) {
|
||||
while (*p == '|' || *p == ' ' || *p == '\t')
|
||||
++p;
|
||||
if (*p == '\0')
|
||||
break;
|
||||
bool negate = false;
|
||||
if (*p == '!') {
|
||||
negate = true;
|
||||
++p;
|
||||
}
|
||||
char tok[64];
|
||||
size_t n = 0;
|
||||
while (*p != '\0' && *p != '|' && *p != ' ' && *p != '\t') {
|
||||
if (n + 1 < sizeof(tok))
|
||||
tok[n++] = *p;
|
||||
++p;
|
||||
}
|
||||
tok[n] = '\0';
|
||||
|
||||
if (strcmp(tok, "HELPFUL") == 0)
|
||||
out.helpful = true;
|
||||
else if (strcmp(tok, "HARMFUL") == 0)
|
||||
out.harmful = true;
|
||||
else if (strcmp(tok, "PLAYER") == 0)
|
||||
out.caster = negate ? Data::CasterMode::NotPlayer
|
||||
: Data::CasterMode::PlayerOnly;
|
||||
else if (strcmp(tok, "DISPELLABLE") == 0)
|
||||
out.dispel = negate ? Data::DispelMode::NotDispellable
|
||||
: Data::DispelMode::DispellableOnly;
|
||||
else if (strcmp(tok, "CROWD_CONTROL") == 0)
|
||||
out.cc = negate ? Data::CcMode::NotCrowdControl
|
||||
: Data::CcMode::CrowdControlOnly;
|
||||
// else: accepted and ignored (retail-only / unimplemented token).
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const char *ArgUnit(void *L, int idx) {
|
||||
@@ -102,10 +158,10 @@ const char *ArgOptString(void *L, int idx) {
|
||||
// or nil if no such aura. Used by `GetAuraDataByIndex` and the
|
||||
// filter-locked aliases.
|
||||
int PushAuraByIndex(void *L, const char *unitToken, int index,
|
||||
Data::Filter filter, bool playerOnly = false) {
|
||||
Data::Filter filter, Data::Match match = {}) {
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
if (unit != nullptr) {
|
||||
const int slot = Data::FindNthSlot(unit, index, filter, playerOnly);
|
||||
const int slot = Data::FindNthSlot(unit, index, filter, match);
|
||||
if (slot >= 0) {
|
||||
Data::Push(L, unit, slot);
|
||||
return 1;
|
||||
@@ -113,14 +169,14 @@ int PushAuraByIndex(void *L, const char *unitToken, int index,
|
||||
// Descriptor exhausted — an aura the engine dropped from the slot array
|
||||
// (rogue stealth, nearby party range fluctuation) may still be live in
|
||||
// the Aura::Source cache. Surface it after the descriptor entries.
|
||||
if (Data::PushNthCacheFallback(L, unit, index, filter, playerOnly))
|
||||
if (Data::PushNthCacheFallback(L, unit, index, filter, match))
|
||||
return 1;
|
||||
} else {
|
||||
// No live CGUnit — an out-of-range / cross-map groupmate. The server
|
||||
// still sends their aura spell IDs via SMSG_PARTY_MEMBER_STATS; read
|
||||
// them the same way the built-in UnitBuff/UnitDebuff do.
|
||||
if (Data::PushNthGroupAura(L, GuidForOutOfRange(unitToken), index,
|
||||
filter, playerOnly))
|
||||
filter, match))
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushNil(L);
|
||||
@@ -135,8 +191,8 @@ int __fastcall Script_GetAuraDataByIndex(void *L) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
return PushAuraByIndex(L, unit, index, ParseFilter(filterStr),
|
||||
HasPlayerFilter(filterStr));
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
return PushAuraByIndex(L, unit, index, RangeFilter(pf), pf.ToMatch());
|
||||
}
|
||||
|
||||
int __fastcall Script_GetBuffDataByIndex(void *L) {
|
||||
@@ -168,23 +224,19 @@ int __fastcall Script_GetUnitAuraBySpellID(void *L) {
|
||||
return 1;
|
||||
}
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
Data::Filter f;
|
||||
const Data::Filter *fp = nullptr;
|
||||
if (filterStr != nullptr) {
|
||||
f = ParseFilter(filterStr);
|
||||
fp = &f;
|
||||
}
|
||||
const bool playerOnly = HasPlayerFilter(filterStr);
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
Data::Filter f = RangeFilter(pf);
|
||||
const Data::Filter *fp = (pf.helpful || pf.harmful) ? &f : nullptr;
|
||||
if (unit != nullptr) {
|
||||
const int slot = Data::FindSlotBySpellID(
|
||||
unit, static_cast<uint32_t>(spellID), fp, playerOnly);
|
||||
unit, static_cast<uint32_t>(spellID), fp, pf.ToMatch());
|
||||
if (slot >= 0) {
|
||||
Data::Push(L, unit, slot);
|
||||
return 1;
|
||||
}
|
||||
} else if (Data::PushGroupAuraBySpellID(L, GuidForOutOfRange(unitToken),
|
||||
static_cast<uint32_t>(spellID), fp,
|
||||
playerOnly)) {
|
||||
pf.ToMatch())) {
|
||||
// Out-of-range groupmate — read from the group-member aura array.
|
||||
return 1;
|
||||
}
|
||||
@@ -201,21 +253,17 @@ int __fastcall Script_GetAuraDataBySpellName(void *L) {
|
||||
return 1;
|
||||
}
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
Data::Filter f;
|
||||
const Data::Filter *fp = nullptr;
|
||||
if (filterStr != nullptr) {
|
||||
f = ParseFilter(filterStr);
|
||||
fp = &f;
|
||||
}
|
||||
const bool playerOnly = HasPlayerFilter(filterStr);
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
Data::Filter f = RangeFilter(pf);
|
||||
const Data::Filter *fp = (pf.helpful || pf.harmful) ? &f : nullptr;
|
||||
if (unit != nullptr) {
|
||||
const int slot = Data::FindSlotBySpellName(unit, spellName, fp, playerOnly);
|
||||
const int slot = Data::FindSlotBySpellName(unit, spellName, fp, pf.ToMatch());
|
||||
if (slot >= 0) {
|
||||
Data::Push(L, unit, slot);
|
||||
return 1;
|
||||
}
|
||||
} else if (Data::PushGroupAuraBySpellName(L, GuidForOutOfRange(unitToken),
|
||||
spellName, fp, playerOnly)) {
|
||||
spellName, fp, pf.ToMatch())) {
|
||||
// Out-of-range groupmate — read from the group-member aura array.
|
||||
return 1;
|
||||
}
|
||||
@@ -243,7 +291,7 @@ int __fastcall Script_GetPlayerAuraBySpellID(void *L) {
|
||||
// sequential keys starting from `nextKey`. Updates `nextKey` so a
|
||||
// follow-up call can append to the same outer table.
|
||||
void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
|
||||
Data::Filter filter, int &nextKey, bool playerOnly) {
|
||||
Data::Filter filter, int &nextKey, Data::Match match) {
|
||||
const int start = (filter == Data::Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
@@ -253,14 +301,15 @@ void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!Data::IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (playerOnly && !Data::IsPlayerCast(unit, slot))
|
||||
if (!Data::MatchesAura(match, Data::IsPlayerCast(unit, slot),
|
||||
Data::ReadSpellID(unit, slot)))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
Data::Push(L, unit, slot);
|
||||
Game::Lua::SetTable(L, outerIdx);
|
||||
}
|
||||
// Append auras the descriptor dropped but Aura::Source still has live.
|
||||
Data::AppendCacheFallbacks(L, unit, filter, playerOnly, outerIdx, nextKey);
|
||||
Data::AppendCacheFallbacks(L, unit, filter, match, outerIdx, nextKey);
|
||||
}
|
||||
|
||||
int __fastcall Script_GetUnitAuras(void *L) {
|
||||
@@ -274,26 +323,24 @@ int __fastcall Script_GetUnitAuras(void *L) {
|
||||
// Range tokens are independent of PLAYER: an explicit HELPFUL/HARMFUL
|
||||
// selects that range, neither selects both. So `"PLAYER"` alone returns
|
||||
// both ranges restricted to player-cast auras (matching retail).
|
||||
const bool playerOnly = HasPlayerFilter(filterStr);
|
||||
const bool hasHelpful = filterStr != nullptr && strstr(filterStr, "HELPFUL") != nullptr;
|
||||
const bool hasHarmful = filterStr != nullptr && strstr(filterStr, "HARMFUL") != nullptr;
|
||||
const bool both = !hasHelpful && !hasHarmful;
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
const bool both = !pf.helpful && !pf.harmful;
|
||||
|
||||
int nextKey = 1;
|
||||
if (unit != nullptr) {
|
||||
if (hasHelpful || both)
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Helpful, nextKey, playerOnly);
|
||||
if (hasHarmful || both)
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Harmful, nextKey, playerOnly);
|
||||
if (pf.helpful || both)
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Helpful, nextKey, pf.ToMatch());
|
||||
if (pf.harmful || both)
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Harmful, nextKey, pf.ToMatch());
|
||||
} else {
|
||||
// No live CGUnit — out-of-range / cross-map groupmate. Enumerate the
|
||||
// group-member aura array (spell IDs the server still transmits), the
|
||||
// same source the built-in UnitBuff/UnitDebuff read out of range.
|
||||
const uint64_t guid = GuidForOutOfRange(unitToken);
|
||||
if (hasHelpful || both)
|
||||
Data::AppendGroupAuras(L, guid, Data::Filter::Helpful, playerOnly, 1, nextKey);
|
||||
if (hasHarmful || both)
|
||||
Data::AppendGroupAuras(L, guid, Data::Filter::Harmful, playerOnly, 1, nextKey);
|
||||
if (pf.helpful || both)
|
||||
Data::AppendGroupAuras(L, guid, Data::Filter::Helpful, pf.ToMatch(), 1, nextKey);
|
||||
if (pf.harmful || both)
|
||||
Data::AppendGroupAuras(L, guid, Data::Filter::Harmful, pf.ToMatch(), 1, nextKey);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
+146
-66
@@ -19,6 +19,8 @@
|
||||
#include "dbc/Lookup.h"
|
||||
#include "group/MemberStats.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "spell/CrowdControl.h"
|
||||
#include "spell/IsSelfBuff.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -28,13 +30,6 @@ namespace Aura::Data {
|
||||
|
||||
namespace {
|
||||
|
||||
// Spell.dbc record field offsets used locally. `spell/Info.cpp` has
|
||||
// the same set of locals for `Script_GetSpellInfo`; promoting them
|
||||
// to `Offsets.h` is a future refactor — for now we mirror its
|
||||
// convention so both files stay in sync.
|
||||
constexpr int OFF_SPELL_ICON_ID = 0x1D4;
|
||||
constexpr int OFF_SPELLICON_PATH = 0x04;
|
||||
|
||||
const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
if (unit == nullptr)
|
||||
return nullptr;
|
||||
@@ -72,13 +67,13 @@ const char *SpellIconPath(const uint8_t *spellRecord) {
|
||||
if (spellRecord == nullptr)
|
||||
return nullptr;
|
||||
const int iconID = *reinterpret_cast<const int *>(
|
||||
spellRecord + OFF_SPELL_ICON_ID);
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
if (iconID <= 0)
|
||||
return nullptr;
|
||||
return DBC::StringField(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT,
|
||||
static_cast<uint32_t>(iconID),
|
||||
OFF_SPELLICON_PATH);
|
||||
Offsets::OFF_SPELLICON_PATH);
|
||||
}
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *);
|
||||
@@ -209,6 +204,18 @@ double SpellBaseDurationSeconds(uint32_t spellID, int unitLevel) {
|
||||
// Reads the player's level from UNIT_FIELD_LEVEL. Returns 0 when
|
||||
// unresolved (pre-login, no descriptor) — caller should treat that
|
||||
// as "skip level scaling".
|
||||
// True if the engine-tick expiration `expMs` has already reached/passed now.
|
||||
// Same signed-wrap-safe compare `PlayerBuffExpirationSeconds` uses (absorbs
|
||||
// the ~24.86-day GetTickCount wrap). An already-elapsed cached expiration is
|
||||
// not meaningful — the caster is kept regardless, but the time is reported as
|
||||
// unknown rather than as a negative remaining.
|
||||
bool ExpirationElapsed(uint32_t expMs) {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
const uint32_t nowMs = reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
return static_cast<int32_t>(nowMs - expMs) >= 0;
|
||||
}
|
||||
|
||||
int PlayerLevel(const uint8_t *player) {
|
||||
auto *desc = Descriptor(player);
|
||||
if (desc == nullptr)
|
||||
@@ -217,6 +224,25 @@ int PlayerLevel(const uint8_t *player) {
|
||||
desc + Offsets::OFF_UNIT_FIELD_LEVEL);
|
||||
}
|
||||
|
||||
// The caster GUID attributed to `(guid, spellID)`: the observed caster from
|
||||
// the Aura::Source cache, else — when we never saw the cast — the unit itself
|
||||
// if the spell is a self-only-target buff (self-buffs can't be cross-cast).
|
||||
// 0 when genuinely unknown. Single source of truth for both display
|
||||
// (sourceGUID/sourceUnit) and the PLAYER caster filter, so a self-buff shows a
|
||||
// source AND matches HELPFUL|PLAYER instead of the two paths disagreeing.
|
||||
uint64_t EffectiveCaster(uint64_t guid, uint32_t spellID) {
|
||||
if (guid == 0 || spellID == 0)
|
||||
return 0;
|
||||
uint64_t c = 0;
|
||||
uint32_t expMs = 0;
|
||||
uint32_t durMs = 0;
|
||||
if (Aura::Source::Get(guid, spellID, &c, &expMs, &durMs) && c != 0)
|
||||
return c; // observed caster
|
||||
if (Spell::IsSelfBuff::IsSelfBuff(spellID))
|
||||
return guid; // self-only-target aura → cast by the unit itself
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t ReadSpellID(const uint8_t *unit, int slot) {
|
||||
@@ -250,16 +276,67 @@ bool IsPlayerCast(const uint8_t *unit, int slot) {
|
||||
const uint32_t spellID = ReadSpellID(unit, slot);
|
||||
if (spellID == 0)
|
||||
return false;
|
||||
uint64_t casterGuid = 0;
|
||||
uint32_t expMs = 0;
|
||||
uint32_t durMs = 0;
|
||||
if (!Aura::Source::Get(UnitGuid(unit), spellID, &casterGuid, &expMs, &durMs))
|
||||
const uint64_t player = Unit::Identity::PlayerGuid();
|
||||
return player != 0 && EffectiveCaster(UnitGuid(unit), spellID) == player;
|
||||
}
|
||||
|
||||
// Applies the PLAYER / !PLAYER caster restriction. `isPlayerCast` is the
|
||||
// per-aura "was this cast by the local player" answer (false on a cache miss).
|
||||
bool CasterMatches(CasterMode caster, bool isPlayerCast) {
|
||||
switch (caster) {
|
||||
case CasterMode::PlayerOnly: return isPlayerCast;
|
||||
case CasterMode::NotPlayer: return !isPlayerCast;
|
||||
default: return true; // Any
|
||||
}
|
||||
}
|
||||
|
||||
bool IsDispellable(uint32_t spellID) {
|
||||
const uint8_t *rec = SpellRecord(spellID);
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
return casterGuid != 0 && casterGuid == Unit::Identity::PlayerGuid();
|
||||
const uint32_t dispelType = *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_DISPEL_TYPE);
|
||||
// Magic(1) / Curse(2) / Disease(3) / Poison(4) — the server's
|
||||
// DISPEL_ALL_MASK. None(0) / Stealth(5) / Invisibility(6) / Enrage(9)
|
||||
// etc. are not dispellable by any mechanism.
|
||||
return dispelType >= 1 && dispelType <= 4;
|
||||
}
|
||||
|
||||
bool DispelMatches(DispelMode dispel, uint32_t spellID) {
|
||||
switch (dispel) {
|
||||
case DispelMode::DispellableOnly: return IsDispellable(spellID);
|
||||
case DispelMode::NotDispellable: return !IsDispellable(spellID);
|
||||
default: return true; // Any
|
||||
}
|
||||
}
|
||||
|
||||
bool CcMatches(CcMode cc, uint32_t spellID) {
|
||||
switch (cc) {
|
||||
case CcMode::CrowdControlOnly:
|
||||
return Spell::CrowdControl::IsCrowdControl(spellID);
|
||||
case CcMode::NotCrowdControl:
|
||||
return !Spell::CrowdControl::IsCrowdControl(spellID);
|
||||
default:
|
||||
return true; // Any
|
||||
}
|
||||
}
|
||||
|
||||
bool MatchesAura(const Match &match, bool isPlayerCast, uint32_t spellID) {
|
||||
return CasterMatches(match.caster, isPlayerCast) &&
|
||||
DispelMatches(match.dispel, spellID) &&
|
||||
CcMatches(match.cc, spellID);
|
||||
}
|
||||
|
||||
// Group-array analog of IsPlayerCast: the group aura carries no caster, so we
|
||||
// consult the Aura::Source cache by (guid, spellID). A miss counts as "not the
|
||||
// player" (same as IsPlayerCast).
|
||||
bool GroupIsPlayerCast(uint64_t guid, uint32_t spellID) {
|
||||
const uint64_t player = Unit::Identity::PlayerGuid();
|
||||
return player != 0 && EffectiveCaster(guid, spellID) == player;
|
||||
}
|
||||
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
bool playerOnly) {
|
||||
Match match) {
|
||||
if (unit == nullptr || oneBasedIndex < 1)
|
||||
return -1;
|
||||
const int start = (filter == Filter::Harmful)
|
||||
@@ -272,7 +349,7 @@ int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
continue;
|
||||
if (++matches == oneBasedIndex)
|
||||
return slot;
|
||||
@@ -281,7 +358,7 @@ int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
}
|
||||
|
||||
int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
const Filter *filter, bool playerOnly) {
|
||||
const Filter *filter, Match match) {
|
||||
if (unit == nullptr || spellID == 0)
|
||||
return -1;
|
||||
const int start = (filter != nullptr && *filter == Filter::Harmful)
|
||||
@@ -295,7 +372,7 @@ int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
continue;
|
||||
if (ReadSpellID(unit, slot) != spellID)
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
continue;
|
||||
return slot;
|
||||
}
|
||||
@@ -303,7 +380,7 @@ int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
}
|
||||
|
||||
int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const Filter *filter, bool playerOnly) {
|
||||
const Filter *filter, Match match) {
|
||||
if (unit == nullptr || spellName == nullptr || *spellName == '\0')
|
||||
return -1;
|
||||
const int start = (filter != nullptr && *filter == Filter::Harmful)
|
||||
@@ -318,7 +395,7 @@ int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const char *name = LocalizedSpellName(SpellRecord(ReadSpellID(unit, slot)));
|
||||
if (name == nullptr || std::strcmp(name, spellName) != 0)
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
continue;
|
||||
return slot;
|
||||
}
|
||||
@@ -469,12 +546,21 @@ static void PushEnriched(void *L, uint64_t guid, uint32_t spellID,
|
||||
uint32_t expMs = 0;
|
||||
uint32_t durMs = 0;
|
||||
if (Aura::Source::Get(guid, spellID, &c, &expMs, &durMs)) {
|
||||
if (expirationTime == 0.0 && expMs != 0)
|
||||
// A cached expiration that already elapsed while the aura is still
|
||||
// present (non-player casters get an underestimated base duration)
|
||||
// is not meaningful — report unknown (0) rather than a negative
|
||||
// remaining time.
|
||||
if (expirationTime == 0.0 && expMs != 0 && !ExpirationElapsed(expMs))
|
||||
expirationTime = static_cast<double>(expMs) * 0.001;
|
||||
if (durMs != 0)
|
||||
duration = static_cast<double>(durMs) * 0.001;
|
||||
casterGuid = c;
|
||||
}
|
||||
// Caster (sourceGUID/sourceUnit): observed caster, else the self-buff
|
||||
// inference — same resolution the PLAYER filter uses, so a self-buff
|
||||
// both shows a source and matches HELPFUL|PLAYER. Timing stays unknown
|
||||
// when we never saw the cast; only the caster is inferred, and it's
|
||||
// exact.
|
||||
casterGuid = EffectiveCaster(guid, spellID);
|
||||
}
|
||||
BuildTable(L, spellID, applications, isHelpful, duration, expirationTime,
|
||||
casterGuid);
|
||||
@@ -548,8 +634,8 @@ bool DescriptorHasVisibleAura(const uint8_t *unit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Counts populated descriptor slots matching `filter` (and `playerOnly`).
|
||||
int CountSlots(const uint8_t *unit, Filter filter, bool playerOnly) {
|
||||
// Counts populated descriptor slots matching `filter` (and `caster`).
|
||||
int CountSlots(const uint8_t *unit, Filter filter, Match match) {
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
const int start = (filter == Filter::Harmful)
|
||||
@@ -561,7 +647,7 @@ int CountSlots(const uint8_t *unit, Filter filter, bool playerOnly) {
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
continue;
|
||||
++n;
|
||||
}
|
||||
@@ -571,21 +657,21 @@ int CountSlots(const uint8_t *unit, Filter filter, bool playerOnly) {
|
||||
constexpr int kFallbackMax = Offsets::UNIT_AURA_TOTAL;
|
||||
|
||||
// Whether cache entry `c` should be surfaced as a fallback for `unit` under
|
||||
// `filter`/`playerOnly`: the spell must be a user-visible aura, it must not
|
||||
// `filter`/`caster`: the spell must be a user-visible aura, it must not
|
||||
// already be in a populated descriptor slot (no double-listing the same live
|
||||
// aura) and, when player-filtered, must have been cast by us. Superseded /
|
||||
// dispelled auras are kept out of the cache by `Aura::Source`'s removal
|
||||
// eviction, not filtered here.
|
||||
bool FallbackEligible(const uint8_t *unit, const Aura::Source::CachedAura &c,
|
||||
Filter filter, bool playerOnly) {
|
||||
Filter filter, Match match) {
|
||||
if (!IsVisible(SpellRecord(c.spellId)))
|
||||
return false; // hidden/internal aura (e.g. SPELL_ATTR_HIDDEN_CLIENTSIDE):
|
||||
// the descriptor path drops these via IsSlotPopulated, so
|
||||
// the cache path must apply the same engine predicate or
|
||||
// they leak through as tooltip-less phantom buffs.
|
||||
if (FindSlotBySpellID(unit, c.spellId, &filter, false) >= 0)
|
||||
if (FindSlotBySpellID(unit, c.spellId, &filter, Match{}) >= 0)
|
||||
return false; // still in the descriptor — returned via the slot path
|
||||
if (playerOnly && c.casterGuid != Unit::Identity::PlayerGuid())
|
||||
if (!MatchesAura(match, c.casterGuid == Unit::Identity::PlayerGuid(), c.spellId))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
@@ -593,7 +679,7 @@ bool FallbackEligible(const uint8_t *unit, const Aura::Source::CachedAura &c,
|
||||
} // namespace
|
||||
|
||||
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
Filter filter, bool playerOnly) {
|
||||
Filter filter, Match match) {
|
||||
if (unit == nullptr)
|
||||
return false;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
@@ -602,7 +688,7 @@ bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
// The descriptor held `descCount` matches; the caller already found fewer
|
||||
// than `oneBasedIndex` there, so the cache supplies index
|
||||
// `oneBasedIndex - descCount` (1-based) of the entries it didn't.
|
||||
const int target = oneBasedIndex - CountSlots(unit, filter, playerOnly);
|
||||
const int target = oneBasedIndex - CountSlots(unit, filter, match);
|
||||
if (target < 1)
|
||||
return false;
|
||||
const uint64_t guid = UnitGuid(unit);
|
||||
@@ -613,7 +699,7 @@ bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
int seen = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!FallbackEligible(unit, buf[i], filter, playerOnly))
|
||||
if (!FallbackEligible(unit, buf[i], filter, match))
|
||||
continue;
|
||||
if (++seen == target) {
|
||||
PushFromCache(L, unit, buf[i], filter == Filter::Helpful);
|
||||
@@ -624,7 +710,7 @@ bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
}
|
||||
|
||||
void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
bool playerOnly, int outerIdx, int &nextKey) {
|
||||
Match match, int outerIdx, int &nextKey) {
|
||||
if (unit == nullptr)
|
||||
return;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
@@ -637,7 +723,7 @@ void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
const int n = Aura::Source::Enumerate(
|
||||
guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!FallbackEligible(unit, buf[i], filter, playerOnly))
|
||||
if (!FallbackEligible(unit, buf[i], filter, match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
PushFromCache(L, unit, buf[i], filter == Filter::Helpful);
|
||||
@@ -668,22 +754,16 @@ uint32_t GroupSpellID(const uint16_t *arr, int slot) {
|
||||
// the engine's per-slot test in Script_UnitBuff/UnitDebuff — a non-zero spell
|
||||
// ID with a real Spell.dbc record that passes the visibility predicate (the
|
||||
// server only transmits *applied* auras, so there is no flag nibble to check).
|
||||
// `playerOnly` additionally requires the `Aura::Source` cache to attribute the
|
||||
// `caster` additionally requires the `Aura::Source` cache to attribute the
|
||||
// cast to the local player (a miss excludes it — we can't confirm the caster
|
||||
// from the group array alone).
|
||||
bool GroupSlotEligible(uint64_t guid, const uint16_t *arr, int slot,
|
||||
bool playerOnly) {
|
||||
Match match) {
|
||||
const uint32_t id = GroupSpellID(arr, slot);
|
||||
if (id == 0 || !IsVisible(SpellRecord(id)))
|
||||
return false;
|
||||
if (playerOnly) {
|
||||
uint64_t c = 0;
|
||||
uint32_t e = 0;
|
||||
uint32_t d = 0;
|
||||
if (!Aura::Source::Get(guid, id, &c, &e, &d) ||
|
||||
c != Unit::Identity::PlayerGuid())
|
||||
return false;
|
||||
}
|
||||
if (!MatchesAura(match, GroupIsPlayerCast(guid, id), id))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -715,13 +795,13 @@ bool GroupArrayHasVisibleAura(const uint16_t *arr) {
|
||||
// aura, not already present in the array, and — when player-filtered — cast by
|
||||
// us. See GroupArrayHasVisibleAura for when this path runs.
|
||||
bool GroupFallbackEligible(const uint16_t *arr, const Aura::Source::CachedAura &c,
|
||||
bool playerOnly) {
|
||||
Match match) {
|
||||
if (!IsVisible(SpellRecord(c.spellId)))
|
||||
return false;
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot)
|
||||
if (GroupSpellID(arr, slot) == c.spellId)
|
||||
return false;
|
||||
if (playerOnly && c.casterGuid != Unit::Identity::PlayerGuid())
|
||||
if (!MatchesAura(match, c.casterGuid == Unit::Identity::PlayerGuid(), c.spellId))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
@@ -729,13 +809,13 @@ bool GroupFallbackEligible(const uint16_t *arr, const Aura::Source::CachedAura &
|
||||
// Nth (1-based) cache-fallback aura of `filter`'s kind for an out-of-range
|
||||
// member whose group array is empty. Pushes the AuraData table on a hit.
|
||||
bool PushNthGroupCacheFallback(void *L, uint64_t guid, const uint16_t *arr,
|
||||
int oneBasedIndex, Filter filter, bool playerOnly) {
|
||||
int oneBasedIndex, Filter filter, Match match) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n =
|
||||
Aura::Source::Enumerate(guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
int seen = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!GroupFallbackEligible(arr, buf[i], playerOnly))
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
continue;
|
||||
if (++seen == oneBasedIndex) {
|
||||
PushEnriched(L, guid, buf[i].spellId, filter == Filter::Helpful, 1,
|
||||
@@ -748,14 +828,14 @@ bool PushNthGroupCacheFallback(void *L, uint64_t guid, const uint16_t *arr,
|
||||
|
||||
// Append every cache-fallback aura of `filter`'s kind into the array table.
|
||||
void AppendGroupCacheFallbacks(void *L, uint64_t guid, const uint16_t *arr,
|
||||
Filter filter, bool playerOnly, int outerIdx,
|
||||
Filter filter, Match match, int outerIdx,
|
||||
int &nextKey) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n =
|
||||
Aura::Source::Enumerate(guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
const int level = GroupMemberLevel(guid);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!GroupFallbackEligible(arr, buf[i], playerOnly))
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
PushEnriched(L, guid, buf[i].spellId, filter == Filter::Helpful, 1, level,
|
||||
@@ -768,7 +848,7 @@ void AppendGroupCacheFallbacks(void *L, uint64_t guid, const uint16_t *arr,
|
||||
// (when non-null), of the given kind. Backs the by-ID / by-name group queries.
|
||||
bool PushGroupCacheFallbackMatch(void *L, uint64_t guid, const uint16_t *arr,
|
||||
bool harmful, uint32_t spellID,
|
||||
const char *spellName, bool playerOnly) {
|
||||
const char *spellName, Match match) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(guid, harmful, buf, kFallbackMax);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
@@ -779,7 +859,7 @@ bool PushGroupCacheFallbackMatch(void *L, uint64_t guid, const uint16_t *arr,
|
||||
if (nm == nullptr || std::strcmp(nm, spellName) != 0)
|
||||
continue;
|
||||
}
|
||||
if (!GroupFallbackEligible(arr, buf[i], playerOnly))
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
continue;
|
||||
PushEnriched(L, guid, buf[i].spellId, !harmful, 1, GroupMemberLevel(guid),
|
||||
false);
|
||||
@@ -791,7 +871,7 @@ bool PushGroupCacheFallbackMatch(void *L, uint64_t guid, const uint16_t *arr,
|
||||
} // namespace
|
||||
|
||||
bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
bool playerOnly) {
|
||||
Match match) {
|
||||
if (guid == 0 || oneBasedIndex < 1)
|
||||
return false;
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
@@ -802,7 +882,7 @@ bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
GroupRange(filter, start, end);
|
||||
int matches = 0;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!GroupSlotEligible(guid, arr, slot, playerOnly))
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
continue;
|
||||
if (++matches == oneBasedIndex) {
|
||||
PushEnriched(L, guid, GroupSpellID(arr, slot),
|
||||
@@ -814,12 +894,12 @@ bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
if (!GroupArrayHasVisibleAura(arr))
|
||||
return PushNthGroupCacheFallback(L, guid, arr, oneBasedIndex, filter,
|
||||
playerOnly);
|
||||
match);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
const Filter *filter, bool playerOnly) {
|
||||
const Filter *filter, Match match) {
|
||||
if (guid == 0 || spellID == 0)
|
||||
return false;
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
@@ -835,7 +915,7 @@ bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (GroupSpellID(arr, slot) != spellID)
|
||||
continue;
|
||||
if (!GroupSlotEligible(guid, arr, slot, playerOnly))
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
continue;
|
||||
PushEnriched(L, guid, spellID, slot < Offsets::UNIT_AURA_BUFF_COUNT, 1,
|
||||
GroupMemberLevel(guid), false);
|
||||
@@ -845,18 +925,18 @@ bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, spellID, nullptr,
|
||||
playerOnly))
|
||||
match))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, spellID, nullptr,
|
||||
playerOnly))
|
||||
match))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
const Filter *filter, bool playerOnly) {
|
||||
const Filter *filter, Match match) {
|
||||
if (guid == 0 || spellName == nullptr || *spellName == '\0')
|
||||
return false;
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
@@ -876,7 +956,7 @@ bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
const char *name = LocalizedSpellName(SpellRecord(id));
|
||||
if (name == nullptr || std::strcmp(name, spellName) != 0)
|
||||
continue;
|
||||
if (!GroupSlotEligible(guid, arr, slot, playerOnly))
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
continue;
|
||||
PushEnriched(L, guid, id, slot < Offsets::UNIT_AURA_BUFF_COUNT, 1,
|
||||
GroupMemberLevel(guid), false);
|
||||
@@ -886,17 +966,17 @@ bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, 0, spellName,
|
||||
playerOnly))
|
||||
match))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, 0, spellName,
|
||||
playerOnly))
|
||||
match))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AppendGroupAuras(void *L, uint64_t guid, Filter filter, bool playerOnly,
|
||||
void AppendGroupAuras(void *L, uint64_t guid, Filter filter, Match match,
|
||||
int outerIdx, int &nextKey) {
|
||||
if (guid == 0)
|
||||
return;
|
||||
@@ -908,7 +988,7 @@ void AppendGroupAuras(void *L, uint64_t guid, Filter filter, bool playerOnly,
|
||||
GroupRange(filter, start, end);
|
||||
const int level = GroupMemberLevel(guid);
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!GroupSlotEligible(guid, arr, slot, playerOnly))
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
PushEnriched(L, guid, GroupSpellID(arr, slot),
|
||||
@@ -917,7 +997,7 @@ void AppendGroupAuras(void *L, uint64_t guid, Filter filter, bool playerOnly,
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
if (!GroupArrayHasVisibleAura(arr))
|
||||
AppendGroupCacheFallbacks(L, guid, arr, filter, playerOnly, outerIdx,
|
||||
AppendGroupCacheFallbacks(L, guid, arr, filter, match, outerIdx,
|
||||
nextKey);
|
||||
}
|
||||
|
||||
|
||||
+54
-9
@@ -33,6 +33,37 @@ namespace Aura::Data {
|
||||
// to helpful when no filter is specified.
|
||||
enum class Filter { Helpful, Harmful };
|
||||
|
||||
// Caster restriction from the `PLAYER` / `!PLAYER` aura filter tokens.
|
||||
// `Any` = no restriction; `PlayerOnly` = only auras the local player cast
|
||||
// (`PLAYER`); `NotPlayer` = only auras NOT cast by the local player
|
||||
// (`!PLAYER`). Caster attribution comes from the `Aura::Source` cache; a
|
||||
// cache miss counts as "not the player" (so `PlayerOnly` excludes it and
|
||||
// `NotPlayer` includes it — matching `IsPlayerCast`'s miss semantics).
|
||||
enum class CasterMode { Any, PlayerOnly, NotPlayer };
|
||||
|
||||
// Restriction from the `DISPELLABLE` / `!DISPELLABLE` aura filter tokens.
|
||||
// `Any` = no restriction; `DispellableOnly` = only auras that can be
|
||||
// dispelled / purged / stolen by SOME mechanism (dispel type Magic, Curse,
|
||||
// Disease, or Poison — the server's DISPEL_ALL_MASK); `NotDispellable` = the
|
||||
// complement. "Regardless of whether the local player can dispel it" —
|
||||
// matching the modern AuraFilters semantics.
|
||||
enum class DispelMode { Any, DispellableOnly, NotDispellable };
|
||||
|
||||
// Restriction from the `CROWD_CONTROL` / `!CROWD_CONTROL` aura filter tokens.
|
||||
// `Any` = no restriction; `CrowdControlOnly` = only auras that are a crowd-
|
||||
// control effect (stun / fear / silence / root / etc., per
|
||||
// `Spell::CrowdControl`); `NotCrowdControl` = the complement.
|
||||
enum class CcMode { Any, CrowdControlOnly, NotCrowdControl };
|
||||
|
||||
// A parsed aura-filter's per-aura predicates (beyond the helpful/harmful
|
||||
// range, which is the separate `Filter`). Extensible: new orthogonal filter
|
||||
// dimensions add a field here rather than another function parameter.
|
||||
struct Match {
|
||||
CasterMode caster = CasterMode::Any;
|
||||
DispelMode dispel = DispelMode::Any;
|
||||
CcMode cc = CcMode::Any;
|
||||
};
|
||||
|
||||
// Reads the spell ID at the given absolute slot (0..47). Returns 0
|
||||
// if the slot is empty or the unit pointer is null.
|
||||
uint32_t ReadSpellID(const uint8_t *unit, int slot);
|
||||
@@ -50,27 +81,41 @@ bool IsSlotPopulated(const uint8_t *unit, int slot);
|
||||
// UNIT_AURA_TOTAL-1 for Harmful. When `playerOnly` is true, only auras
|
||||
// the local player cast (per `IsPlayerCast`) are counted/returned.
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
bool playerOnly = false);
|
||||
Match match = {});
|
||||
|
||||
// Finds the absolute slot of a populated aura with the given spell
|
||||
// ID, optionally restricted to one filter range. `filter` of
|
||||
// nullptr searches both ranges (helpful first, then harmful).
|
||||
// `playerOnly` restricts to player-cast auras. Returns -1 if not found.
|
||||
int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
const Filter *filter, bool playerOnly = false);
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// Finds the absolute slot of a populated aura whose locale-resolved
|
||||
// spell name exactly matches `spellName`. Case-sensitive. Otherwise
|
||||
// same semantics as `FindSlotBySpellID`. Returns -1 if not found
|
||||
// or if `spellName` is null/empty.
|
||||
int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const Filter *filter, bool playerOnly = false);
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// True iff the aura at `slot` was cast by the local player, per the
|
||||
// `Aura::Source` cache. False on a cache miss (caster unknown) — so a
|
||||
// PLAYER-filtered query excludes auras whose cast we didn't observe.
|
||||
bool IsPlayerCast(const uint8_t *unit, int slot);
|
||||
|
||||
// Applies a `CasterMode` to a per-aura "was cast by the local player" answer.
|
||||
// `Any` → always true; `PlayerOnly` → the answer; `NotPlayer` → its negation.
|
||||
bool CasterMatches(CasterMode caster, bool isPlayerCast);
|
||||
|
||||
// True iff the spell's dispel type is one a dispel/purge/steal can remove —
|
||||
// Magic, Curse, Disease, or Poison (`Spell.dbc` Dispel ∈ 1..4). None,
|
||||
// Stealth, Invisibility, Enrage, etc. are not dispellable. Verified against
|
||||
// the server's DISPEL_ALL_MASK.
|
||||
bool IsDispellable(uint32_t spellID);
|
||||
|
||||
// Applies a full `Match` (caster + dispel predicates) to one aura, given the
|
||||
// per-aura "was cast by the local player" answer and its spell ID.
|
||||
bool MatchesAura(const Match &match, bool isPlayerCast, uint32_t spellID);
|
||||
|
||||
// Display stack count for the given slot (engine stores stacks-1,
|
||||
// we add 1). Returns 0 if the unit is null.
|
||||
int ReadStacks(const uint8_t *unit, int slot);
|
||||
@@ -122,7 +167,7 @@ void Push(void *L, const uint8_t *unit, int slot);
|
||||
// helpful/harmful classification matches `filter` and that aren't already in a
|
||||
// populated descriptor slot are considered.
|
||||
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
Filter filter, bool playerOnly = false);
|
||||
Filter filter, Match match = {});
|
||||
|
||||
// Appends every eligible `Aura::Source` cache fallback for `unit` matching
|
||||
// `filter` into the array table at `outerIdx`, continuing from `nextKey`
|
||||
@@ -130,7 +175,7 @@ bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
// for `GetUnitAuras`. Skips entries already present in a populated descriptor
|
||||
// slot so it never double-lists.
|
||||
void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
bool playerOnly, int outerIdx, int &nextKey);
|
||||
Match match, int outerIdx, int &nextKey);
|
||||
|
||||
// ── Out-of-range groupmate path ────────────────────────────────────────────
|
||||
//
|
||||
@@ -154,23 +199,23 @@ void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
// matching `filter`. On a hit pushes the table and returns true; otherwise
|
||||
// pushes nothing and returns false (caller pushes nil).
|
||||
bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
bool playerOnly = false);
|
||||
Match match = {});
|
||||
|
||||
// Pushes AuraData for the group-array aura with `spellID` on `guid`, optionally
|
||||
// restricted to one filter range (nullptr = both, helpful first). Returns true
|
||||
// on a hit (table pushed), false otherwise (nothing pushed).
|
||||
bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
const Filter *filter, bool playerOnly = false);
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// As `PushGroupAuraBySpellID` but matched by locale-resolved spell name
|
||||
// (case-sensitive exact match), mirroring `FindSlotBySpellName`.
|
||||
bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
const Filter *filter, bool playerOnly = false);
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// Appends every group-array aura on `guid` matching `filter` into the array
|
||||
// table at `outerIdx`, continuing from `nextKey` (updated in place). The
|
||||
// bulk-enumeration analog of `PushNthGroupAura`, for `GetUnitAuras`.
|
||||
void AppendGroupAuras(void *L, uint64_t guid, Filter filter, bool playerOnly,
|
||||
void AppendGroupAuras(void *L, uint64_t guid, Filter filter, Match match,
|
||||
int outerIdx, int &nextKey);
|
||||
|
||||
} // namespace Aura::Data
|
||||
|
||||
+92
-10
@@ -135,9 +135,18 @@ struct Entry {
|
||||
bool used;
|
||||
};
|
||||
|
||||
constexpr int kCacheSize = 256;
|
||||
// Sized for the realistic worst case: a fully raid-buffed 40-man plus its
|
||||
// debuff load. We cache one entry per (target, spellId) for EVERY
|
||||
// aura-applying SMSG_SPELL_GO we observe — not just auras on the player, but
|
||||
// every buff cast on every unit in view — so the live working set in a raid
|
||||
// is ~40 members × ~30 persistent buffs ≈ 1200, plus debuffs. At the old 256
|
||||
// the table was permanently full and the overflow path evicted live buffs,
|
||||
// dropping their source (the "lost sourceGUID on a buff" report). 2048 gives
|
||||
// a fully-buffed 40-man comfortable headroom; the tick sweep still reclaims
|
||||
// expired/orphaned entries so it rarely approaches full outside a raid.
|
||||
// (~40 bytes/entry → ~80 KB static.)
|
||||
constexpr int kCacheSize = 2048;
|
||||
Entry g_cache[kCacheSize];
|
||||
int g_writeCursor = 0;
|
||||
|
||||
// SMSG_SPELL_GO arrives before the SMSG_UPDATE_OBJECT that adds the aura to
|
||||
// the target's descriptor, so for a brief window a just-captured entry names
|
||||
@@ -149,6 +158,43 @@ int g_writeCursor = 0;
|
||||
// gets dropped (and OnAuraRemoved evicts real removals directly regardless).
|
||||
constexpr uint32_t kEvictGraceMs = 2000;
|
||||
|
||||
// True if `guid`'s live unit descriptor still lists `spellId` in any aura
|
||||
// slot. The descriptor is the presence authority while the unit is in view,
|
||||
// so an entry backing a still-listed aura must never be timer-reclaimed: the
|
||||
// caster (sourceGUID / sourceUnit) is immutable for the aura's lifetime, but
|
||||
// our `expirationMs` is only a base-duration estimate for non-player casters
|
||||
// (we lack their mod tables), which underestimates talent-extended auras and
|
||||
// elapses while the aura is still up. Resolves the GUID through the object
|
||||
// manager — non-throwing, returns null when the unit isn't loaded / is out of
|
||||
// range — so an out-of-range or stealthed unit reports false and its
|
||||
// fallback-only entry stays timer-reclaimable exactly as before.
|
||||
bool DescriptorListsAura(uint64_t guid, uint32_t spellId) {
|
||||
if (guid == 0 || spellId == 0)
|
||||
return false;
|
||||
using ResolveByGuid_t = void *(__fastcall *)(int typeMask, const char *dbg,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi, int priority);
|
||||
constexpr int kUnitOrPlayerMask =
|
||||
(1 << Offsets::OBJECT_TYPE_UNIT) | (1 << Offsets::OBJECT_TYPE_PLAYER);
|
||||
auto resolve = reinterpret_cast<ResolveByGuid_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
|
||||
const auto *unit = static_cast<const uint8_t *>(
|
||||
resolve(kUnitOrPlayerMask, nullptr, static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0));
|
||||
if (unit == nullptr)
|
||||
return false;
|
||||
const auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_CGUNIT_OBJECT_FIELDS);
|
||||
if (desc == nullptr)
|
||||
return false;
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot) {
|
||||
if (*reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_AURA + slot * 4) == spellId)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// `fromCast` true: the SpellGo hook — authoritative caster + caster-modified
|
||||
// (talented) timing. False: the OnAuraAdded application hook — timing only,
|
||||
// no caster, and it must not clobber an entry SpellGo already owns (that
|
||||
@@ -180,18 +226,44 @@ void Store(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Take a free slot, else an expired one, else evict round-robin.
|
||||
// Take a free slot, else an expired one whose aura the descriptor no
|
||||
// longer lists (a still-present aura keeps its slot so its caster isn't
|
||||
// lost — see DescriptorListsAura), else evict round-robin.
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used || (e.expirationMs != 0 && now >= e.expirationMs)) {
|
||||
if (!e.used ||
|
||||
(e.expirationMs != 0 && now >= e.expirationMs &&
|
||||
!DescriptorListsAura(e.targetGuid, e.spellId))) {
|
||||
e = {targetGuid, casterGuid, spellId, expirationMs, durationMs,
|
||||
now, kind, true};
|
||||
return;
|
||||
}
|
||||
}
|
||||
Entry &slot = g_cache[g_writeCursor];
|
||||
g_writeCursor = (g_writeCursor + 1) % kCacheSize;
|
||||
slot = {targetGuid, casterGuid, spellId, expirationMs, durationMs, now,
|
||||
kind, true};
|
||||
// Saturated. Honor the same invariant as the tick sweep: an entry whose
|
||||
// aura is still present on a resolvable unit is NEVER evicted — its caster
|
||||
// can't be recovered once dropped (the "buff still on the player lost its
|
||||
// source" case). Prefer the least-recently-updated ORPHAN (no live
|
||||
// descriptor backing: out of range / despawned / genuinely gone); only if
|
||||
// every single slot is a present, live aura — true saturation we can't
|
||||
// avoid — fall back to the global LRU victim. With kCacheSize sized to the
|
||||
// raid working set, reaching here is already unlikely and the saturated
|
||||
// fallback is practically unreachable.
|
||||
Entry *orphan = nullptr;
|
||||
Entry *lru = &g_cache[0];
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used) { // defensive: a freed slot always wins outright
|
||||
orphan = &e;
|
||||
break;
|
||||
}
|
||||
if (e.stampMs < lru->stampMs)
|
||||
lru = &e;
|
||||
if (DescriptorListsAura(e.targetGuid, e.spellId))
|
||||
continue; // still present on a live descriptor — protect it
|
||||
if (orphan == nullptr || e.stampMs < orphan->stampMs)
|
||||
orphan = &e;
|
||||
}
|
||||
Entry *victim = (orphan != nullptr) ? orphan : lru;
|
||||
*victim = {targetGuid, casterGuid, spellId, expirationMs, durationMs, now,
|
||||
kind, true};
|
||||
}
|
||||
|
||||
// ---- Out-of-range group-member aura snapshots ---------------------------
|
||||
@@ -547,8 +619,18 @@ void OnWorldTick() {
|
||||
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
if (e.used && e.expirationMs != 0 && now >= e.expirationMs)
|
||||
e.used = false;
|
||||
if (!e.used || e.expirationMs == 0 || now < e.expirationMs)
|
||||
continue;
|
||||
// A timer elapse doesn't prove the aura is gone — expirationMs is only
|
||||
// a base-duration estimate for non-player casters. Keep any entry whose
|
||||
// aura the owning unit's descriptor still lists (descriptor = presence
|
||||
// authority in view; caster is immutable for the aura's life). Only
|
||||
// genuinely orphaned entries (unit out of range / stealthed / despawned
|
||||
// → no descriptor backing) are timer-reclaimed; the fallback path
|
||||
// already skips expired entries, so this can't resurface a phantom.
|
||||
if (DescriptorListsAura(e.targetGuid, e.spellId))
|
||||
continue;
|
||||
e.used = false;
|
||||
}
|
||||
// Forget snapshots for members we haven't polled recently (left the group,
|
||||
// or no longer displayed) so a later re-appearance re-baselines cleanly.
|
||||
|
||||
@@ -410,6 +410,56 @@ void CallEngineMouseover(uint64_t guid) {
|
||||
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32), 0, 0);
|
||||
}
|
||||
|
||||
// True when the GUID resolves to a live unit/player object (loaded, in range).
|
||||
// False for an offline or far out-of-range party/raid member — the case the
|
||||
// engine mouseover setter skips the tooltip for. Same resolver Unit::Mouseover
|
||||
// uses for its unit test.
|
||||
bool GuidHasLiveUnit(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return false;
|
||||
constexpr int kUnitOrPlayerMask =
|
||||
(1 << Offsets::OBJECT_TYPE_UNIT) | (1 << Offsets::OBJECT_TYPE_PLAYER);
|
||||
auto resolve = reinterpret_cast<void *(__fastcall *)(int, const char *, uint32_t,
|
||||
uint32_t, int)>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
|
||||
return resolve(kUnitOrPlayerMask, "ClassicAPI",
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0) != nullptr;
|
||||
}
|
||||
|
||||
// Build the mouseover tooltip for a unit with no live object, straight from the
|
||||
// party/raid roster. FUN_00529FE0 is the same unit-tooltip builder SetUnit and
|
||||
// the engine mouseover use; with no object it fills name/level/class + the
|
||||
// "Offline" line from the roster (verified: `GameTooltip:SetUnit("party1")`
|
||||
// works for an offline member). The engine's setter (FUN_00492890) gates this
|
||||
// call on a live object, so offline members get nothing — we do it here.
|
||||
void BuildRosterUnitTooltip(uint64_t guid) {
|
||||
void *tooltip = *reinterpret_cast<void *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_GAMETOOLTIP_OBJECT_PTR));
|
||||
if (tooltip == nullptr)
|
||||
return;
|
||||
|
||||
// Fire OnTooltipSetDefaultAnchor first, exactly as the engine mouseover
|
||||
// setter (FUN_00492890) does before it builds — FrameXML's handler sets
|
||||
// the tooltip's owner + anchor, which is what actually shows it. Without
|
||||
// this, a rebuild after the tooltip was hidden on the previous leave has
|
||||
// no owner and never re-appears (only a subsequent live-unit hover, which
|
||||
// takes the engine's own anchor path, would). Use the self-contained
|
||||
// invoker FUN_FRAME_INVOKE_SCRIPT, safe to call from the WorldTick.
|
||||
const int anchorHandler = *reinterpret_cast<const int *>(
|
||||
reinterpret_cast<uint8_t *>(tooltip) +
|
||||
Offsets::OFF_TOOLTIP_SET_DEFAULT_ANCHOR_HANDLER);
|
||||
if (anchorHandler != 0)
|
||||
reinterpret_cast<void(__fastcall *)(int, void *)>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FRAME_INVOKE_SCRIPT))(anchorHandler,
|
||||
tooltip);
|
||||
|
||||
uint32_t packed[2] = {static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32)};
|
||||
reinterpret_cast<void(__thiscall *)(void *, uint32_t *)>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GAMETOOLTIP_BUILD_UNIT))(tooltip, packed);
|
||||
}
|
||||
|
||||
void MouseoverTick() {
|
||||
const void *focus = CurrentMouseFocus();
|
||||
|
||||
@@ -423,6 +473,11 @@ void MouseoverTick() {
|
||||
|
||||
if (guid != g_lastSet) {
|
||||
CallEngineMouseover(guid); // sets slot + highlight + tooltip + event
|
||||
// Offline / far out-of-range party or raid member: the setter wrote the
|
||||
// GUID slot but skipped the tooltip (no live object). Build it from the
|
||||
// roster so hovering the frame still shows name / level / "Offline".
|
||||
if (guid != 0 && !GuidHasLiveUnit(guid))
|
||||
BuildRosterUnitTooltip(guid);
|
||||
g_lastSet = guid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
#include "event/Custom.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "net/PacketReader.h"
|
||||
#include "spell/CrowdControl.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
@@ -61,19 +62,15 @@ int NowMs() {
|
||||
reinterpret_cast<TickMs_t>(Offsets::FUN_OS_TICKCOUNT_MS)());
|
||||
}
|
||||
|
||||
// Spell.dbc field readers (mirrors the small locals in Spell::Cast).
|
||||
constexpr int OFF_NAME = 0x1E0; // localized name[9]
|
||||
constexpr int OFF_ICON_ID = 0x1D4; // -> SpellIcon.dbc
|
||||
|
||||
const char *SpellName(const uint8_t *rec) {
|
||||
const int locale = *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *n =
|
||||
*reinterpret_cast<const char *const *>(rec + OFF_NAME + locale * 4);
|
||||
*reinterpret_cast<const char *const *>(rec + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
return (n != nullptr) ? n : "";
|
||||
}
|
||||
|
||||
const char *SpellIconPath(const uint8_t *rec) {
|
||||
const int iconID = *reinterpret_cast<const int *>(rec + OFF_ICON_ID);
|
||||
const int iconID = *reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
const uint8_t *iconRec = DBC::Record(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT,
|
||||
static_cast<uint32_t>(iconID));
|
||||
@@ -87,30 +84,9 @@ int SpellSchoolIndex(const uint8_t *rec) {
|
||||
return *reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_SCHOOL);
|
||||
}
|
||||
|
||||
// Classify a spell's control-loss type from its EffectApplyAuraName array, or
|
||||
// null if it applies no control-loss aura. Priority-ordered (strongest control
|
||||
// first) so a multi-effect spell resolves to one locType, matching retail.
|
||||
const char *ClassifyControlLoss(const uint8_t *rec) {
|
||||
const int32_t *aura = reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
auto has = [&](int type) {
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i)
|
||||
if (aura[i] == type)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
if (has(Offsets::SPELL_AURA_MOD_POSSESS)) return "POSSESS";
|
||||
if (has(Offsets::SPELL_AURA_MOD_CHARM)) return "CHARM";
|
||||
if (has(Offsets::SPELL_AURA_MOD_STUN)) return "STUN";
|
||||
if (has(Offsets::SPELL_AURA_MOD_FEAR)) return "FEAR";
|
||||
if (has(Offsets::SPELL_AURA_MOD_CONFUSE)) return "CONFUSE";
|
||||
if (has(Offsets::SPELL_AURA_MOD_PACIFY_SILENCE)) return "PACIFYSILENCE";
|
||||
if (has(Offsets::SPELL_AURA_MOD_SILENCE)) return "SILENCE";
|
||||
if (has(Offsets::SPELL_AURA_MOD_PACIFY)) return "PACIFY";
|
||||
if (has(Offsets::SPELL_AURA_MOD_ROOT)) return "ROOT";
|
||||
if (has(Offsets::SPELL_AURA_MOD_DISARM)) return "DISARM";
|
||||
return nullptr;
|
||||
}
|
||||
// The control-loss classifier lives in `Spell::CrowdControl::Classify` so the
|
||||
// `C_UnitAuras` CROWD_CONTROL aura filter shares this hard-control set (that
|
||||
// filter additionally counts snares, which are crowd control but not LoC).
|
||||
|
||||
// ---- School-interrupt lockout state (fed by SMSG_SPELL_COOLDOWN) -----------
|
||||
|
||||
@@ -203,7 +179,7 @@ int BuildList(LocEntry *out, int maxOut) {
|
||||
const uint64_t playerGuid = Unit::Identity::PlayerGuid();
|
||||
for (int i = 1; n < maxOut; ++i) {
|
||||
const int slot = Aura::Data::FindNthSlot(
|
||||
player, i, Aura::Data::Filter::Harmful, false);
|
||||
player, i, Aura::Data::Filter::Harmful, Aura::Data::Match{});
|
||||
if (slot < 0)
|
||||
break;
|
||||
const uint32_t spellID = Aura::Data::ReadSpellID(player, slot);
|
||||
@@ -213,7 +189,7 @@ int BuildList(LocEntry *out, int maxOut) {
|
||||
Spell::Lookup::RecordForID(static_cast<int>(spellID));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
const char *locType = ClassifyControlLoss(rec);
|
||||
const char *locType = Spell::CrowdControl::Classify(rec);
|
||||
if (locType == nullptr)
|
||||
continue;
|
||||
|
||||
|
||||
+1
-2
@@ -46,7 +46,6 @@ namespace Macro::Spell {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int OFF_SPELL_RANK = 0x204; // 9-locale char *[9] in Spell.dbc
|
||||
|
||||
using MacroSlotToEntry_t = const uint8_t *(__fastcall *)(unsigned slot0Based);
|
||||
|
||||
@@ -88,7 +87,7 @@ int __fastcall Script_GetMacroSpell(void *L) {
|
||||
if (name == nullptr)
|
||||
return 0;
|
||||
const char *rank = *reinterpret_cast<const char *const *>(
|
||||
record + OFF_SPELL_RANK + locale * 4);
|
||||
record + Offsets::OFF_SPELL_RECORD_RANK + locale * 4);
|
||||
|
||||
Game::Lua::PushString(L, name);
|
||||
// Rank slot is empty for most non-class spells (racials, procs,
|
||||
|
||||
+10
-12
@@ -19,13 +19,13 @@
|
||||
// vs. on-next-swing abilities like Heroic Strike or Maul. We test
|
||||
// against the engine constant directly.
|
||||
//
|
||||
// Ranged — both vanilla ranged auto-attacks (`75` Auto Shot, `5019`
|
||||
// Shoot wand) set bit 1 of `Spell.dbc.Attributes` (the
|
||||
// `SPELL_ATTR_AUTO_REPEAT` flag at value `0x02`). Verified empirically
|
||||
// against the in-game cache: Attr `0x00050012` (Auto Shot) and
|
||||
// `0x00000012` (Shoot) share that bit; on-next-swing Heroic Strike
|
||||
// (`0x00050014`) and other ranged spells like Aimed Shot / Fireball do
|
||||
// NOT. The attribute check naturally extends to any future
|
||||
// Ranged — the two vanilla ranged auto-attacks (`75` Auto Shot, `5019`
|
||||
// Shoot wand) are exactly the spells carrying SPELL_ATTR_EX2_AUTOREPEAT_FLAG
|
||||
// (AttributesEx2 bit 5, `0x20`). Verified from Spell.dbc: Auto Shot and
|
||||
// Shoot have Ex2 `0x20`; the on-cast ranged abilities (Aimed Shot 19434,
|
||||
// Multi-Shot 2643) do NOT — they only set the generic RANGED bit
|
||||
// (Attributes bit 1), which is why an earlier RANGED test wrongly matched
|
||||
// them. The AUTOREPEAT flag is the exact signal and extends to any future
|
||||
// auto-repeating spell a private server might add.
|
||||
|
||||
#include "Lookup.h"
|
||||
@@ -42,8 +42,6 @@ namespace Spell::AutoAttack {
|
||||
namespace {
|
||||
|
||||
constexpr int SPELL_AUTO_ATTACK = 6603;
|
||||
constexpr int OFF_SPELL_ATTRIBUTES = 0x18;
|
||||
constexpr uint32_t SPELL_ATTR_AUTO_REPEAT = 0x00000002;
|
||||
|
||||
bool IsMelee(int spellID) {
|
||||
return spellID == SPELL_AUTO_ATTACK;
|
||||
@@ -57,9 +55,9 @@ bool IsRanged(int spellID) {
|
||||
static_cast<uint32_t>(spellID));
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
const uint32_t attr = *reinterpret_cast<const uint32_t *>(
|
||||
rec + OFF_SPELL_ATTRIBUTES);
|
||||
return (attr & SPELL_ATTR_AUTO_REPEAT) != 0;
|
||||
const uint32_t attrEx2 = *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
return (attrEx2 & Offsets::SPELL_ATTR_EX2_AUTOREPEAT_FLAG) != 0;
|
||||
}
|
||||
|
||||
int ReadSpellID(void *L) {
|
||||
|
||||
+10
-20
@@ -89,13 +89,6 @@ using TickMs_t = uint32_t(__fastcall *)();
|
||||
using GetCastTime_t = uint32_t(__fastcall *)(int spellID, int unit, int flag);
|
||||
using GetDuration_t = int(__fastcall *)(const uint8_t *spellRecord, int unit, int skipMod);
|
||||
|
||||
// Spell.dbc record field offsets (mirrors Spell::Info's locals).
|
||||
constexpr int OFF_NAME = 0x1E0; // localized name[9]
|
||||
constexpr int OFF_ICON_ID = 0x1D4; // -> SpellIcon.dbc
|
||||
constexpr int OFF_ATTRIBUTES = 0x18; // u32 Attributes flags
|
||||
constexpr int OFF_ATTRIBUTES_EX2 = 0x24; // u32 AttributesEx2 flags
|
||||
constexpr int OFF_SUBRECORD_VALUE = 0x04; // SpellIcon path field
|
||||
|
||||
// SPELL_ATTR_TRADESPELL — set on profession recipe casts (enchant,
|
||||
// cooking with a cast, etc.). The version-stable `isTradeskill` source:
|
||||
// 3.3.5's Script_UnitCastingInfo reads bit 0x20 of Attributes (at +0x10
|
||||
@@ -106,7 +99,6 @@ constexpr uint32_t SPELL_ATTR_TRADESPELL = 0x20;
|
||||
// flat +500ms "aim time" to these (SpellEntry::GetCastTime). Auto-repeat
|
||||
// shots (Auto Shot / Shoot, AttributesEx2 bit 0x20) are the one exclusion.
|
||||
constexpr uint32_t SPELL_ATTR_RANGED = 0x2;
|
||||
constexpr uint32_t SPELL_ATTR_EX2_AUTOREPEAT = 0x20;
|
||||
|
||||
struct TrackedSpell {
|
||||
int spellID; // 0 = not casting / channeling
|
||||
@@ -188,10 +180,10 @@ int CastTimeMs(int spellID) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec != nullptr) {
|
||||
const uint32_t attr =
|
||||
*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES);
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
const uint32_t attrEx2 =
|
||||
*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES_EX2);
|
||||
if ((attr & SPELL_ATTR_RANGED) && !(attrEx2 & SPELL_ATTR_EX2_AUTOREPEAT))
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
if ((attr & SPELL_ATTR_RANGED) && !(attrEx2 & Offsets::SPELL_ATTR_EX2_AUTOREPEAT_FLAG))
|
||||
ms += 500;
|
||||
}
|
||||
}
|
||||
@@ -212,24 +204,25 @@ int ChannelDurationMs(int spellID) {
|
||||
|
||||
const char *SpellName(const uint8_t *rec) {
|
||||
const int locale = *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
return *reinterpret_cast<const char *const *>(rec + OFF_NAME + locale * 4);
|
||||
return *reinterpret_cast<const char *const *>(rec + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
}
|
||||
|
||||
bool IsTradeskill(const uint8_t *rec) {
|
||||
return (*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES) & SPELL_ATTR_TRADESPELL) != 0;
|
||||
return (*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
|
||||
SPELL_ATTR_TRADESPELL) != 0;
|
||||
}
|
||||
|
||||
// Spell icon texture path, or "" if there's no icon record. SpellIcon.dbc
|
||||
// already stores the full "Interface\Icons\..." path (unlike item icons,
|
||||
// which are bare filenames) — so it's used verbatim, no prefix.
|
||||
const char *SpellIconPath(const uint8_t *rec) {
|
||||
const int iconID = *reinterpret_cast<const int *>(rec + OFF_ICON_ID);
|
||||
const int iconID = *reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
const uint8_t *iconRec = DBC::Record(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT,
|
||||
static_cast<uint32_t>(iconID));
|
||||
if (iconRec == nullptr)
|
||||
return "";
|
||||
const char *path = *reinterpret_cast<const char *const *>(iconRec + OFF_SUBRECORD_VALUE);
|
||||
const char *path = *reinterpret_cast<const char *const *>(iconRec + Offsets::OFF_SPELLICON_PATH);
|
||||
return (path != nullptr) ? path : "";
|
||||
}
|
||||
|
||||
@@ -340,12 +333,9 @@ const Game::HookAutoRegister _castStartHook{
|
||||
// thing at a time). Regular casts back `UnitCastingInfo`; channels add real
|
||||
// times to `UnitChannelInfo` (validated against the live +0x228 field).
|
||||
|
||||
constexpr int OFF_ATTRIBUTES_EX = 0x1C; // Spell.dbc AttributesEx
|
||||
constexpr uint32_t SPELL_ATTR_EX_CHANNELED = 0x4 | 0x40; // IS_CHANNELED | SELF
|
||||
|
||||
bool IsChannelSpell(const uint8_t *rec) {
|
||||
return (*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES_EX) &
|
||||
SPELL_ATTR_EX_CHANNELED) != 0;
|
||||
return (*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX) &
|
||||
Offsets::SPELL_ATTR_EX_CHANNELED) != 0;
|
||||
}
|
||||
|
||||
// Channel duration for a non-player caster — base (skipMod=1), since we
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
namespace Spell::CastByID {
|
||||
|
||||
// Offset of the locale-resolved Name[9] table inside a Spell.dbc record.
|
||||
static constexpr int OFF_NAME = 0x1E0;
|
||||
|
||||
using NameToSlot_t = int(__fastcall *)(const char *name, void *out);
|
||||
static NameToSlot_t NameToSlot_o = nullptr;
|
||||
@@ -48,7 +46,7 @@ static const char *LocaleNameForID(int spellID) {
|
||||
return nullptr;
|
||||
const int locale = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
return *reinterpret_cast<const char *const *>(record + OFF_NAME + locale * 4);
|
||||
return *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
}
|
||||
|
||||
// Hook on the single name → spellbook-slot resolver
|
||||
|
||||
+10
-17
@@ -29,9 +29,6 @@ namespace Spell::CastEvents {
|
||||
|
||||
namespace {
|
||||
|
||||
// Spell.dbc localized string arrays (9 locale slots, 4 bytes each).
|
||||
constexpr int OFF_NAME = 0x1E0; // Name[9]
|
||||
constexpr int OFF_RANK = 0x204; // Rank[9]
|
||||
|
||||
constexpr const char *kStart = "UNIT_SPELLCAST_START";
|
||||
constexpr const char *kStop = "UNIT_SPELLCAST_STOP";
|
||||
@@ -120,8 +117,8 @@ void Fire(const char *eventName, int spellID, int guidNum, int type = 3) {
|
||||
char castGuid[48];
|
||||
BuildCastGuid(castGuid, sizeof(castGuid), type, spellID, guidNum);
|
||||
Event::Custom::Fire(slot, "%s%s%d%s%s", "player", castGuid, spellID,
|
||||
LocalizedField(rec, OFF_NAME),
|
||||
LocalizedField(rec, OFF_RANK));
|
||||
LocalizedField(rec, Offsets::OFF_SPELL_NAMES),
|
||||
LocalizedField(rec, Offsets::OFF_SPELL_RECORD_RANK));
|
||||
}
|
||||
|
||||
// SENT is the one event whose shape differs from the rest, matching modern:
|
||||
@@ -138,8 +135,8 @@ void FireSent(int spellID, int guidNum, const char *target) {
|
||||
char castGuid[48];
|
||||
BuildCastGuid(castGuid, sizeof(castGuid), /*type=*/3, spellID, guidNum);
|
||||
Event::Custom::Fire(slot, "%s%s%s%d%s%s", "player", target ? target : "",
|
||||
castGuid, spellID, LocalizedField(rec, OFF_NAME),
|
||||
LocalizedField(rec, OFF_RANK));
|
||||
castGuid, spellID, LocalizedField(rec, Offsets::OFF_SPELL_NAMES),
|
||||
LocalizedField(rec, Offsets::OFF_SPELL_RECORD_RANK));
|
||||
}
|
||||
|
||||
// Fire a RETICLE event `(unitTarget, castGUID, spellID, spellName, rank)`.
|
||||
@@ -158,8 +155,8 @@ void FireReticle(const char *eventName, int spellID) {
|
||||
if (rec == nullptr)
|
||||
return;
|
||||
Event::Custom::Fire(slot, "%s%s%d%s%s", "player", "", spellID,
|
||||
LocalizedField(rec, OFF_NAME),
|
||||
LocalizedField(rec, OFF_RANK));
|
||||
LocalizedField(rec, Offsets::OFF_SPELL_NAMES),
|
||||
LocalizedField(rec, Offsets::OFF_SPELL_RECORD_RANK));
|
||||
}
|
||||
|
||||
// spellID whose reticle we last reported active (0 = no reticle up).
|
||||
@@ -257,15 +254,11 @@ int g_pendingChanSuccSpell = 0;
|
||||
int g_pendingChanSuccTMs = 0;
|
||||
constexpr int kChanSuccDeferMs = 500;
|
||||
|
||||
// Spell.dbc AttributesEx channel bits — same test Spell::Cast uses.
|
||||
constexpr int OFF_ATTRIBUTES_EX = 0x1C;
|
||||
constexpr uint32_t SPELL_ATTR_EX_CHANNELED = 0x4 | 0x40; // IS_CHANNELED | SELF
|
||||
|
||||
bool IsChanneledSpell(int spellID) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
return rec != nullptr &&
|
||||
(*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES_EX) &
|
||||
SPELL_ATTR_EX_CHANNELED) != 0;
|
||||
(*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX) &
|
||||
Offsets::SPELL_ATTR_EX_CHANNELED) != 0;
|
||||
}
|
||||
|
||||
int NowMs() {
|
||||
@@ -390,8 +383,8 @@ void FireRemote(const char *eventName, uint64_t casterGuid, int spellID,
|
||||
return;
|
||||
char castGuid[48];
|
||||
BuildCastGuid(castGuid, sizeof(castGuid), /*type=*/3, spellID, guidNum);
|
||||
const char *name = LocalizedField(rec, OFF_NAME);
|
||||
const char *rank = LocalizedField(rec, OFF_RANK);
|
||||
const char *name = LocalizedField(rec, Offsets::OFF_SPELL_NAMES);
|
||||
const char *rank = LocalizedField(rec, Offsets::OFF_SPELL_RECORD_RANK);
|
||||
for (int i = 0; i < n; ++i)
|
||||
Event::Custom::Fire(slot, "%s%s%d%s%s", tokens[i], castGuid, spellID,
|
||||
name, rank);
|
||||
|
||||
@@ -52,10 +52,6 @@ namespace Spell::CastNoToggle {
|
||||
|
||||
namespace {
|
||||
|
||||
// Offset of the locale-resolved Name[9] table inside a Spell.dbc record
|
||||
// (same value used by `Spell::Info`; duplicated here to keep this module
|
||||
// header-free).
|
||||
constexpr int OFF_NAME = 0x1E0;
|
||||
|
||||
using CastSpellByName_t = int(__fastcall *)(void *L);
|
||||
using NameToSlot_t = int(__fastcall *)(const char *name, int *outBookType);
|
||||
@@ -75,7 +71,7 @@ const char *LocaleName(int spellID) {
|
||||
return nullptr;
|
||||
const int locale = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
return *reinterpret_cast<const char *const *>(record + OFF_NAME + locale * 4);
|
||||
return *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
}
|
||||
|
||||
// Case-insensitive name match for the auto-repeat-name gate. Walks
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "spell/CrowdControl.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "spell/Lookup.h"
|
||||
|
||||
namespace Spell::CrowdControl {
|
||||
|
||||
const char *Classify(const uint8_t *rec) {
|
||||
if (rec == nullptr)
|
||||
return nullptr;
|
||||
const int32_t *aura = reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
auto has = [&](int type) {
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i)
|
||||
if (aura[i] == type)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
if (has(Offsets::SPELL_AURA_MOD_POSSESS)) return "POSSESS";
|
||||
if (has(Offsets::SPELL_AURA_MOD_CHARM)) return "CHARM";
|
||||
if (has(Offsets::SPELL_AURA_MOD_STUN)) return "STUN";
|
||||
if (has(Offsets::SPELL_AURA_MOD_FEAR)) return "FEAR";
|
||||
if (has(Offsets::SPELL_AURA_MOD_CONFUSE)) return "CONFUSE";
|
||||
if (has(Offsets::SPELL_AURA_MOD_PACIFY_SILENCE)) return "PACIFYSILENCE";
|
||||
if (has(Offsets::SPELL_AURA_MOD_SILENCE)) return "SILENCE";
|
||||
if (has(Offsets::SPELL_AURA_MOD_PACIFY)) return "PACIFY";
|
||||
if (has(Offsets::SPELL_AURA_MOD_ROOT)) return "ROOT";
|
||||
if (has(Offsets::SPELL_AURA_MOD_DISARM)) return "DISARM";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsCrowdControl(uint32_t spellID) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellID));
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
// Crowd control is broader than loss of control: it also counts movement
|
||||
// slows / snares, which `Classify` (the LoC set) deliberately excludes.
|
||||
if (Classify(rec) != nullptr)
|
||||
return true;
|
||||
const int32_t *aura = reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i)
|
||||
if (aura[i] == Offsets::SPELL_AURA_MOD_DECREASE_SPEED)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Spell::CrowdControl
|
||||
@@ -0,0 +1,40 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Crowd-control classification of a Spell.dbc record, shared by
|
||||
// `C_LossOfControl` (which reports the control-loss type) and the
|
||||
// `C_UnitAuras` `CROWD_CONTROL` aura filter (which only needs presence). One
|
||||
// source of truth for "is this a hard control effect" so the two never drift.
|
||||
|
||||
namespace Spell::CrowdControl {
|
||||
|
||||
// Classifies a spell by its `EffectApplyAuraName` set into a control-loss
|
||||
// type string, priority-ordered (strongest control first) so a multi-effect
|
||||
// spell resolves to one type:
|
||||
// POSSESS, CHARM, STUN, FEAR, CONFUSE, PACIFYSILENCE, SILENCE, PACIFY,
|
||||
// ROOT, DISARM
|
||||
// Returns nullptr if the spell applies no control-loss aura (`spellRecord`
|
||||
// may be null → nullptr). Movement-only slows/snares are intentionally NOT
|
||||
// classified — they are not a loss of control.
|
||||
const char *Classify(const uint8_t *spellRecord);
|
||||
|
||||
// True iff `spellID` is a crowd-control aura. Broader than `Classify`: it
|
||||
// also counts movement slows / snares (`MOD_DECREASE_SPEED`), which are crowd
|
||||
// control but not loss of control. Backs the C_UnitAuras CROWD_CONTROL filter.
|
||||
bool IsCrowdControl(uint32_t spellID);
|
||||
|
||||
} // namespace Spell::CrowdControl
|
||||
+28
-39
@@ -22,24 +22,14 @@
|
||||
|
||||
namespace Spell::Info {
|
||||
|
||||
// Spell.dbc record offsets (from BuildSpellTooltip / Script_GetSpellName / Script_GetSpellTexture).
|
||||
static constexpr int OFF_ATTRIBUTES_EX = 0x1C;
|
||||
static constexpr int OFF_CASTING_TIME_INDEX = 0x48;
|
||||
static constexpr int OFF_POWER_TYPE = 0x7C;
|
||||
static constexpr int OFF_MANA_COST = 0x80;
|
||||
static constexpr int OFF_RANGE_INDEX = 0x90;
|
||||
static constexpr int OFF_ICON_ID = 0x1D4;
|
||||
static constexpr int OFF_NAME = 0x1E0;
|
||||
static constexpr int OFF_RANK = 0x204;
|
||||
|
||||
// Spell.dbc Attributes (+0x18) and AttributesEx (+0x1C) flag bits we read.
|
||||
// Both bits are 0x40, but on different fields:
|
||||
// Attributes bit 6 = SPELL_ATTR_PASSIVE — passive spell (no cast bar,
|
||||
// applies its effect as soon as learned/equipped)
|
||||
// AttributesEx bit 6 = SPELL_ATTR_EX_FUNNEL_PERCENT — funnel channel
|
||||
static constexpr int OFF_ATTRIBUTES = 0x18;
|
||||
static constexpr uint32_t SPELL_ATTR_PASSIVE = 0x40;
|
||||
static constexpr uint32_t SPELL_ATTR_EX_FUNNEL = 0x40;
|
||||
// GetSpellInfo's isFunnel means a health-funnel spell — AttributesEx2 (+0x20)
|
||||
// bit 11 = SPELL_ATTR_EX2_HEALTH_FUNNEL. Verified from Spell.dbc: Health
|
||||
// Funnel (755) Ex2=0x808 and Hellfire (1949) Ex2=0x800 carry it, while
|
||||
// channeled non-funnels (Drain Life, Mind Flay, Arcane Missiles, Rain of
|
||||
// Fire) do not. An earlier version read AttributesEx bit 6, which is
|
||||
// CHANNELED_2 — false for Health Funnel itself and true for unrelated
|
||||
// channels, so it never actually detected funnel spells.
|
||||
static constexpr uint32_t SPELL_ATTR_EX2_HEALTH_FUNNEL = 0x800;
|
||||
// Spell.dbc effect-target arrays. Each spell has 3 effects, each
|
||||
// with an implicit target A and an implicit target B (the latter
|
||||
// often 0). Used by IsSpellHarmful / IsSpellHelpful — vanilla has
|
||||
@@ -48,9 +38,6 @@ static constexpr uint32_t SPELL_ATTR_EX_FUNNEL = 0x40;
|
||||
// have it), so we classify by walking effect targets and checking
|
||||
// whether any falls into a known hostile-target or friendly-target
|
||||
// set. Same algorithm CMaNGOS uses in `SpellMgr::IsPositiveSpell`.
|
||||
static constexpr int OFF_EFFECT_IMPLICIT_TARGET_A = 0x148; // uint32[3]
|
||||
static constexpr int OFF_EFFECT_IMPLICIT_TARGET_B = 0x154; // uint32[3]
|
||||
|
||||
// Target IDs from vanilla 1.12's `SpellTarget` enum that mark a
|
||||
// spell as hostile-targeted (covers single-target damage,
|
||||
// debuffs, AoE damage, etc.). List is conservative — anything not
|
||||
@@ -142,25 +129,26 @@ static bool ReadSpellInfo(int spellID, SpellInfoData &out) {
|
||||
const int locale = ReadGlobal<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
|
||||
out.spellID = spellID;
|
||||
out.name = *reinterpret_cast<const char *const *>(record + OFF_NAME + locale * 4);
|
||||
out.rank = *reinterpret_cast<const char *const *>(record + OFF_RANK + locale * 4);
|
||||
out.name = *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
out.rank = *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_RECORD_RANK + locale * 4);
|
||||
|
||||
out.iconPath = nullptr;
|
||||
const int iconID = *reinterpret_cast<const int *>(record + OFF_ICON_ID);
|
||||
const int iconID = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
if (auto *iconRec = LookupSubRecord(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT, iconID)) {
|
||||
out.iconPath = *reinterpret_cast<const char *const *>(iconRec + 4);
|
||||
out.iconPath = *reinterpret_cast<const char *const *>(iconRec + Offsets::OFF_SPELLICON_PATH);
|
||||
}
|
||||
|
||||
out.cost = *reinterpret_cast<const int *>(record + OFF_MANA_COST);
|
||||
out.cost = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_MANA_COST);
|
||||
|
||||
const uint32_t attrEx = *reinterpret_cast<const uint32_t *>(record + OFF_ATTRIBUTES_EX);
|
||||
out.isFunnel = (attrEx & SPELL_ATTR_EX_FUNNEL) != 0;
|
||||
const uint32_t attrEx2 = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
out.isFunnel = (attrEx2 & SPELL_ATTR_EX2_HEALTH_FUNNEL) != 0;
|
||||
|
||||
out.powerType = *reinterpret_cast<const int *>(record + OFF_POWER_TYPE);
|
||||
out.powerType = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_POWER_TYPE);
|
||||
|
||||
out.castTimeMs = 0;
|
||||
const int castIndex = *reinterpret_cast<const int *>(record + OFF_CASTING_TIME_INDEX);
|
||||
const int castIndex = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_CASTING_TIME_INDEX);
|
||||
if (auto *castRec = LookupSubRecord(Offsets::VAR_SPELL_CAST_TIMES_RECORDS,
|
||||
Offsets::VAR_SPELL_CAST_TIMES_COUNT, castIndex)) {
|
||||
out.castTimeMs = *reinterpret_cast<const int *>(castRec + 4);
|
||||
@@ -168,7 +156,7 @@ static bool ReadSpellInfo(int spellID, SpellInfoData &out) {
|
||||
|
||||
out.minRange = 0.0f;
|
||||
out.maxRange = 0.0f;
|
||||
const int rangeIndex = *reinterpret_cast<const int *>(record + OFF_RANGE_INDEX);
|
||||
const int rangeIndex = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_RANGE_INDEX);
|
||||
if (auto *rangeRec = LookupSubRecord(Offsets::VAR_SPELL_RANGE_RECORDS,
|
||||
Offsets::VAR_SPELL_RANGE_COUNT, rangeIndex)) {
|
||||
out.minRange = *reinterpret_cast<const float *>(rangeRec + 4);
|
||||
@@ -288,7 +276,7 @@ static int __fastcall Script_C_GetSpellName(void *L) {
|
||||
return 0; // nil for unknown spellID
|
||||
|
||||
const int locale = ReadGlobal<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *name = *reinterpret_cast<const char *const *>(record + OFF_NAME + locale * 4);
|
||||
const char *name = *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (name == nullptr || *name == '\0')
|
||||
return 0; // empty / no name in current locale → nil
|
||||
Game::Lua::PushString(L, name);
|
||||
@@ -301,12 +289,12 @@ static int __fastcall Script_C_GetSpellTexture(void *L) {
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
|
||||
const int iconID = *reinterpret_cast<const int *>(record + OFF_ICON_ID);
|
||||
const int iconID = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
auto *iconRec = LookupSubRecord(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT, iconID);
|
||||
if (iconRec == nullptr)
|
||||
return 0;
|
||||
const char *path = *reinterpret_cast<const char *const *>(iconRec + 4);
|
||||
const char *path = *reinterpret_cast<const char *const *>(iconRec + Offsets::OFF_SPELLICON_PATH);
|
||||
if (path == nullptr || *path == '\0')
|
||||
return 0;
|
||||
Game::Lua::PushString(L, path);
|
||||
@@ -327,7 +315,7 @@ static bool BuildSpellLink(int spellID, char *out, size_t outLen) {
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
const int locale = ReadGlobal<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *name = *reinterpret_cast<const char *const *>(record + OFF_NAME + locale * 4);
|
||||
const char *name = *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (name == nullptr || *name == '\0')
|
||||
return false;
|
||||
const int n = std::snprintf(out, outLen, "|cff71d5ff|Hspell:%d:0|h[%s]|h|r",
|
||||
@@ -404,8 +392,9 @@ static int PushIsPassive(void *L, int spellID) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
const uint32_t attr = *reinterpret_cast<const uint32_t *>(record + OFF_ATTRIBUTES);
|
||||
Game::Lua::PushBool(L, (attr & SPELL_ATTR_PASSIVE) != 0);
|
||||
const uint32_t attr = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
Game::Lua::PushBool(L, (attr & Offsets::SPELL_ATTR_PASSIVE) != 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -534,9 +523,9 @@ static bool AnyEffectTarget(const uint8_t *record, Pred pred) {
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
auto *targetsA = reinterpret_cast<const uint32_t *>(
|
||||
record + OFF_EFFECT_IMPLICIT_TARGET_A);
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A);
|
||||
auto *targetsB = reinterpret_cast<const uint32_t *>(
|
||||
record + OFF_EFFECT_IMPLICIT_TARGET_B);
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_B);
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (pred(targetsA[i]) || pred(targetsB[i]))
|
||||
return true;
|
||||
|
||||
+32
-33
@@ -16,7 +16,11 @@
|
||||
// `Spell.dbc.EffectImplicitTargetA[3]` and `EffectImplicitTargetB[3]`
|
||||
// — an effect is "self-only" when both targets are either
|
||||
// `TARGET_NONE` (0) or `TARGET_SELF` (1). Effects with a zero
|
||||
// `Effect` code are skipped (unused effect slots).
|
||||
// `Effect` code are skipped (unused effect slots). The record-scan core
|
||||
// is `Spell::IsSelfBuff::IsSelfBuff` (see the header) so the aura layer
|
||||
// can share it for sourceGUID inference.
|
||||
|
||||
#include "spell/IsSelfBuff.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -31,9 +35,6 @@ namespace {
|
||||
// Spell.dbc per-effect arrays. Each is an int32[3] in the record.
|
||||
// Field-index derivation against the known `EffectApplyAuraName` at
|
||||
// field 91 / +0x16C — the implicit-target pair sits at field 82/85.
|
||||
constexpr int OFF_SPELL_EFFECT = 0xF4; // SPELL_EFFECT_* code
|
||||
constexpr int OFF_SPELL_EFFECT_TARGET_A = 0x148;
|
||||
constexpr int OFF_SPELL_EFFECT_TARGET_B = 0x154;
|
||||
constexpr int EFFECT_COUNT = 3;
|
||||
|
||||
// Vanilla 1.12 implicit-target codes (CMaNGOS `Targets` enum).
|
||||
@@ -46,41 +47,39 @@ bool IsSelfOnlyTarget(int target) {
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsSelfBuff(uint32_t spellID) {
|
||||
if (spellID == 0)
|
||||
return false;
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT, spellID);
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
|
||||
bool sawEffect = false;
|
||||
for (int i = 0; i < EFFECT_COUNT; ++i) {
|
||||
const int effect = *reinterpret_cast<const int *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT + i * 4);
|
||||
if (effect == 0)
|
||||
continue;
|
||||
sawEffect = true;
|
||||
const int targetA = *reinterpret_cast<const int *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A + i * 4);
|
||||
const int targetB = *reinterpret_cast<const int *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_B + i * 4);
|
||||
if (!IsSelfOnlyTarget(targetA) || !IsSelfOnlyTarget(targetB))
|
||||
return false;
|
||||
}
|
||||
return sawEffect;
|
||||
}
|
||||
|
||||
static int __fastcall Script_IsSelfBuff(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (spellID <= 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT,
|
||||
static_cast<uint32_t>(spellID));
|
||||
if (rec == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool sawEffect = false;
|
||||
for (int i = 0; i < EFFECT_COUNT; ++i) {
|
||||
const int effect = *reinterpret_cast<const int *>(
|
||||
rec + OFF_SPELL_EFFECT + i * 4);
|
||||
if (effect == 0)
|
||||
continue;
|
||||
sawEffect = true;
|
||||
const int targetA = *reinterpret_cast<const int *>(
|
||||
rec + OFF_SPELL_EFFECT_TARGET_A + i * 4);
|
||||
const int targetB = *reinterpret_cast<const int *>(
|
||||
rec + OFF_SPELL_EFFECT_TARGET_B + i * 4);
|
||||
if (!IsSelfOnlyTarget(targetA) || !IsSelfOnlyTarget(targetB)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Game::Lua::PushBool(L, sawEffect);
|
||||
Game::Lua::PushBool(
|
||||
L, spellID > 0 && IsSelfBuff(static_cast<uint32_t>(spellID)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Spell::IsSelfBuff {
|
||||
|
||||
// True iff every active effect of `spellID` targets only the caster — each
|
||||
// effect's implicit target A and B is `TARGET_NONE` (0) or `TARGET_SELF` (1),
|
||||
// read from `Spell.dbc.EffectImplicitTargetA/B[3]`. Such an aura can only have
|
||||
// been self-cast, so a unit carrying one is definitionally its own source.
|
||||
// False for an unknown spell or one with no active effect. Shared by the
|
||||
// `C_Spell.IsSelfBuff` Lua binding and the `C_UnitAuras` sourceGUID inference.
|
||||
bool IsSelfBuff(uint32_t spellID);
|
||||
|
||||
} // namespace Spell::IsSelfBuff
|
||||
+32
-43
@@ -84,37 +84,35 @@ int SpellBaseLevel(int spellID) {
|
||||
record + Offsets::OFF_SPELL_RECORD_BASE_LEVEL);
|
||||
}
|
||||
|
||||
// Spell.dbc attribute fields/bits used to filter learnable-spell
|
||||
// results down to what's actually user-facing.
|
||||
// Spell.dbc attribute filter for the learnable-spell walk. Only the
|
||||
// verified HIDDEN_CLIENTSIDE bit is used:
|
||||
//
|
||||
// Attributes (+0x18)
|
||||
// bit 0x80 = HIDDEN_CLIENTSIDE — engine-internal helpers like
|
||||
// "Defensive Stance Passive" (7376) that pair with
|
||||
// a user-visible spell (Defensive Stance, 71) and
|
||||
// never appear in the spellbook. Skip these.
|
||||
// Attributes (+0x18) bit 0x80 = HIDDEN_CLIENTSIDE — engine-internal
|
||||
// helpers like "Defensive Stance Passive" (7376) that pair with a
|
||||
// user-visible spell (Defensive Stance, 71) and never appear in the
|
||||
// spellbook. Skip these.
|
||||
//
|
||||
// AttributesEx2 (+0x24)
|
||||
// bit 0x02000000 = HIDE_FROM_AUTOLEARN — the same gate Cata's
|
||||
// GetCurrentLevelSpells checks
|
||||
// (FUN_00911c60's `(spellData[+0x24] &
|
||||
// 0x02000000) == 0`). Catches proc-only and
|
||||
// engine-internal spells with a BaseLevel.
|
||||
constexpr int OFF_SPELL_RECORD_ATTRIBUTES = 0x18;
|
||||
constexpr int OFF_SPELL_RECORD_ATTRIBUTES_EX2 = 0x24;
|
||||
// A prior version also tested AttributesEx3 (+0x24) bit 0x02000000 as a
|
||||
// "hide from auto-learn" gate ported from a MoP function (FUN_00911c60).
|
||||
// That was invalid on every count: FUN_00911c60 is outside 1.12's .text
|
||||
// and its `+0x24` is not 1.12's AttributesEx3. Verified against Spell.dbc,
|
||||
// the 1.12 bit is a sparse grab-bag (44 / 27925 records: passive procs,
|
||||
// odd DoT ranks, the Night Elf priest Starshards line, Turtle customs) —
|
||||
// not an auto-learn flag — so it could only wrongly hide legitimate
|
||||
// learnable spells (Starshards carries it). Removed. The authoritative
|
||||
// auto-learn signal is SkillLineAbility.learnOnGetSkill (col 9: 2 =
|
||||
// race/class, 1 = profession, 0 = trainer-taught), not a Spell.dbc bit;
|
||||
// this feature deliberately reports the broader "trainable at level" set,
|
||||
// so it applies no learnOnGetSkill filter.
|
||||
constexpr uint32_t SPELL_ATTR_HIDDEN_CLIENTSIDE = 0x80;
|
||||
constexpr uint32_t SPELL_ATTR_EX2_HIDE_FROM_AUTOLEARN = 0x02000000;
|
||||
|
||||
bool SpellHiddenFromAutoLearn(int spellID) {
|
||||
bool SpellHiddenFromSpellbook(int spellID) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
const uint32_t attributes = *reinterpret_cast<const uint32_t *>(
|
||||
record + OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
if ((attributes & SPELL_ATTR_HIDDEN_CLIENTSIDE) != 0)
|
||||
return true;
|
||||
const uint32_t attrEx2 = *reinterpret_cast<const uint32_t *>(
|
||||
record + OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
return (attrEx2 & SPELL_ATTR_EX2_HIDE_FROM_AUTOLEARN) != 0;
|
||||
record + Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
return (attributes & SPELL_ATTR_HIDDEN_CLIENTSIDE) != 0;
|
||||
}
|
||||
|
||||
// ---- Target-level gate for ranked beneficial auras ----------------------
|
||||
@@ -132,12 +130,6 @@ bool SpellHiddenFromAutoLearn(int spellID) {
|
||||
// checks are NOT in the 1.12 client's Spell.dbc — server-only columns — so
|
||||
// the spellLevel rule is the only client-readable target-level mechanism.)
|
||||
|
||||
constexpr int OFF_SPELL_MAX_LEVEL = 0x6C; // uint32 (scaling cap level)
|
||||
constexpr int OFF_SPELL_SPELL_LEVEL = 0x74; // uint32 (rank's effective level)
|
||||
constexpr int OFF_SPELL_EFFECT = 0xF4; // uint32[3] effect type
|
||||
constexpr int OFF_SPELL_EFFECT_TARGET_A = 0x148; // uint32[3] implicit target A
|
||||
constexpr int OFF_SPELL_RANK = 0x204; // char*[9] localized rank text
|
||||
constexpr uint32_t SPELL_ATTR_PASSIVE = 0x40; // Attributes bit
|
||||
constexpr uint32_t SPELL_EFFECT_APPLY_AURA = 6;
|
||||
constexpr uint32_t SPELL_EFFECT_APPLY_AREA_AURA_PARTY = 35;
|
||||
|
||||
@@ -178,7 +170,7 @@ bool IsPositiveRankTarget(uint32_t t) {
|
||||
// empty Rank and un-gated) vs ranked ones (Divine Spirit / Fortitude).
|
||||
bool HasRankString(const uint8_t *record) {
|
||||
const char *rank = *reinterpret_cast<const char *const *>(
|
||||
record + OFF_SPELL_RANK + LocaleIndex() * 4);
|
||||
record + Offsets::OFF_SPELL_RECORD_RANK + LocaleIndex() * 4);
|
||||
return rank != nullptr && *rank != '\0';
|
||||
}
|
||||
|
||||
@@ -186,19 +178,19 @@ bool HasRankString(const uint8_t *record) {
|
||||
// the spell isn't subject to the ranked-positive-aura target-level rule.
|
||||
int RequiredTargetLevel(const uint8_t *record) {
|
||||
const int spellLevel =
|
||||
*reinterpret_cast<const int *>(record + OFF_SPELL_SPELL_LEVEL);
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_SPELL_LEVEL);
|
||||
if (spellLevel <= 10) // targetLevel + 10 >= spellLevel holds for any level
|
||||
return 0;
|
||||
const uint32_t attr =
|
||||
*reinterpret_cast<const uint32_t *>(record + OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
if (attr & SPELL_ATTR_PASSIVE)
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
if (attr & Offsets::SPELL_ATTR_PASSIVE)
|
||||
return 0;
|
||||
if (!HasRankString(record)) // single-rank spell → not gated
|
||||
return 0;
|
||||
|
||||
auto *effect = reinterpret_cast<const uint32_t *>(record + OFF_SPELL_EFFECT);
|
||||
auto *effect = reinterpret_cast<const uint32_t *>(record + Offsets::OFF_SPELL_RECORD_EFFECT);
|
||||
auto *targetA =
|
||||
reinterpret_cast<const uint32_t *>(record + OFF_SPELL_EFFECT_TARGET_A);
|
||||
reinterpret_cast<const uint32_t *>(record + Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A);
|
||||
bool positiveAura = false;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if ((effect[i] == SPELL_EFFECT_APPLY_AURA &&
|
||||
@@ -229,13 +221,13 @@ int __fastcall Script_GetSpellLevelInfo(void *L) {
|
||||
return 0;
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + OFF_SPELL_SPELL_LEVEL)));
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_SPELL_LEVEL)));
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_BASE_LEVEL)));
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + OFF_SPELL_MAX_LEVEL)));
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_MAX_LEVEL)));
|
||||
return 3;
|
||||
}
|
||||
|
||||
@@ -425,12 +417,9 @@ int __fastcall Script_GetCurrentLevelSpells(void *L) {
|
||||
// GetCurrentLevelSpells excludes them too.
|
||||
if (TalentSpellSet().count(spellID) != 0)
|
||||
continue;
|
||||
// Skip spells flagged "hide from auto-learn list" — the
|
||||
// same AttributesEx2 bit Cata's GetCurrentLevelSpells
|
||||
// checks (FUN_00911c60). Catches engine-internal or proc-
|
||||
// only spells that have a BaseLevel but aren't meant to
|
||||
// surface as learnable.
|
||||
if (SpellHiddenFromAutoLearn(spellID))
|
||||
// Skip engine-internal spellbook-hidden spells (HIDDEN_CLIENTSIDE):
|
||||
// they can carry a BaseLevel but never surface as learnable.
|
||||
if (SpellHiddenFromSpellbook(spellID))
|
||||
continue;
|
||||
if (!seen.insert(spellID).second)
|
||||
continue;
|
||||
|
||||
+4
-3
@@ -51,9 +51,10 @@ float Apply(const uint8_t *spellRecord, int op, float base) {
|
||||
|
||||
const uint32_t familyName = *reinterpret_cast<const uint32_t *>(
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_FAMILY_NAME);
|
||||
const uint32_t attrEx2 = *reinterpret_cast<const uint32_t *>(
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
if (familyName == 0 || (attrEx2 & Offsets::SPELL_ATTR_EX2_NO_SPELL_MODS) != 0)
|
||||
const uint32_t attrEx3 = *reinterpret_cast<const uint32_t *>(
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX3);
|
||||
if (familyName == 0 ||
|
||||
(attrEx3 & Offsets::SPELL_ATTR_EX3_IGNORE_CASTER_MODIFIERS) != 0)
|
||||
return base;
|
||||
if (familyName != PlayerSpellFamily())
|
||||
return base;
|
||||
|
||||
@@ -49,8 +49,6 @@ namespace {
|
||||
|
||||
using GetSpellCost_t = uint32_t(__fastcall *)(int spellID, int unit);
|
||||
|
||||
constexpr int OFF_POWER_TYPE = 0x7C; // int PowerType
|
||||
constexpr int OFF_MANA_COST_PERCENT = 0x270; // int, % of base resource (0 = flat)
|
||||
|
||||
uint32_t EffectiveCost(int spellID) {
|
||||
return reinterpret_cast<GetSpellCost_t>(Offsets::FUN_GET_SPELL_COST)(spellID, 0);
|
||||
@@ -66,8 +64,8 @@ int __fastcall Script_GetSpellPowerCost(void *L) {
|
||||
if (cost == 0xFFFFFFFF)
|
||||
return 0; // engine error (no player context, etc.)
|
||||
|
||||
const int powerType = *reinterpret_cast<const int *>(rec + OFF_POWER_TYPE);
|
||||
const int costPercent = *reinterpret_cast<const int *>(rec + OFF_MANA_COST_PERCENT);
|
||||
const int powerType = *reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_RECORD_POWER_TYPE);
|
||||
const int costPercent = *reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_RECORD_MANA_COST_PERCENT);
|
||||
if (cost == 0 && costPercent == 0)
|
||||
return 0; // nil — no resource cost
|
||||
|
||||
|
||||
+1
-2
@@ -23,7 +23,6 @@ namespace {
|
||||
|
||||
// Spell.dbc RangeIndex + SpellRange.dbc min/max offsets (verified,
|
||||
// documented in CLAUDE.md under "Sub-DBC record layouts").
|
||||
constexpr int OFF_SPELL_RANGE_INDEX = 0x90;
|
||||
constexpr int OFF_SPELLRANGE_MIN = 0x04;
|
||||
constexpr int OFF_SPELLRANGE_MAX = 0x08;
|
||||
|
||||
@@ -42,7 +41,7 @@ bool SpellIDHasRange(int spellID) {
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
const uint32_t rangeIdx =
|
||||
*reinterpret_cast<const uint32_t *>(rec + OFF_SPELL_RANGE_INDEX);
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_RANGE_INDEX);
|
||||
if (rangeIdx == 0)
|
||||
return false;
|
||||
const uint8_t *rangeRec = DBC::Record(Offsets::VAR_SPELL_RANGE_RECORDS,
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Spell.GetSpellDispelType` / `IsNextMeleeSpell` / `ResetsMeleeSwing` —
|
||||
// read raw Spell.dbc fields straight off the record so pfUI can drop its
|
||||
// nampower `GetSpellRecField` dependency.
|
||||
//
|
||||
// The Spell.dbc record fields we read live in `Offsets.h` (the single source
|
||||
// of truth for the record layout): `OFF_SPELL_DISPEL_TYPE` (+0x10),
|
||||
// `OFF_SPELL_RECORD_ATTRIBUTES` (+0x18), `OFF_SPELL_RECORD_ATTRIBUTES_EX2`
|
||||
// (+0x20), `OFF_SPELL_RECORD_INTERRUPT_FLAGS` (+0x54).
|
||||
//
|
||||
// Melee rules (server core, Spell.h / Spell.cpp):
|
||||
// on-next-swing = Attributes & (0x04 | 0x400) — two bits (Spell.h:434)
|
||||
// resets swing = (InterruptFlags & 0x08) && !(AttributesEx2 & 0x20000)
|
||||
// (Spell.cpp:3964 + Spell.h:441)
|
||||
|
||||
#include "Arg.h"
|
||||
#include "Lookup.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Spell::RecordFields {
|
||||
|
||||
namespace {
|
||||
|
||||
// Attributes bits 0x04 (SPELL_ATTR_ON_NEXT_SWING) and 0x400
|
||||
// (SPELL_ATTR_ON_NEXT_SWING_2) both mark an on-next-melee-swing ability
|
||||
// (server Spell.h:434 tests the pair).
|
||||
constexpr uint32_t SPELL_ATTR_ON_NEXT_SWING = 0x04 | 0x400;
|
||||
// InterruptFlags bit 0x08 = SPELL_INTERRUPT_FLAG_AUTOATTACK.
|
||||
constexpr uint32_t SPELL_INTERRUPT_AUTOATTACK = 0x08;
|
||||
// AttributesEx2 bit 0x20000 = NOT_RESET_AUTO_ACTIONS — exempts the spell
|
||||
// from the swing reset the AUTOATTACK interrupt flag would otherwise cause.
|
||||
constexpr uint32_t SPELL_ATTR_EX2_NOT_RESET_AUTO_ACTIONS = 0x20000;
|
||||
|
||||
uint32_t ReadField(const uint8_t *rec, int offset) {
|
||||
return *reinterpret_cast<const uint32_t *>(rec + offset);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// C_Spell.GetSpellDispelType(spellId) -> number. 0 for a non-dispellable
|
||||
// spell or an unknown ID.
|
||||
static int __fastcall Script_GetSpellDispelType(void *L) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(Spell::Arg::ResolveSpellID(L, 1));
|
||||
Game::Lua::PushNumber(
|
||||
L, rec != nullptr
|
||||
? static_cast<double>(ReadField(rec, Offsets::OFF_SPELL_DISPEL_TYPE))
|
||||
: 0.0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// C_Spell.IsNextMeleeSpell(spellId) -> bool. True for on-next-swing abilities
|
||||
// (Heroic Strike, Maul, Cleave, Raptor Strike, Slam).
|
||||
static int __fastcall Script_IsNextMeleeSpell(void *L) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(Spell::Arg::ResolveSpellID(L, 1));
|
||||
const bool onNextSwing =
|
||||
rec != nullptr &&
|
||||
(ReadField(rec, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) & SPELL_ATTR_ON_NEXT_SWING) != 0;
|
||||
Game::Lua::PushBool(L, onNextSwing);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// C_Spell.ResetsMeleeSwing(spellId) -> bool. True when casting the spell
|
||||
// resets the melee swing timer (AUTOATTACK interrupt flag set and the
|
||||
// NOT_RESET_AUTO_ACTIONS exemption clear).
|
||||
static int __fastcall Script_ResetsMeleeSwing(void *L) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(Spell::Arg::ResolveSpellID(L, 1));
|
||||
const bool resets =
|
||||
rec != nullptr &&
|
||||
(ReadField(rec, Offsets::OFF_SPELL_RECORD_INTERRUPT_FLAGS) &
|
||||
SPELL_INTERRUPT_AUTOATTACK) != 0 &&
|
||||
(ReadField(rec, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2) &
|
||||
SPELL_ATTR_EX2_NOT_RESET_AUTO_ACTIONS) == 0;
|
||||
Game::Lua::PushBool(L, resets);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellDispelType",
|
||||
&Script_GetSpellDispelType);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "IsNextMeleeSpell",
|
||||
&Script_IsNextMeleeSpell);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "ResetsMeleeSwing",
|
||||
&Script_ResetsMeleeSwing);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Spell::RecordFields
|
||||
@@ -29,11 +29,6 @@ namespace Spell::Subtext {
|
||||
|
||||
namespace {
|
||||
|
||||
// `Spell.dbc.Rank[9]` — same 9-slot locale-string array shape as the
|
||||
// adjacent `Name[9]` field. Defined locally; sibling modules
|
||||
// (`Info.cpp`, `Tooltip.cpp`, `macro/Spell.cpp`) carry the same
|
||||
// constant — promoting to `Offsets.h` is a future refactor.
|
||||
constexpr int OFF_SPELL_RANK = 0x204;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -46,7 +41,7 @@ static int __fastcall Script_GetSpellSubtext(void *L) {
|
||||
const char *subtext = DBC::LocalizedField(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT,
|
||||
static_cast<uint32_t>(spellID),
|
||||
OFF_SPELL_RANK);
|
||||
Offsets::OFF_SPELL_RECORD_RANK);
|
||||
if (subtext == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
|
||||
@@ -24,8 +24,6 @@ namespace Spell::Tooltip {
|
||||
// pulled from Spell::Info — that module keeps these as `static
|
||||
// constexpr` privates and exposing them just for two readers here
|
||||
// would force a header change.
|
||||
static constexpr int OFF_SPELL_NAME = 0x1E0; // localized char *[9]
|
||||
static constexpr int OFF_SPELL_RANK = 0x204; // localized char *[9]
|
||||
|
||||
using BuildSpellTooltip_t = void(__thiscall *)(void *thisObj, int spellID, int arg2, int arg3,
|
||||
int isPet, int showRank, int arg6, int arg7);
|
||||
@@ -94,7 +92,7 @@ void AppendByID(void *L, int spellID) {
|
||||
return;
|
||||
const int locale = *reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + OFF_SPELL_NAME + locale * 4);
|
||||
record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (name == nullptr || name[0] == '\0')
|
||||
return;
|
||||
|
||||
@@ -154,9 +152,9 @@ static int __fastcall Script_GameTooltipGetSpell(void *L) {
|
||||
|
||||
const int locale = *reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + OFF_SPELL_NAME + locale * 4);
|
||||
record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
const char *rank = *reinterpret_cast<const char *const *>(
|
||||
record + OFF_SPELL_RANK + locale * 4);
|
||||
record + Offsets::OFF_SPELL_RECORD_RANK + locale * 4);
|
||||
|
||||
if (name == nullptr)
|
||||
return 0;
|
||||
|
||||
@@ -29,8 +29,6 @@ using GetSpellCost_t = uint32_t(__fastcall *)(int spellID, int unit);
|
||||
|
||||
// Spell.dbc record field offsets we read for usability. Fully documented
|
||||
// in CLAUDE.md.
|
||||
constexpr int OFF_SPELL_POWER_TYPE = 0x7C; // int8: 0=mana, 1=rage, 2=focus, 3=energy, 4=happiness
|
||||
constexpr int OFF_SPELL_MANA_COST = 0x80; // u32 base cost (fallback only)
|
||||
|
||||
// Returns true if the player knows `spellID` per the engine's spell-
|
||||
// knowledge bitmap at `[VAR_PLAYER_SPELL_BITMAP]`. Same check
|
||||
@@ -179,12 +177,12 @@ Usability ComputeUsability(void *L, int spellID) {
|
||||
// resolve a cost (shouldn't happen here: the player descriptor
|
||||
// resolved above, so it has a player context).
|
||||
const int powerType = static_cast<int>(*reinterpret_cast<const int8_t *>(
|
||||
record + OFF_SPELL_POWER_TYPE));
|
||||
record + Offsets::OFF_SPELL_RECORD_POWER_TYPE));
|
||||
if (powerType >= 0 && powerType <= 4) {
|
||||
auto getCost = reinterpret_cast<GetSpellCost_t>(Offsets::FUN_GET_SPELL_COST);
|
||||
uint32_t cost = getCost(spellID, 0 /* local player */);
|
||||
if (cost == 0xFFFFFFFF)
|
||||
cost = *reinterpret_cast<const uint32_t *>(record + OFF_SPELL_MANA_COST);
|
||||
cost = *reinterpret_cast<const uint32_t *>(record + Offsets::OFF_SPELL_RECORD_MANA_COST);
|
||||
if (cost > 0) {
|
||||
const int currentPower = *reinterpret_cast<const int *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_POWER1 + powerType * 4);
|
||||
|
||||
+7
-14
@@ -56,13 +56,6 @@ namespace Totem::Tracker {
|
||||
|
||||
namespace {
|
||||
|
||||
// Spell.dbc record field offsets (see spell/Info.cpp for the shared set).
|
||||
constexpr int OFF_EFFECT = 0xF4; // int32[3] — SPELL_EFFECT_*
|
||||
constexpr int OFF_TOTEM_TOOL = 0xA0; // int32[2] — Totem (required tool item IDs)
|
||||
constexpr int OFF_MISC_VALUE = 0x1A8; // int32[3] — EffectMiscValue
|
||||
constexpr int OFF_ICON_ID = 0x1D4; // int32 — SpellIconID
|
||||
constexpr int OFF_NAME = 0x1E0; // char*[9] locale array
|
||||
|
||||
// SUMMON_TOTEM_SLOT1..4 spell effects (verified against Spell.dbc: Searing
|
||||
// 87/Fire, Stoneskin 88/Earth, Healing Stream 89/Water, Windfury 90/Air).
|
||||
constexpr int kEffectSummonTotemSlot1 = 87;
|
||||
@@ -101,7 +94,7 @@ uint32_t NowMs() {
|
||||
|
||||
// The totem tool item a spell requires (`Totem[0]`), or 0.
|
||||
uint32_t TotemToolItem(const uint8_t *rec) {
|
||||
const int32_t t = *reinterpret_cast<const int32_t *>(rec + OFF_TOTEM_TOOL);
|
||||
const int32_t t = *reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELL_RECORD_TOTEM);
|
||||
return t > 0 ? static_cast<uint32_t>(t) : 0;
|
||||
}
|
||||
|
||||
@@ -131,7 +124,7 @@ void ScanTools() {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
const auto *eff = reinterpret_cast<const int32_t *>(rec + OFF_EFFECT);
|
||||
const auto *eff = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELL_RECORD_EFFECT);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
const int e = eff[i];
|
||||
if (e < kEffectSummonTotemSlot1 || e > kEffectSummonTotemSlot4)
|
||||
@@ -413,10 +406,10 @@ int __fastcall Script_GetTotemInfo(void *L) {
|
||||
*reinterpret_cast<const int *>(static_cast<uintptr_t>(
|
||||
Offsets::VAR_LOCALE_INDEX));
|
||||
const char *n = *reinterpret_cast<const char *const *>(
|
||||
rec + OFF_NAME + locale * 4);
|
||||
rec + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (n != nullptr)
|
||||
name = n;
|
||||
icon = IconPath(*reinterpret_cast<const int *>(rec + OFF_ICON_ID));
|
||||
icon = IconPath(*reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_RECORD_ICON_ID));
|
||||
}
|
||||
|
||||
Game::Lua::PushBool(L, haveTotem);
|
||||
@@ -563,7 +556,7 @@ int __fastcall Script_GameTooltipSetTotem(void *L) {
|
||||
const int locale =
|
||||
*reinterpret_cast<const int *>(static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
const char *name =
|
||||
*reinterpret_cast<const char *const *>(rec + OFF_NAME + locale * 4);
|
||||
*reinterpret_cast<const char *const *>(rec + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (name == nullptr || name[0] == '\0')
|
||||
return 0;
|
||||
|
||||
@@ -630,8 +623,8 @@ void OnPlayerSpellGo(uint32_t spellID) {
|
||||
if (rec == nullptr)
|
||||
return;
|
||||
|
||||
const auto *effects = reinterpret_cast<const int32_t *>(rec + OFF_EFFECT);
|
||||
const auto *misc = reinterpret_cast<const int32_t *>(rec + OFF_MISC_VALUE);
|
||||
const auto *effects = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELL_RECORD_EFFECT);
|
||||
const auto *misc = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELL_RECORD_EFFECT_MISC_VALUE);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
const int eff = effects[i];
|
||||
if (eff < kEffectSummonTotemSlot1 || eff > kEffectSummonTotemSlot4)
|
||||
|
||||
Reference in New Issue
Block a user