mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 20:37:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b497bb7fb0 | ||
|
|
4d5fe8d41b | ||
|
|
46cae9d465 | ||
|
|
93360ce482 | ||
|
|
b879730dfb | ||
|
|
9cbf185b3c | ||
|
|
68f6a687cc | ||
|
|
2d5ecdc493 | ||
|
|
783ffd05bf | ||
|
|
f559eb4d07 | ||
|
|
2de7176b94 | ||
|
|
13008ab8aa | ||
|
|
0df8885045 | ||
|
|
8fa344a084 | ||
|
|
2612118ff6 | ||
|
|
76e40fec96 | ||
|
|
b72c52d9f7 | ||
|
|
1447317cd3 | ||
|
|
f293653837 | ||
|
|
ba1e12fca9 | ||
|
|
867478f410 | ||
|
|
caeb598b78 | ||
|
|
37ca209210 | ||
|
|
94c5eaafd2 | ||
|
|
3f09b3f883 | ||
|
|
9f640de74b | ||
|
|
67b968c36c | ||
|
|
f833aad769 | ||
|
|
b38673f51f | ||
|
|
f2161265be | ||
|
|
eefcbb17f2 | ||
|
|
599a65d80c | ||
|
|
228c5daa0f | ||
|
|
cc6953a530 | ||
|
|
84da9c23df | ||
|
|
6e58cb81d1 | ||
|
|
8a065481b4 | ||
|
|
2a7cbf75ca | ||
|
|
ff5ce5cdcb | ||
|
|
47b6a66b96 | ||
|
|
e78c36bb70 | ||
|
|
41fb17502d | ||
|
|
a989499cae | ||
|
|
722c257acb | ||
|
|
a6c7098106 | ||
|
|
7caee620e8 | ||
|
|
954533a170 | ||
|
|
dbe5ab40ee | ||
|
|
f3b328247c | ||
|
|
470361fbce | ||
|
|
2d266a9b76 | ||
|
|
8e3e207f83 | ||
|
|
1fd1295679 | ||
|
|
e4a75e68ca | ||
|
|
28bf41c647 | ||
|
|
ac97f9b960 | ||
|
|
7f6c6e2dd4 | ||
|
|
9d09266fc6 | ||
|
|
d972433ab0 | ||
|
|
b5513c6b22 | ||
|
|
8a434fea0e | ||
|
|
dbbba88aad | ||
|
|
b8bd0bce4e | ||
|
|
c3a80053e9 | ||
|
|
5a3badb5bd |
+80
-16
@@ -4,48 +4,112 @@ on:
|
||||
push:
|
||||
branches: ['**']
|
||||
tags: ['v*']
|
||||
# Manual escape hatch (e.g. when push webhooks are throttled during a
|
||||
# GitHub Actions incident). Run from the Actions UI:
|
||||
# - select a `vX.Y.Z` tag ref -> release build of that tag, OR
|
||||
# - run from a branch and pass `tag: vX.Y.Z` -> release build that
|
||||
# creates/attaches the release to that tag.
|
||||
# Leave `tag` blank on a branch run for a plain dev build (artifact).
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build (e.g. v2.0.0). Blank = dev build.'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
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:
|
||||
submodules: recursive
|
||||
|
||||
- name: Stamp embedded TOC version
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: pwsh
|
||||
# Resolve the effective version once so every step below agrees.
|
||||
# is_release=true when a vX.Y.Z tag is in play (pushed tag,
|
||||
# dispatched-from-tag, or a `tag` input); the embedded addon's
|
||||
# `## Version:` and the compiled CLASSIC_API_VERSION are both
|
||||
# stamped from it at embed/compile time (see cmake/embed_addon.cmake)
|
||||
# — no source edit.
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
env:
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
$version = '${{ github.ref_name }}' -replace '^v',''
|
||||
$toc = 'AddOns/!!!ClassicAPI/!!!ClassicAPI.toc'
|
||||
(Get-Content $toc) -replace '^## Version:.*', "## Version: $version" |
|
||||
Set-Content $toc
|
||||
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 (tag)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: cmake -B build -A Win32 "-DCLASSICAPI_TAG=${{ github.ref_name }}"
|
||||
- name: Configure (release)
|
||||
if: steps.ver.outputs.is_release == 'true'
|
||||
run: cmake -B build -A Win32 "-DCLASSICAPI_TAG=${{ steps.ver.outputs.tag }}"
|
||||
|
||||
- name: Configure (branch)
|
||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||
- name: Configure (dev)
|
||||
if: steps.ver.outputs.is_release != 'true'
|
||||
run: cmake -B build -A Win32
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build --config Release
|
||||
|
||||
- name: Upload artifact
|
||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||
if: steps.ver.outputs.is_release != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ClassicAPI-${{ github.sha }}
|
||||
path: build/Release/ClassicAPI.dll
|
||||
|
||||
- name: Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: steps.ver.outputs.is_release == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: build/Release/ClassicAPI.dll
|
||||
tag_name: ${{ steps.ver.outputs.tag }}
|
||||
files: |
|
||||
build/Release/ClassicAPI.dll
|
||||
build/Release/ClassicAPI.map
|
||||
generate_release_notes: true
|
||||
|
||||
+3
-1
@@ -45,4 +45,6 @@
|
||||
# debug information files
|
||||
*.dwo
|
||||
|
||||
CLAUDE.md
|
||||
CLAUDE.md
|
||||
|
||||
/AddOns/!!!ClassicAPI/.classicapi-dev
|
||||
|
||||
@@ -58,3 +58,26 @@ if C_AddOns.DoesAddOnExist("ShaguTweaks") then
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Guard UPDATE_MOUSEOVER_UNIT handlers (issue #12).
|
||||
function CAPI_MouseoverClearedCompat(frame)
|
||||
if not frame then return end
|
||||
local originalOnEvent = frame:GetScript("OnEvent")
|
||||
frame:SetScript("OnEvent", function()
|
||||
if event == "UPDATE_MOUSEOVER_UNIT" and not UnitExists("mouseover") then
|
||||
return
|
||||
end
|
||||
|
||||
if originalOnEvent then
|
||||
originalOnEvent()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
CAPI_MouseoverClearedCompat(GameTooltip)
|
||||
|
||||
if C_AddOns.DoesAddOnExist("Puppeteer") then
|
||||
EventUtil.ContinueOnAddOnLoaded("Puppeteer", function()
|
||||
CAPI_MouseoverClearedCompat(PTEnemyUpdater)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -7,46 +7,16 @@
|
||||
-- the only duration packet (SMSG_UPDATE_AURA_DURATION) is self-scoped to the
|
||||
-- aura-bearer, so an observing caster hears nothing (and on a mob the packet
|
||||
-- isn't even built). We can't hear the change, but we DO see the *triggering*
|
||||
-- cast, so C_UnitAuras.RegisterAuraDurationModifier mirrors the server's edit
|
||||
-- off that trigger.
|
||||
-- cast, so a registered rule mirrors the server's edit off that trigger.
|
||||
--
|
||||
-- A trigger is matched by exact spellID (stable across ranks -- these are the
|
||||
-- IDs the server binds its spell script to); the affected aura by
|
||||
-- SpellFamilyName + a family-flag overlap (rank-proof, exactly how the server
|
||||
-- scripts locate it). Flags below are from the client's Spell.dbc. When the
|
||||
-- trigger is a whole class of spells rather than a named ability,
|
||||
-- RegisterAuraDurationModifierByTrigger matches it by SpellFamilyName + school
|
||||
-- index instead (see Shadow Weaving below).
|
||||
--
|
||||
-- NOT included on purpose: Carnage's Rip/Rake refresh fires behind a
|
||||
-- server-side probability roll on Ferocious Bite. The client can't know the
|
||||
-- roll's outcome, so inferring it would show wrong timers. If you accept that,
|
||||
-- register it yourself, e.g. (DRUID = 7):
|
||||
-- C_UnitAuras.RegisterAuraDurationModifier(<ferociousBiteID>, 7, tonumber("800000",16), 108, "refresh") -- Rip
|
||||
-- C_UnitAuras.RegisterAuraDurationModifier(<ferociousBiteID>, 7, tonumber("1000",16), 494, "refresh") -- Rake
|
||||
-- Conflagrate's *full*-consume variant (other servers) removes Immolate
|
||||
-- outright; that clears the aura slot and ClassicAPI evicts it via the normal
|
||||
-- removal path, so the reduce rule below is harmless there and correct on
|
||||
-- Turtle (where Immolate keeps ticking with 3s shaved off).
|
||||
-- A trigger keyed to a whole class of spells (rather than a named ability) is
|
||||
-- registered from Lua with RegisterAuraDurationModifierByTrigger, matched by
|
||||
-- SpellFamilyName + school index (see Shadow Weaving below). Exact-spellID
|
||||
-- triggers are registered from C++ (Aura::Source::AddDurationMod): Turtle's
|
||||
-- Conflagrate -> Immolate and Molten Blast -> Flame Shock live in the DLL
|
||||
-- (src/turtle/DurationMods.cpp), as does Carnage, whose per-combo-point roll no
|
||||
-- rule can express (src/turtle/Carnage.cpp).
|
||||
|
||||
if TURTLE_WOW_VERSION then
|
||||
local WARLOCK, SHAMAN = 5, 11
|
||||
-- Lua 5.0 has no 0x hex literals; tonumber(hex, 16) keeps the flags readable.
|
||||
local IMMOLATE_FLAG = tonumber("4", 16) -- CF_WARLOCK_IMMOLATE (bit 2)
|
||||
local FLAMESHOCK_FLAG = tonumber("10000000", 16) -- CF_SHAMAN_FLAME_SHOCK (bit 28)
|
||||
|
||||
-- Conflagrate (all ranks) -> shave 3s off the caster's Immolate.
|
||||
local CONFLAGRATE = { 17962, 18930, 18931, 18932 }
|
||||
for i = 1, table.getn(CONFLAGRATE) do
|
||||
C_UnitAuras.RegisterAuraDurationModifier(CONFLAGRATE[i], WARLOCK, IMMOLATE_FLAG, 0, "reduce", 3)
|
||||
end
|
||||
|
||||
-- Molten Blast (all ranks) -> refresh the caster's Flame Shock to full.
|
||||
local MOLTEN_BLAST = { 36916, 36917, 36918, 36919, 36920, 36921 }
|
||||
for i = 1, table.getn(MOLTEN_BLAST) do
|
||||
C_UnitAuras.RegisterAuraDurationModifier(MOLTEN_BLAST[i], SHAMAN, FLAMESHOCK_FLAG, 0, "refresh")
|
||||
end
|
||||
end
|
||||
-- Shadow Weaving (priest): any shadow-school priest cast refreshes the caster's
|
||||
-- Shadow Vulnerability (15258, 15s, stacks to 5). The 1->5 stack changes fire
|
||||
-- OnAuraStacksChanged (already handled); the 5->5 refresh is the silent blind
|
||||
|
||||
@@ -90,31 +90,3 @@ function GameTooltip_ShowCompareItem(self, shift)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Guard the stock GameTooltip UPDATE_MOUSEOVER_UNIT handler (issue #12).
|
||||
--
|
||||
-- ClassicAPI's DLL now fires UPDATE_MOUSEOVER_UNIT on mouseover LOSS too (retail
|
||||
-- parity), not just on gain. The stock inline handler in FrameXML/GameTooltip.xml
|
||||
-- recolors the name unconditionally:
|
||||
-- _G[this:GetName().."TextLeft1"]:SetTextColor(GameTooltip_UnitColor("mouseover"))
|
||||
-- and GameTooltip_UnitColor returns white (1,1,1) when "mouseover" doesn't exist.
|
||||
-- So the loss fire (and the transient clear that happens when clicking a unit)
|
||||
-- repaints the still-visible/fading name white before it disappears -- reported
|
||||
-- as NPC and PvP-flagged-player names flashing white.
|
||||
--
|
||||
-- We wrap the stock handler instead of replacing its body: swallow the event
|
||||
-- only when there is no mouseover unit to color, otherwise delegate to the
|
||||
-- original untouched. During the loss window the name simply keeps its last
|
||||
-- color instead of going white; when a mouseover resolves again it recolors
|
||||
-- normally. (1.12 OnEvent handlers read the `event`/`this` globals the engine
|
||||
-- sets before invoking us, so calling the original as a plain function works.)
|
||||
local originalOnEvent = GameTooltip:GetScript("OnEvent")
|
||||
GameTooltip:SetScript("OnEvent", function()
|
||||
if ( event == "UPDATE_MOUSEOVER_UNIT" and not UnitExists("mouseover") ) then
|
||||
return;
|
||||
end
|
||||
|
||||
if ( originalOnEvent ) then
|
||||
originalOnEvent();
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -7,19 +7,46 @@
|
||||
-- the cursor -- the same UnitPopup menu Blizzard's PlayerFrame / TargetFrame /
|
||||
-- PartyMemberFrame show on right-click. Lets a unit frame express its
|
||||
-- right-click menu purely as an attribute (e.g. type2 = "menu") instead of a
|
||||
-- hand-rolled OnClick handler.
|
||||
-- hand-rolled OnClick handler. pfUI's unit frames use exactly this path
|
||||
-- (api/unitframes.lua -> ClassicAPI_ToggleUnitMenu).
|
||||
--
|
||||
-- The unit -> menu-type resolution mirrors Blizzard's own generic resolver
|
||||
-- (TargetFrameDropDown_Initialize in FrameXML): self -> SELF, pet -> PET, a
|
||||
-- grouped player -> PARTY (whisper / inspect / trade / follow / promote / ...),
|
||||
-- any other player -> PLAYER (adds INVITE), anything else (NPC) ->
|
||||
-- RAID_TARGET_ICON. Vanilla's stock UI never wires a unit to the "RAID" menu
|
||||
-- (there are no clickable raid unit frames in 1.12), and PARTY carries the
|
||||
-- options players actually want on a grouped member, so grouped players (party
|
||||
-- or raid) use PARTY -- matching how pfUI drives its raid menu.
|
||||
-- RAID_TARGET_ICON (the raid-marker list). Vanilla's stock UI never wires a
|
||||
-- unit to the "RAID" menu (there are no clickable raid unit frames in 1.12),
|
||||
-- and PARTY carries the options players actually want on a grouped member, so
|
||||
-- grouped players (party or raid) use PARTY -- matching how pfUI drives its
|
||||
-- raid menu.
|
||||
--
|
||||
-- Focus / Clear Focus: we add one contextual entry -- "Set Focus" when the
|
||||
-- unit is not the current focus, "Clear Focus" when it is -- for every real,
|
||||
-- non-self unit, NPCs included. This is done entirely inside our own dropdown:
|
||||
-- no hooksecurefunc, and no mutation of Blizzard's shared UnitPopupMenus tables
|
||||
-- (so default frames and other addons are unaffected). Because
|
||||
-- UnitPopup_ShowMenu hardwires every button's click to UnitPopup_OnClick (which
|
||||
-- does not know our keys), we add the Focus button ourselves with our own func.
|
||||
-- To keep Cancel last, we show the standard menu MINUS its trailing Cancel,
|
||||
-- then append Focus and Cancel.
|
||||
--
|
||||
-- This also fixes NPCs opening no menu at all: an NPC resolves to the
|
||||
-- RAID_TARGET_ICON menu, whose entries UnitPopup_HideButtons hides entirely
|
||||
-- unless you are in a group and can place markers -- so solo the menu was
|
||||
-- empty and never opened. Appending Focus + Cancel ourselves guarantees the
|
||||
-- menu always opens (markers still show when they are allowed).
|
||||
--
|
||||
-- Backed by the DLL's FocusUnit / ClearFocus globals and the `focus` unit
|
||||
-- token (src/unit/Focus.cpp). Labels come from the localized SET_FOCUS /
|
||||
-- CLEAR_FOCUS globals (locales/*.lua).
|
||||
|
||||
local dropdown;
|
||||
|
||||
-- Scratch menu key we own, rebuilt each open as a copy of the resolved base
|
||||
-- menu with its trailing CANCEL removed. Kept out of Blizzard's tables so the
|
||||
-- stock menus are never mutated.
|
||||
local SCRATCH_MENU = "CLASSICAPI_UNITMENU";
|
||||
|
||||
local function EnsureDropdown()
|
||||
if not dropdown then
|
||||
dropdown = CreateFrame("Frame", "ClassicAPIUnitMenuDropDown", UIParent, "UIDropDownMenuTemplate");
|
||||
@@ -42,18 +69,57 @@ local function ResolveMenu(unit)
|
||||
return "RAID_TARGET_ICON";
|
||||
end
|
||||
|
||||
-- Append the contextual Focus / Clear Focus button for `unit`, with our own
|
||||
-- click func. Skipped for the player themselves (focusing yourself is a no-op).
|
||||
local function AddFocusButton(unit)
|
||||
if UnitIsUnit(unit, "player") then
|
||||
return;
|
||||
end
|
||||
local info = {};
|
||||
info.notCheckable = 1;
|
||||
if UnitExists("focus") and UnitIsUnit(unit, "focus") then
|
||||
info.text = CLEAR_FOCUS;
|
||||
info.func = function() ClearFocus(); end;
|
||||
else
|
||||
info.text = SET_FOCUS;
|
||||
info.func = function() FocusUnit(unit); end;
|
||||
end
|
||||
UIDropDownMenu_AddButton(info);
|
||||
end
|
||||
|
||||
local function AddCancelButton()
|
||||
local info = {};
|
||||
info.text = CANCEL;
|
||||
info.notCheckable = 1;
|
||||
info.func = function() end;
|
||||
UIDropDownMenu_AddButton(info);
|
||||
end
|
||||
|
||||
function ClassicAPI_ToggleUnitMenu(unit)
|
||||
if not unit or not UnitExists(unit) then
|
||||
return;
|
||||
end
|
||||
local which = ResolveMenu(unit);
|
||||
local name;
|
||||
if which == "RAID_TARGET_ICON" then
|
||||
name = RAID_TARGET_ICON;
|
||||
end
|
||||
local dd = EnsureDropdown();
|
||||
dd.initialize = function()
|
||||
UnitPopup_ShowMenu(dd, which, unit, name);
|
||||
dd.initialize = function(level)
|
||||
level = level or UIDROPDOWNMENU_MENU_LEVEL or 1;
|
||||
|
||||
if level > 1 then
|
||||
UnitPopup_ShowMenu(dd, which, unit);
|
||||
return;
|
||||
end
|
||||
|
||||
local base = UnitPopupMenus[which];
|
||||
local scratch = {};
|
||||
for _, value in ipairs(base) do
|
||||
if value ~= "CANCEL" then
|
||||
table.insert(scratch, value);
|
||||
end
|
||||
end
|
||||
UnitPopupMenus[SCRATCH_MENU] = scratch;
|
||||
UnitPopup_ShowMenu(dd, SCRATCH_MENU, unit);
|
||||
AddFocusButton(unit);
|
||||
AddCancelButton();
|
||||
end
|
||||
ToggleDropDownMenu(1, nil, dd, "cursor");
|
||||
end
|
||||
|
||||
@@ -2,7 +2,33 @@ if GetLocale() ~= "deDE" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Fokus setzen";
|
||||
BINDING_NAME_TARGETFOCUS = "Fokus anvisieren";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Seid Ihr sicher, dass Ihr das Ausrüstungsset \"%s\" löschen wollt?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Möchtet Ihr das Ausrüstungsset \"%s\" speichern?";
|
||||
DEBUFF_SYMBOL_CURSE = "Fl";
|
||||
DEBUFF_SYMBOL_DISEASE = "Kr";
|
||||
DEBUFF_SYMBOL_MAGIC = "Ma";
|
||||
DEBUFF_SYMBOL_POISON = "Gi";
|
||||
DEBUG_FRAMESTACK = "Frame Stack";
|
||||
EQUIPMENT_MANAGER = "Ausrüstungsmanager";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Diesen Platz ignorieren";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "Der Ausrüstungsmanager ist abgeschaltet.";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(%d fehlen)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "In Beuteln platzieren";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Diesen Platz einbeziehen";
|
||||
EQUIPMENT_SETS = "Ausrüstungs-Sets: |cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "Ein Ausrüstungsset mit diesem Namen gibt es bereits.";
|
||||
EQUIPMENT_SETS_TOO_MANY = "Ihr könnt keine weiteren Ausrüstungssets erstellen.";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Spezialisierung zuweisen:";
|
||||
EQUIPMENT_SET_EDIT = "Name/Icon ändern";
|
||||
EQUIPMENT_SET_SETTINGS = "Einstellungen";
|
||||
EQUIPSET_EQUIP = "Anlegen";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Ausrüstungstausch fehlgeschlagen. Inventar ist voll.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Einige Gegenstände des Sets '%s' können nicht gewechselt werden, da Ihr Euch im Kampf befindet.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Mindestens ein Teil des Sets '%s' fehlt und kann nicht ausgerüstet werden.";
|
||||
CLEAR_FOCUS = "Fokus löschen";
|
||||
SET_FOCUS = "Fokus setzen";
|
||||
FOCUSTARGET = "Ziel als Fokus festlegen";
|
||||
GEARSETS_POPUP_TEXT = "Setnamen eingeben (Max. 16 Zeichen):";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4IGegenstand:Gegenstände;";
|
||||
ITEMS_EQUIPPED = "%d |4Gegenstand:Gegenstände; ausgerüstet";
|
||||
ITEMS_IN_INVENTORY = "%d |4Gegenstand:Gegenstände; im Inventar";
|
||||
@@ -15,6 +41,10 @@ COPPER_AMOUNT_SYMBOL = "K";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "G";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "Alle Symbole";
|
||||
ICON_FILTER_ITEM = "Gegenstände";
|
||||
ICON_FILTER_SPELL = "Zauber";
|
||||
ICON_SELECTION_TITLE_CURRENT = "Aktuell ausgewählt";
|
||||
ITEM_DELTA_DESCRIPTION = "Wenn Ihr diesen Gegenstand ersetzt, werden sich folgende Werte ändern:";
|
||||
ITEM_MOD_MANA_SHORT = "Mana";
|
||||
ITEM_MOD_HEALTH_SHORT = "Gesundheit";
|
||||
@@ -42,7 +72,11 @@ ITEM_MOD_BLOCK_VALUE_SHORT = "Blocken";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Schaden pro Sekunde";
|
||||
LARGE_NUMBER_SEPERATOR = ".";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Ausrüstungsmanager";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "Neues Set";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Notizen für %s eintragen:";
|
||||
SET_NOTE = "Notiz eintragen";
|
||||
SILVER_AMOUNT_SYMBOL = "S";
|
||||
SLASH_CLEARFOCUS1 = "/fokusaufheben";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
|
||||
@@ -1,11 +1,41 @@
|
||||
BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Are you sure you want to delete the equipment set %s?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Would you like to save the equipment set \"%s\"?";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
DEBUFF_SYMBOL_CURSE = "Cu";
|
||||
DEBUFF_SYMBOL_DISEASE = "Di";
|
||||
DEBUFF_SYMBOL_MAGIC = "Ma";
|
||||
DEBUFF_SYMBOL_POISON = "Po";
|
||||
DEBUG_FRAMESTACK = "Frame Stack";
|
||||
EQUIPMENT_MANAGER = "Equipment Manager";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Ignore This Slot";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "The Equipment Manager is disabled.";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(%d missing)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "Place In Bags";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Include This Slot";
|
||||
EQUIPMENT_SETS = "Equipment Sets: |cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "An equipment set with that name already exists.";
|
||||
EQUIPMENT_SETS_TOO_MANY = "You cannot create any more new equipment sets.";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Assign to Specialization:";
|
||||
EQUIPMENT_SET_EDIT = "Change Name/Icon";
|
||||
EQUIPMENT_SET_SETTINGS = "Settings";
|
||||
EQUIPSET_EQUIP = "Equip";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Equipment swap failed. Inventory is full.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Some items from set %s cannot be swapped because you are in combat.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "One or more items from set %s were missing and could not be equipped.";
|
||||
CLEAR_FOCUS = "Clear Focus";
|
||||
SET_FOCUS = "Set Focus";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
GEARSETS_POPUP_TEXT = "Enter Set Name (Max 16 Characters):";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "g";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "All Icons";
|
||||
ICON_FILTER_ITEM = "Items";
|
||||
ICON_FILTER_SPELL = "Spells";
|
||||
ICON_SELECTION_TITLE_CURRENT = "Currently Selected";
|
||||
ITEM_DELTA_DESCRIPTION = "If you replace this item, the following stat changes will occur:";
|
||||
ITEM_MOD_MANA_SHORT = "Mana";
|
||||
ITEM_MOD_HEALTH_SHORT = "Health";
|
||||
@@ -39,7 +69,11 @@ ITEM_MISSING = "%s missing";
|
||||
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Equipment Manager";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "New Set";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Set Notes for %s:";
|
||||
SET_NOTE = "Set Note";
|
||||
SILVER_AMOUNT_SYMBOL = "s";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
|
||||
@@ -2,7 +2,33 @@ if GetLocale() ~= "esES" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Establecer objetivo";
|
||||
BINDING_NAME_TARGETFOCUS = "Seleccionar enfocado";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "¿Seguro que quieres eliminar el equipamiento %s?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "¿Quieres guardar el conjunto de equipamiento \"%s\"?";
|
||||
DEBUFF_SYMBOL_CURSE = "Mal";
|
||||
DEBUFF_SYMBOL_DISEASE = "En";
|
||||
DEBUFF_SYMBOL_MAGIC = "Mag";
|
||||
DEBUFF_SYMBOL_POISON = "Ve";
|
||||
DEBUG_FRAMESTACK = "Trama de pila";
|
||||
EQUIPMENT_MANAGER = "Gestor de equipamiento";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Ignorar esta casilla";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "El Gestor de equipamiento está desactivado.";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(falta %d)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "Colocar en bolsas";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Incluir esta casilla";
|
||||
EQUIPMENT_SETS = "Conjuntos de equipamiento: |cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "Ya existe un conjunto de equipamiento con ese nombre.";
|
||||
EQUIPMENT_SETS_TOO_MANY = "Ya no puedes crear más equipamientos.";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Asignar a especialización:";
|
||||
EQUIPMENT_SET_EDIT = "Cambiar nombre/icono";
|
||||
EQUIPMENT_SET_SETTINGS = "Config.";
|
||||
EQUIPSET_EQUIP = "Equipar";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "No se ha podido cambiar el equipamiento. Inventario lleno.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Algunos objetos del equipo %s no se cambiarán porque estás en combate.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Falta uno o más objetos del equipo %s y no se ha podido equipar.";
|
||||
CLEAR_FOCUS = "Quitar foco";
|
||||
SET_FOCUS = "Establecer foco";
|
||||
FOCUSTARGET = "Establecer objetivo";
|
||||
GEARSETS_POPUP_TEXT = "Nombra este equipamiento (Máx. 16 carac.):";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4objeto:objetos;";
|
||||
ITEMS_EQUIPPED = "Equipado: %d |4objeto:objetos;";
|
||||
ITEMS_IN_INVENTORY = "En inventario: %d |4objeto:objetos;";
|
||||
@@ -15,6 +41,10 @@ COPPER_AMOUNT_SYMBOL = "c";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "o";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "Todos los iconos";
|
||||
ICON_FILTER_ITEM = "Objetos";
|
||||
ICON_FILTER_SPELL = "Hechizos";
|
||||
ICON_SELECTION_TITLE_CURRENT = "Selección actual";
|
||||
ITEM_DELTA_DESCRIPTION = "Si reemplazas este objeto, se producirán los siguientes cambios en las estadísticas:";
|
||||
ITEM_MOD_MANA_SHORT = "Maná";
|
||||
ITEM_MOD_HEALTH_SHORT = "Salud";
|
||||
@@ -42,7 +72,11 @@ ITEM_MOD_BLOCK_VALUE_SHORT = "Valor de bloqueo";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "daño por segundo";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Gestor de equipamiento";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "Nuevo conjunto";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Establecer nota para %s:";
|
||||
SET_NOTE = "Establecer nota";
|
||||
SILVER_AMOUNT_SYMBOL = "p";
|
||||
SLASH_CLEARFOCUS1 = "/quitarenfoque";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
|
||||
@@ -2,6 +2,31 @@ if GetLocale() ~= "frFR" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Définir cible";
|
||||
BINDING_NAME_TARGETFOCUS = "Reprendre cible";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Voulez-vous vraiment supprimer l'ensemble d'équipement %s ?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Voulez-vous sauvegarder l'ensemble d'équipement %s ?";
|
||||
DEBUFF_SYMBOL_CURSE = "Mn";
|
||||
DEBUFF_SYMBOL_DISEASE = "Md";
|
||||
DEBUFF_SYMBOL_MAGIC = "Mg";
|
||||
DEBUFF_SYMBOL_POISON = "Po";
|
||||
DEBUG_FRAMESTACK = "Empilement des cadres";
|
||||
EQUIPMENT_MANAGER = "Gestionnaire d'équipement";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Ignorer cet emplacement";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "Le Gestionnaire d'équipement est désactivé.";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(objet manquant : %d)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "Mettre en sac";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Inclure cet emplacement";
|
||||
EQUIPMENT_SETS = "Ensembles d'équipement : |cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "Un ensemble d'équipement portant ce nom existe déjà.";
|
||||
EQUIPMENT_SETS_TOO_MANY = "Vous ne pouvez plus créer de nouveaux ensembles d'équipement.";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Assigner à la spécialisation :";
|
||||
EQUIPMENT_SET_EDIT = "Modifier le nom/l'icône";
|
||||
EQUIPMENT_SET_SETTINGS = "Réglages";
|
||||
EQUIPSET_EQUIP = "Equiper";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Échec de l'échange d'équipement. L'inventaire est plein.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Certains objets de l'ensemble %s ne peuvent pas être échangés car vous êtes en combat.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Une ou plusieurs pièces de l'ensemble %s manquaient et n'ont pu être équipées.";
|
||||
CLEAR_FOCUS = "Effacer la focalisation";
|
||||
SET_FOCUS = "Définir la focalisation";
|
||||
FOCUSTARGET = "Définir la cible de focalisation";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4objet:objets;";
|
||||
ITEMS_EQUIPPED = "%d |4objet:objets; équipés";
|
||||
@@ -12,9 +37,14 @@ ITEMS_NOT_IN_INVENTORY = "%d |4objet:objets; manquants";
|
||||
TARGETFOCUS = "Cibler l'unité focalisée";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
GEARSETS_POPUP_TEXT = "Nom de l'ensemble (16 caractères max.) :";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "o";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "Toutes les icônes";
|
||||
ICON_FILTER_ITEM = "Objets";
|
||||
ICON_FILTER_SPELL = "Sorts";
|
||||
ICON_SELECTION_TITLE_CURRENT = "Sélection actuelle";
|
||||
ITEM_DELTA_DESCRIPTION = "Si vous remplacez cet objet, les caractéristiques suivantes seront modifiées :";
|
||||
ITEM_MOD_MANA_SHORT = "Mana";
|
||||
ITEM_MOD_HEALTH_SHORT = "Vie";
|
||||
@@ -42,7 +72,11 @@ ITEM_MOD_BLOCK_VALUE_SHORT = "Valeur de blocage";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Dégâts par seconde";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Gestionnaire d'équipement";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "Nouvel ensemble";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Écrire une note pour %s :";
|
||||
SET_NOTE = "Écrire une note";
|
||||
SILVER_AMOUNT_SYMBOL = "a";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/annulerfocal";
|
||||
|
||||
@@ -2,6 +2,31 @@ if GetLocale() ~= "ruRU" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Запомненная цель";
|
||||
BINDING_NAME_TARGETFOCUS = "Восстановить цель";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Вы действительно хотите удалить комплект экипировки \"%s\"?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Хотите сохранить комплект экипировки \"%s\"?";
|
||||
DEBUFF_SYMBOL_CURSE = "Пр";
|
||||
DEBUFF_SYMBOL_DISEASE = "Бз";
|
||||
DEBUFF_SYMBOL_MAGIC = "Мг";
|
||||
DEBUFF_SYMBOL_POISON = "Яд";
|
||||
DEBUG_FRAMESTACK = "Соединение рамок";
|
||||
EQUIPMENT_MANAGER = "Комплекты экипировки";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Пропустить ячейку";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "Управление экипировкой отключено.";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(предмет для ячейки \"%d\" отсутствует)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "Поместить в сумку";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Учитывать ячейку";
|
||||
EQUIPMENT_SETS = "Комплекты экипировки: |cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "Это название уже используется для другого комплекта.";
|
||||
EQUIPMENT_SETS_TOO_MANY = "Вы не можете создавать новые комплекты экипировки.";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Выбрать специализацию:";
|
||||
EQUIPMENT_SET_EDIT = "Изменить название/значок";
|
||||
EQUIPMENT_SET_SETTINGS = "Параметры";
|
||||
EQUIPSET_EQUIP = "Надеть";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Не удалось заменить комплект обмундирования. Инвентарь переполнен.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Некоторые предметы из набора \"%s\" не будут экипированы, поскольку вы участвуете в бою.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Недостает одного или более предметов из набора \"%s\".";
|
||||
CLEAR_FOCUS = "Отменить запоминание цели";
|
||||
SET_FOCUS = "Запомнить цель";
|
||||
FOCUSTARGET = "Запомнить цель";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
|
||||
ITEMS_EQUIPPED = "%d |4предмет экипирован:предмета экипировано:предметов экипировано;";
|
||||
@@ -12,9 +37,14 @@ ITEMS_NOT_IN_INVENTORY = "%d |4предмет отсутствует:предм
|
||||
TARGETFOCUS = "Восстановить цель";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "м";
|
||||
GEARSETS_POPUP_TEXT = "Введите название комплекта (не более 16 символов):";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "з";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "Все значки";
|
||||
ICON_FILTER_ITEM = "Предметы";
|
||||
ICON_FILTER_SPELL = "Заклинания";
|
||||
ICON_SELECTION_TITLE_CURRENT = "Текущий выбор";
|
||||
ITEM_DELTA_DESCRIPTION = "Если вы замените этот предмет, произойдут следующие изменения:";
|
||||
ITEM_MOD_MANA_SHORT = "к мане";
|
||||
ITEM_MOD_HEALTH_SHORT = "к здоровью";
|
||||
@@ -42,7 +72,11 @@ ITEM_MOD_BLOCK_VALUE_SHORT = "к показателю блокирования";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "к урону в секунду";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Комплекты экипировки";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "Новый комплект";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Заметка о |3-5(%s):";
|
||||
SET_NOTE = "Добавить заметку";
|
||||
SILVER_AMOUNT_SYMBOL = "с";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/сбросфокуса";
|
||||
|
||||
@@ -2,6 +2,31 @@ if GetLocale() ~= "zhCN" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "焦点目标";
|
||||
BINDING_NAME_TARGETFOCUS = "目标焦点";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "你确认要删除装备方案%s吗?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "你想要保存装备方案\"%s\"吗?";
|
||||
DEBUFF_SYMBOL_CURSE = "诅";
|
||||
DEBUFF_SYMBOL_DISEASE = "疾";
|
||||
DEBUFF_SYMBOL_MAGIC = "魔";
|
||||
DEBUFF_SYMBOL_POISON = "毒";
|
||||
DEBUG_FRAMESTACK = "框架栈";
|
||||
EQUIPMENT_MANAGER = "装备管理";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "忽略此栏位";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "装备管理已禁用。";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(缺少%d)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "放入背包";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "包括此栏位";
|
||||
EQUIPMENT_SETS = "装备配置方案:|cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "一个重名的装备方案已经存在。";
|
||||
EQUIPMENT_SETS_TOO_MANY = "你不能再创建新的装备方案了。";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "指定专精:";
|
||||
EQUIPMENT_SET_EDIT = "修改名称/图标";
|
||||
EQUIPMENT_SET_SETTINGS = "设置";
|
||||
EQUIPSET_EQUIP = "装备";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "装备更换失败。背包已满。";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "装备方案%s中的某些物品不能在战斗中切换。";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "未找到套装方案%s中的一件或多件物品,无法装备。";
|
||||
CLEAR_FOCUS = "清除焦点";
|
||||
SET_FOCUS = "设置焦点";
|
||||
FOCUSTARGET = "设定焦点目标";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
ITEMS_EQUIPPED = "已装备%d件物品";
|
||||
@@ -12,9 +37,14 @@ ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
TARGETFOCUS = "选中焦点目标";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "铜";
|
||||
GEARSETS_POPUP_TEXT = "输入方案名称(最多16个字符):";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "金";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "所有图标";
|
||||
ICON_FILTER_ITEM = "物品";
|
||||
ICON_FILTER_SPELL = "法术";
|
||||
ICON_SELECTION_TITLE_CURRENT = "当前已选择";
|
||||
ITEM_DELTA_DESCRIPTION = "如果你替换该物品,将会产生以下的属性变更:";
|
||||
ITEM_MOD_MANA_SHORT = "法力值";
|
||||
ITEM_MOD_HEALTH_SHORT = "生命值";
|
||||
@@ -42,7 +72,11 @@ ITEM_MOD_BLOCK_VALUE_SHORT = "格挡值";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒伤害";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "装备管理";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "新的方案";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "为%s设置备注:";
|
||||
SET_NOTE = "设置备注";
|
||||
SILVER_AMOUNT_SYMBOL = "银";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
|
||||
@@ -2,6 +2,31 @@ if GetLocale() ~= "zhTW" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "鎖定目標";
|
||||
BINDING_NAME_TARGETFOCUS = "選定鎖定目標";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "你確定要刪除裝備設定:%s嗎?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "你想要儲存裝備設定「%s」嗎?";
|
||||
DEBUFF_SYMBOL_CURSE = "咒";
|
||||
DEBUFF_SYMBOL_DISEASE = "疾";
|
||||
DEBUFF_SYMBOL_MAGIC = "魔";
|
||||
DEBUFF_SYMBOL_POISON = "毒";
|
||||
DEBUG_FRAMESTACK = "框架堆疊";
|
||||
EQUIPMENT_MANAGER = "裝備管理員";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "忽略此欄位";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "裝備管理員已關閉。";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d件%s|cffff0000(缺少%d件)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "放進背包";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "包含此欄位";
|
||||
EQUIPMENT_SETS = "裝備設定:|cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "您所選擇的名稱已經有裝備設定使用了。";
|
||||
EQUIPMENT_SETS_TOO_MANY = "你無法製造更多的新裝備設定。";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "指定專精:";
|
||||
EQUIPMENT_SET_EDIT = "改變名稱/圖示";
|
||||
EQUIPMENT_SET_SETTINGS = "設定";
|
||||
EQUIPSET_EQUIP = "裝備";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "裝備切換失敗。物品欄已滿。";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "%s設定中的某些物品必須等到你脫離戰鬥後才能更換。";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "%s設定中的一個或多個物品未尋獲,因此無法裝備。";
|
||||
CLEAR_FOCUS = "清除專注目標";
|
||||
SET_FOCUS = "設定專注目標";
|
||||
FOCUSTARGET = "設定鎖定目標";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
ITEMS_EQUIPPED = "裝備%d件物品";
|
||||
@@ -12,9 +37,14 @@ ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
TARGETFOCUS = "選定鎖定單位";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "銅";
|
||||
GEARSETS_POPUP_TEXT = "輸入設定名稱(最多16個字元):";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "金";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "所有圖示";
|
||||
ICON_FILTER_ITEM = "物品";
|
||||
ICON_FILTER_SPELL = "法術";
|
||||
ICON_SELECTION_TITLE_CURRENT = "目前選擇";
|
||||
ITEM_DELTA_DESCRIPTION = "若你更換此裝備,下列屬性將會改變:";
|
||||
ITEM_MOD_MANA_SHORT = "法力";
|
||||
ITEM_MOD_HEALTH_SHORT = "生命力";
|
||||
@@ -42,7 +72,11 @@ ITEM_MOD_BLOCK_VALUE_SHORT = "格擋值";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒傷害";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "裝備管理員";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "新設定";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "設定%s的註記:";
|
||||
SET_NOTE = "設定註記";
|
||||
SILVER_AMOUNT_SYMBOL = "銀";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/清除專注目標";
|
||||
|
||||
@@ -1018,7 +1018,7 @@ end
|
||||
|
||||
local function _UpdateFrameStackTooltip(tooltip, showHidden)
|
||||
tooltip:ClearLines();
|
||||
tooltip:AddLine("Frame Stack", 1, 1, 1);
|
||||
tooltip:AddLine(DEBUG_FRAMESTACK or "Frame Stack", 1, 1, 1);
|
||||
local matches = _CollectFramesUnderMouse(showHidden);
|
||||
local count = table.getn(matches);
|
||||
if (count == 0) then
|
||||
|
||||
+43
-28
@@ -25,6 +25,39 @@ set(TINYCBOR_SOURCES
|
||||
external/tinycbor/src/cborparser_float.c
|
||||
external/tinycbor/src/cborerrorstrings.c)
|
||||
|
||||
# Version derivation. Dev builds use a sentinel "newer than every
|
||||
# plausible release" so addon-side `CLASSIC_API_VERSION >= X` gates
|
||||
# always pass locally. CI release builds pass `-DCLASSICAPI_TAG=vX.Y.Z`,
|
||||
# which drives BOTH the compiled `CLASSIC_API_VERSION` int AND the
|
||||
# embedded addon's `## Version:` toc line (substituted in
|
||||
# embed_addon.cmake). The on-disk source toc stays `## Version: DEV`;
|
||||
# only the embedded copy is stamped, so a release build needs nothing
|
||||
# but the flag — no source edit.
|
||||
set(CLASSICAPI_VERSION_VALUE 99999999)
|
||||
set(CLASSICAPI_VER_MAJOR 9999)
|
||||
set(CLASSICAPI_VER_MINOR 99)
|
||||
set(CLASSICAPI_VER_PATCH 99)
|
||||
set(CLASSICAPI_TOC_VERSION "DEV")
|
||||
|
||||
set(CLASSICAPI_TAG
|
||||
""
|
||||
CACHE STRING "Release tag like v1.2.3")
|
||||
|
||||
if(CLASSICAPI_TAG MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)$")
|
||||
set(CLASSICAPI_VER_MAJOR "${CMAKE_MATCH_1}")
|
||||
set(CLASSICAPI_VER_MINOR "${CMAKE_MATCH_2}")
|
||||
set(CLASSICAPI_VER_PATCH "${CMAKE_MATCH_3}")
|
||||
math(EXPR CLASSICAPI_VERSION_VALUE
|
||||
"${CLASSICAPI_VER_MAJOR} * 10000 + ${CLASSICAPI_VER_MINOR} * 100 + ${CLASSICAPI_VER_PATCH}")
|
||||
set(CLASSICAPI_TOC_VERSION
|
||||
"${CLASSICAPI_VER_MAJOR}.${CLASSICAPI_VER_MINOR}.${CLASSICAPI_VER_PATCH}")
|
||||
elseif(CLASSICAPI_TAG)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Invalid CLASSICAPI_TAG: ${CLASSICAPI_TAG} (expected vMAJOR.MINOR.PATCH)"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Bundle `AddOns/!!!ClassicAPI/` into a generated header so the DLL can
|
||||
# surface the addon to the engine even when the user doesn't have it
|
||||
# installed on disk. The file-read hook in `src/addons/Embedded.cpp`
|
||||
@@ -34,12 +67,16 @@ set(EMBEDDED_CLASSICAPI_SRC "${CMAKE_CURRENT_SOURCE_DIR}/AddOns/!!!ClassicAPI")
|
||||
set(EMBEDDED_CLASSICAPI_HEADER "${CMAKE_BINARY_DIR}/generated/embedded_classicapi.h")
|
||||
file(GLOB_RECURSE EMBEDDED_CLASSICAPI_FILES CONFIGURE_DEPENDS
|
||||
"${EMBEDDED_CLASSICAPI_SRC}/*")
|
||||
# The `.classicapi-dev` marker is a disk-only developer signal — keep it
|
||||
# out of the embed inputs (embed_addon.cmake filters it too).
|
||||
list(FILTER EMBEDDED_CLASSICAPI_FILES EXCLUDE REGEX "/\\.classicapi-dev$")
|
||||
add_custom_command(
|
||||
OUTPUT "${EMBEDDED_CLASSICAPI_HEADER}"
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DSRC_DIR=${EMBEDDED_CLASSICAPI_SRC}
|
||||
-DOUT_HEADER=${EMBEDDED_CLASSICAPI_HEADER}
|
||||
-DADDON_NAME=ClassicAPI
|
||||
-DADDON_VERSION=${CLASSICAPI_TOC_VERSION}
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_addon.cmake"
|
||||
DEPENDS
|
||||
${EMBEDDED_CLASSICAPI_FILES}
|
||||
@@ -71,33 +108,6 @@ add_library(${PROJECT_NAME} SHARED
|
||||
${EMBEDDED_CLASSICAPI_HEADER}
|
||||
${EMBEDDED_BINDINGS_HEADER})
|
||||
|
||||
# Dev-build sentinel — represents "newer than every plausible
|
||||
# release". Encoded as v9999.99.99 = 9999 * 10000 + 99 * 100 + 99 =
|
||||
# 99,999,999. Picked so addon-side feature gates of the form
|
||||
# `CLASSIC_API_VERSION >= X` always pass during local development;
|
||||
# CI release builds overwrite it via `-DCLASSICAPI_TAG=vX.Y.Z`.
|
||||
set(CLASSICAPI_VERSION_VALUE 99999999)
|
||||
set(CLASSICAPI_VER_MAJOR 9999)
|
||||
set(CLASSICAPI_VER_MINOR 99)
|
||||
set(CLASSICAPI_VER_PATCH 99)
|
||||
|
||||
set(CLASSICAPI_TAG
|
||||
""
|
||||
CACHE STRING "Release tag like v1.2.3")
|
||||
|
||||
if(CLASSICAPI_TAG MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)$")
|
||||
set(CLASSICAPI_VER_MAJOR "${CMAKE_MATCH_1}")
|
||||
set(CLASSICAPI_VER_MINOR "${CMAKE_MATCH_2}")
|
||||
set(CLASSICAPI_VER_PATCH "${CMAKE_MATCH_3}")
|
||||
math(EXPR CLASSICAPI_VERSION_VALUE
|
||||
"${CLASSICAPI_VER_MAJOR} * 10000 + ${CLASSICAPI_VER_MINOR} * 100 + ${CLASSICAPI_VER_PATCH}")
|
||||
elseif(CLASSICAPI_TAG)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Invalid CLASSICAPI_TAG: ${CLASSICAPI_TAG} (expected vMAJOR.MINOR.PATCH)"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_compile_definitions(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE CLASSICAPI_VERSION_VALUE=${CLASSICAPI_VERSION_VALUE})
|
||||
@@ -134,5 +144,10 @@ set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "ClassicAPI")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MT")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "/MT /O2 /Ob2 /DNDEBUG")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS
|
||||
"/OPT:REF /OPT:ICF /MERGE:.rdata=.text /MERGE:.pdata=.text /SECTION:.text,EWR /NODEFAULTLIB:MSVCRT"
|
||||
# /MAP emits build/Release/ClassicAPI.map — a linker-only output (no effect
|
||||
# on code layout) that maps `0001:offset` addresses from WoW's ERROR #132
|
||||
# crash dumps back to functions. Published as a build/release artifact so
|
||||
# any crash from a shipped DLL can be triaged exactly. (/OPT:ICF folds
|
||||
# identical functions, so a few map symbols share an address — harmless.)
|
||||
"/OPT:REF /OPT:ICF /MERGE:.rdata=.text /MERGE:.pdata=.text /SECTION:.text,EWR /NODEFAULTLIB:MSVCRT /MAP"
|
||||
)
|
||||
|
||||
@@ -35,8 +35,8 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `frame:SetShown`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `GetMouseFoci`, `frame:SetShown`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
|
||||
@@ -49,7 +49,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
|
||||
| [Lua](docs/API.md#lua) | `coroutine.create`, `coroutine.resume`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `string.gmatch`, `string.match`, `string.reverse`, `strjoin`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.count`, `table.wipe` |
|
||||
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapOverlays`, `C_Map.GetMapWorldSize` |
|
||||
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
|
||||
@@ -59,8 +59,8 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
|
||||
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GetClass`, `C_PlayerInfo.GetName`, `C_PlayerInfo.GetRace`, `C_PlayerInfo.GetSex`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `C_PlayerInfo.IsConnected` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsOutdoors`, `IsStealthed`, `IsSwimming` |
|
||||
| [System](docs/API.md#system) | `CopyToClipboard`, `GetPhysicalScreenSize` |
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
@@ -71,7 +71,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `UnitClassBase`, `UnitCreatureFamilyID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSpellHaste`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifier`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration` |
|
||||
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
|
||||
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
|
||||
|
||||
@@ -188,6 +188,8 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| Inventory type | `Enum.InventoryType.Index*Type` (0–34, e.g. `IndexHeadType`=1 … `IndexRelicType`=28) |
|
||||
| Item class | `Enum.ItemClass.{Consumable,Container,Weapon,Gem,Armor,Reagent,Projectile,Tradegoods,ItemEnhancement,Recipe,Quiver,Questitem,Key,Miscellaneous,…}` (0–19) |
|
||||
| Item quality | `Enum.ItemQuality.{Poor,Common,Uncommon,Rare,Epic,Legendary,Artifact}` (0–6) |
|
||||
| Spellbook bank | `Enum.SpellBookSpellBank.{Player,Pet}` (0–1) |
|
||||
| Spellbook item type | `Enum.SpellBookItemType.{None,Spell,FutureSpell,PetAction,Flyout}` (0–4; 1.12 only yields `Spell`/`PetAction`) |
|
||||
|
||||
### Unit tokens
|
||||
|
||||
|
||||
+24
-1
@@ -34,6 +34,10 @@ if(NOT ADDON_NAME)
|
||||
endif()
|
||||
|
||||
file(GLOB_RECURSE INPUT_FILES RELATIVE "${SRC_DIR}" "${SRC_DIR}/*")
|
||||
# Never embed the developer marker — it must reflect on-disk presence
|
||||
# only (see src/addons/Embedded.cpp DiskHasDevMarker), and it is
|
||||
# gitignored so it only exists in a developer working tree anyway.
|
||||
list(FILTER INPUT_FILES EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
|
||||
list(SORT INPUT_FILES) # deterministic codegen
|
||||
|
||||
set(BODY "")
|
||||
@@ -41,7 +45,26 @@ set(MANIFEST "")
|
||||
set(INDEX 0)
|
||||
foreach(REL_PATH ${INPUT_FILES})
|
||||
set(ABS_PATH "${SRC_DIR}/${REL_PATH}")
|
||||
file(READ "${ABS_PATH}" CONTENT_HEX HEX)
|
||||
|
||||
# Stamp the addon's `## Version:` line from the build flag rather
|
||||
# than the committed source (which stays `DEV`). This is what makes
|
||||
# a release build a pure function of `-DCLASSICAPI_TAG` — the source
|
||||
# toc is never edited. Only the `.toc` file is rewritten, in-memory,
|
||||
# before hex-encoding; every other file is embedded verbatim.
|
||||
if(ADDON_VERSION AND REL_PATH MATCHES "\\.toc$")
|
||||
file(READ "${ABS_PATH}" TOC_TEXT)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
"## Version: ${ADDON_VERSION}" TOC_TEXT "${TOC_TEXT}")
|
||||
# Round-trip through a temp file to hex-encode the modified text
|
||||
# (portable to CMake 3.10; avoids requiring string(HEX), 3.18+).
|
||||
set(TOC_TMP "${OUT_HEADER}.toc.tmp")
|
||||
file(WRITE "${TOC_TMP}" "${TOC_TEXT}")
|
||||
file(READ "${TOC_TMP}" CONTENT_HEX HEX)
|
||||
file(REMOVE "${TOC_TMP}")
|
||||
else()
|
||||
file(READ "${ABS_PATH}" CONTENT_HEX HEX)
|
||||
endif()
|
||||
|
||||
string(LENGTH "${CONTENT_HEX}" HEX_LEN)
|
||||
math(EXPR FILE_SIZE "${HEX_LEN} / 2")
|
||||
|
||||
|
||||
+604
-163
@@ -131,6 +131,7 @@ build instructions.
|
||||
- [`UNIT_FACTION` event (fire-coverage fix)](#unit_faction-event-fire-coverage-fix)
|
||||
- [`UPDATE_MOUSEOVER_UNIT` event (loss-fire fix)](#update_mouseover_unit-event-loss-fire-fix)
|
||||
- [`UPDATE_SHAPESHIFT_FORM` event](#update_shapeshift_form-event)
|
||||
- [`UNIT_SPELLCAST_*` events](#unit_spellcast_-events)
|
||||
|
||||
- [Expansion](#expansion)
|
||||
- [`GetClassicExpansionLevel()`](#getclassicexpansionlevel)
|
||||
@@ -159,6 +160,7 @@ build instructions.
|
||||
- [`region:IsMouseOver([topOffset, bottomOffset, leftOffset, rightOffset])`](#regionismouseovertopoffset-bottomoffset-leftoffset-rightoffset)
|
||||
- [`region:GetRect()`](#regiongetrect)
|
||||
- [`region:IsDragging()`](#regionisdragging)
|
||||
- [`GetMouseFoci()`](#getmousefoci)
|
||||
- [`frame:SetShown(shown)`](#framesetshownshown)
|
||||
- [`frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])`](#framesetresizeboundsminwidth-minheight--maxwidth-maxheight)
|
||||
- [`frame:HookScript(scriptType, handler)`](#framehookscriptscripttype-handler)
|
||||
@@ -170,6 +172,17 @@ build instructions.
|
||||
- [FriendList](#friendlist)
|
||||
- [`C_FriendList.SendWhoQueryByName(name)`](#c_friendlistsendwhoquerybynamename)
|
||||
- [`C_FriendList.IsWhoQueryPending()`](#c_friendlistiswhoquerypending)
|
||||
- [`C_FriendList.GetNumWhoResults()`](#c_friendlistgetnumwhoresults)
|
||||
- [`C_FriendList.GetWhoInfo(index)`](#c_friendlistgetwhoinfoindex)
|
||||
- [`C_FriendList.IsFriend(guid)`](#c_friendlistisfriendguid)
|
||||
- [`C_FriendList.IsIgnored(token)`](#c_friendlistisignoredtoken)
|
||||
- [`C_FriendList.IsIgnoredByGuid(guid)`](#c_friendlistisignoredbyguidguid)
|
||||
- [`C_FriendList.GetNumFriends()`](#c_friendlistgetnumfriends)
|
||||
- [`C_FriendList.GetNumOnlineFriends()`](#c_friendlistgetnumonlinefriends)
|
||||
- [`C_FriendList.GetFriendInfo(name)`](#c_friendlistgetfriendinfoname)
|
||||
- [`C_FriendList.GetFriendInfoByIndex(index)`](#c_friendlistgetfriendinfobyindexindex)
|
||||
- [`C_FriendList.SetFriendNotes(name, notes)`](#c_friendlistsetfriendnotesname-notes)
|
||||
- [`C_FriendList.SetFriendNotesByIndex(index, notes)`](#c_friendlistsetfriendnotesbyindexindex-notes)
|
||||
|
||||
- [GameObject](#gameobject)
|
||||
- [`C_GameObjectInfo.GetGameObjectInfoByID(gameObjectID)`](#c_gameobjectinfogetgameobjectinfobyidgameobjectid)
|
||||
@@ -204,6 +217,8 @@ build instructions.
|
||||
- [`Enum.ItemClass`](#enumitemclass)
|
||||
- [`Enum.ItemQuality`](#enumitemquality)
|
||||
- [`Enum.PowerType`](#enumpowertype)
|
||||
- [`Enum.SpellBookSpellBank`](#enumspellbookspellbank)
|
||||
- [`Enum.SpellBookItemType`](#enumspellbookitemtype)
|
||||
|
||||
- [Glue](#glue)
|
||||
- [`C_Glue.IsFirstLoadThisSession()`](#c_glueisfirstloadthissession)
|
||||
@@ -335,6 +350,7 @@ build instructions.
|
||||
- [`CastSpellNoToggle` as a macro cast line](#castspellnotoggle-as-a-macro-cast-line)
|
||||
- [`GetMacroSpell(macroSlot)`](#getmacrospellmacroslot)
|
||||
- [`GetMacroIcons` / `GetMacroItemIcons` / `GetLooseMacroIcons` / `GetLooseMacroItemIcons`](#getmacroicons--getmacroitemicons--getloosemacroicons--getloosemacroitemicons)
|
||||
- [`C_Macro.CreateMacro` / `C_Macro.EditMacro`](#c_macrocreatemacro--c_macroeditmacro)
|
||||
|
||||
- [Mail](#mail)
|
||||
- [`GetSendMailItemLink([attachmentIndex])`](#getsendmailitemlinkattachmentindex)
|
||||
@@ -416,6 +432,7 @@ build instructions.
|
||||
- [`C_Spell.GetSpellDescription(spellID)`](#c_spellgetspelldescriptionspellid)
|
||||
- [`C_Spell.GetSpellMechanicByID(spellID)`](#c_spellgetspellmechanicbyidspellid)
|
||||
- [`C_Spell.GetSpellEffectMechanics(spellID)`](#c_spellgetspelleffectmechanicsspellid)
|
||||
- [`C_Spell.GetSpellDispelType(spellID)`](#c_spellgetspelldispeltypespellid)
|
||||
- [`C_Spell.GetSpellRadius(spellID)` / `GetSpellRadius(slot, bookType)`](#c_spellgetspellradiusspellid--getspellradiusslot-booktype)
|
||||
- [`C_Spell.GetSpellPowerCost(spellIdentifier)`](#c_spellgetspellpowercostspellidentifier)
|
||||
- [`C_Spell.GetSpellReagents(spellID)`](#c_spellgetspellreagentsspellid)
|
||||
@@ -436,6 +453,8 @@ build instructions.
|
||||
- [`C_Spell.IsSpellInRange(spellIdentifier, targetUnit)`](#c_spellisspellinrangespellidentifier-targetunit)
|
||||
- [`C_Spell.IsAutoAttackSpell(spellID)`](#c_spellisautoattackspellspellid)
|
||||
- [`C_Spell.IsRangedAutoAttackSpell(spellID)`](#c_spellisrangedautoattackspellspellid)
|
||||
- [`C_Spell.IsNextMeleeSpell(spellID)`](#c_spellisnextmeleespellspellid)
|
||||
- [`C_Spell.ResetsMeleeSwing(spellID)`](#c_spellresetsmeleeswingspellid)
|
||||
- [`IsHarmfulSpell(spell)` / `IsHelpfulSpell(spell)`](#isharmfulspellspell--ishelpfulspellspell)
|
||||
- [`C_Spell.IsSpellHarmful(spellID)` / `C_Spell.IsSpellHelpful(spellID)`](#c_spellisspellharmfulspellid--c_spellisspellhelpfulspellid)
|
||||
- [`GetSpellSchool(spellID)`](#getspellschoolspellid)
|
||||
@@ -445,12 +464,12 @@ build instructions.
|
||||
- [`C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`](#c_spellcancelspellbyidspellid--cancelspellbynamename)
|
||||
- [`C_Spell.UnitCastingInfo(unit)` / `C_Spell.CastingInfo()`](#c_spellunitcastinginfounit--c_spellcastinginfo)
|
||||
- [`C_Spell.UnitChannelInfo(unit)` / `C_Spell.ChannelInfo()`](#c_spellunitchannelinfounit--c_spellchannelinfo)
|
||||
- [`UNIT_SPELLCAST_*` events (player)](#unit_spellcast_-events-player)
|
||||
- [`C_Spell.GetSpellLevelInfo(spellID)`](#c_spellgetspelllevelinfospellid)
|
||||
- [`GetSpellRequiredTargetLevel(spellID)`](#getspellrequiredtargetlevelspellid)
|
||||
|
||||
- [SpellBook](#spellbook)
|
||||
- [`FindSpellBookSlotByID(spellID)`](#findspellbookslotbyidspellid)
|
||||
- [`C_SpellBook.GetSpellBookItemInfo(slotIndex, spellBank)`](#c_spellbookgetspellbookiteminfoslotindex-spellbank)
|
||||
- [`C_SpellBook.GetSpellLevelLearned(spellID)`](#c_spellbookgetspelllevellearnedspellid)
|
||||
- [`C_SpellBook.GetCurrentLevelSpells([level])`](#c_spellbookgetcurrentlevelspellslevel)
|
||||
- [`C_SpellBook.GetSkillLineName(skillLineID)`](#c_spellbookgetskilllinenameskilllineid)
|
||||
@@ -562,7 +581,7 @@ build instructions.
|
||||
- [`C_UnitAuras.GetPlayerAuraBySpellID(spellID)`](#c_unitaurasgetplayeraurabyspellidspellid)
|
||||
- [`C_UnitAuras.GetAuraDataBySpellName(unit, spellName [, filter])`](#c_unitaurasgetauradatabyspellnameunit-spellname--filter)
|
||||
- [`C_UnitAuras.RegisterComboDuration(spellID, baseSeconds, maxSeconds)`](#c_unitaurasregistercombodurationspellid-baseseconds-maxseconds)
|
||||
- [`C_UnitAuras.RegisterAuraDurationModifier(triggerSpellID, affectedFamily, affectedFamilyFlags, affectedIcon, op [, valueSeconds])`](#c_unitaurasregisterauradurationmodifiertriggerspellid-affectedfamily-affectedfamilyflags-affectedicon-op--valueseconds)
|
||||
- [`C_UnitAuras.RegisterAuraDurationModifierByTrigger(triggerFamily, triggerSchool, affectedFamily, affectedFamilyFlags, affectedIcon, op [, valueSeconds])`](#c_unitaurasregisterauradurationmodifierbytriggertriggerfamily-triggerschool-affectedfamily-affectedfamilyflags-affectedicon-op--valueseconds)
|
||||
- [`C_UnitAuras.GetUnitAuras(unit [, filter])`](#c_unitaurasgetunitaurasunit--filter)
|
||||
- [`C_UnitAuras.GetAuraDispelTypeColor(dispelName)`](#c_unitaurasgetauradispeltypecolordispelname)
|
||||
|
||||
@@ -3228,6 +3247,131 @@ The cached "last form" sentinel uses `-1` for "player descriptor not
|
||||
yet resolvable" so transient resolution failures during early login
|
||||
don't get misread as leaving a form.
|
||||
|
||||
### `UNIT_SPELLCAST_*` events
|
||||
|
||||
Backport of the TBC+ cast/channel events to 1.12, for the **local player and
|
||||
other units**. Ported cast-bar / rotation addons (anything written against
|
||||
the modern signature) register these instead of vanilla's arg-less
|
||||
`SPELLCAST_*` events and read `unit, castGUID, spellID` directly. Thirteen
|
||||
events are provided; six also fire for non-player units:
|
||||
|
||||
| Event | Fires when | Units | Args |
|
||||
|-------|-----------|-------|------|
|
||||
| `UNIT_SPELLCAST_SENT` | `CMSG_CAST_SPELL` leaves the client (earliest point) | player | `unit, target, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_START` | a cast-time spell begins | all | `unit, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_STOP` | a cast-time spell ends (any reason) | all | same |
|
||||
| `UNIT_SPELLCAST_DELAYED` | pushback extends the cast | player | same |
|
||||
| `UNIT_SPELLCAST_SUCCEEDED` | the spell goes off (`SMSG_SPELL_GO`) — incl. instants | all | same |
|
||||
| `UNIT_SPELLCAST_INTERRUPTED` | a started **cast** is interrupted (kick, movement, LoS) — never for channels | all | same |
|
||||
| `UNIT_SPELLCAST_FAILED` | a cast is rejected before it starts (range, mana, cooldown) with an error shown | player | same |
|
||||
| `UNIT_SPELLCAST_FAILED_QUIET` | a cast fails with **no** error shown (spammy retry, reticle cancel, …) | player | same |
|
||||
| `UNIT_SPELLCAST_CHANNEL_START` | a channel begins | all | same |
|
||||
| `UNIT_SPELLCAST_CHANNEL_UPDATE` | pushback shortens a channel | player | same |
|
||||
| `UNIT_SPELLCAST_CHANNEL_STOP` | a channel ends | all | same |
|
||||
| `UNIT_SPELLCAST_RETICLE_TARGET` | a ground-target reticle appears (AoE placement — Blizzard, Flare, …) | player | `unit, "", spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_RETICLE_CLEAR` | the reticle is placed or cancelled | player | `unit, "", spellID, spellName, rank` |
|
||||
|
||||
`unit` (arg1) is the token of the casting unit — `"player"` for your own
|
||||
casts, or a unit token (`"target"`, `"focus"`, `"party3"`, `"nameplate2"`,
|
||||
`"pet"`, `"mouseover"`, …) for another unit. `spellName` / `rank` are
|
||||
ClassicAPI tail extensions (modern stops at `spellID`); addons reading only
|
||||
the first three positional args are unaffected.
|
||||
|
||||
**Per-token fan-out.** A caster GUID can map to several tokens at once (your
|
||||
`target` is also `party2` and `nameplate1`). Like retail, the event fires
|
||||
**once per token** currently pointing at the caster, so a `target`-frame
|
||||
cast bar, a `party2` frame, and a nameplate cast bar each get their own
|
||||
event with the same castGUID. Tokens are resolved fresh at fire time (they
|
||||
shift frame-to-frame). If you run **SuperWoW**, its raw-GUID token (`"0x…"`)
|
||||
is deliberately filtered out — only standard tokens are fanned out.
|
||||
|
||||
**Non-player limits.** Only the six events above fire for other units, and
|
||||
they're **best-effort** — driven purely by the packets an observer receives:
|
||||
- `SENT` never fires (only your own outgoing casts are visible).
|
||||
- `DELAYED` / `CHANNEL_UPDATE` never fire (pushback is sent only to the
|
||||
caster, so another unit's bar can't stretch/shrink from damage).
|
||||
- `FAILED` never fires (a pre-cast requirement failure is client-local).
|
||||
- A remote cast/channel only has timing from the moment its
|
||||
`SMSG_SPELL_START` was observed; casters who were already casting when
|
||||
they came into range have no start time.
|
||||
|
||||
**Channels never fire `INTERRUPTED`.** Retail emits only `CHANNEL_STOP` when a
|
||||
channel ends, whether it completed or was cut short (verified against retail),
|
||||
so ClassicAPI matches that for both the player and other units. `INTERRUPTED`
|
||||
is a cast-only event.
|
||||
|
||||
**Reticle events** fire for ground-targeted (AoE) spells only, always for the
|
||||
player: `RETICLE_TARGET` when the placement reticle comes up, `RETICLE_CLEAR`
|
||||
when it's placed or cancelled. There's no cast yet, so the castGUID slot
|
||||
(arg2) is empty — retail pushes `nil` there, but the engine's event
|
||||
dispatcher can't emit a `nil` mid-argument-list, so ClassicAPI pushes `""`
|
||||
instead. `unit` (arg1) and `spellID` (arg3) are exact; arg2 is the only
|
||||
difference and is inconsequential for a reticle.
|
||||
|
||||
**castGUID.** A synthesized string in the modern shape
|
||||
`Cast-<type>-<serverID>-<instanceID>-<zoneUID>-<spellID>-<castUID>`. Vanilla
|
||||
can't know server / instance / zone, so those three fields are `0`; the
|
||||
load-bearing parts are the `spellID` (field 6, which addons `strsplit("-")`
|
||||
out) and a unique-per-cast `castUID` (field 7). **Every event of one cast
|
||||
carries the same castGUID**, so `SENT` → `START`/`CHANNEL_START` →
|
||||
`SUCCEEDED` → `STOP`/`CHANNEL_STOP` all pair up — including across the caster
|
||||
and observers (they converge on the same value), and a chained same-spell
|
||||
recast gets its own castUID. The `type` and `castUID` follow the
|
||||
[spell-cast-GUID spec](https://warcraft.wiki.gg/wiki/GUID#Cast):
|
||||
- **Type 3** (real casts — the common case): `castUID` is time-based — the
|
||||
low 23 bits are the cast's UNIX-epoch second, the higher bits a per-second
|
||||
counter.
|
||||
- **Type 2** (`UNIT_SPELLCAST_FAILED` — a local-only cast that never reached
|
||||
the server): `castUID` is a plain locally-incrementing integer.
|
||||
|
||||
**Ordering** matches modern:
|
||||
|
||||
- Cast-time spell: `SENT → START → SUCCEEDED → STOP`.
|
||||
- Channel: `SENT → CHANNEL_START → SUCCEEDED → CHANNEL_STOP` (CHANNEL_START
|
||||
before SUCCEEDED, as on retail).
|
||||
- Instant: `SENT → SUCCEEDED`.
|
||||
|
||||
**INTERRUPTED vs FAILED vs FAILED_QUIET** follow modern's split: a spell that
|
||||
never started (out of range, not enough mana, on cooldown, LoS to a target)
|
||||
fires `FAILED`, except for a fixed whitelist of "quiet" `SpellCastResult`
|
||||
codes that fire `FAILED_QUIET` instead — `SPELL_IN_PROGRESS` (casting while
|
||||
already casting / a spell-queue rejection), `DONT_REPORT` (fake fails, a
|
||||
cancelled ground reticle), and `CHARMED`. That whitelist mirrors the 3.3.5
|
||||
client's own unit-spellcast dispatch, mapped to vanilla's `SpellCastResult`
|
||||
enum. A spell that was *already casting* and gets stopped (an enemy kick,
|
||||
moving to cancel, breaking LoS mid-cast) fires `INTERRUPTED`. Holding
|
||||
the cast key while running fires `INTERRUPTED` repeatedly (once per retry),
|
||||
each reusing the interrupted cast's castGUID — matching retail.
|
||||
|
||||
**Channel pushback (player).** Taking damage while channeling shortens the
|
||||
channel in vanilla; `CHANNEL_UPDATE` fires on each hit and
|
||||
[`C_Spell.UnitChannelInfo`](#c_spellunitchannelinfounit--c_spellchannelinfo)'s
|
||||
`endTimeMs` re-anchors to the server's new remaining time, so cast bars
|
||||
shrink correctly. (The event carries no time — like retail it's a "re-read
|
||||
now" trigger; timing is read back from `UnitChannelInfo`.)
|
||||
|
||||
Every fire is gated on whether any frame is registered for that event, so
|
||||
the whole system costs one pointer-compare per state transition when no
|
||||
addon uses it (no arg synthesis, no DBC lookups, no per-token fan-out).
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
for _, e in ipairs({
|
||||
"UNIT_SPELLCAST_START", "UNIT_SPELLCAST_STOP",
|
||||
"UNIT_SPELLCAST_SUCCEEDED", "UNIT_SPELLCAST_CHANNEL_START",
|
||||
}) do f:RegisterEvent(e) end
|
||||
f:SetScript("OnEvent", function()
|
||||
-- vanilla passes event/arg1/... as globals, not function params
|
||||
if arg1 == "target" then print(event, arg3) end -- arg3 = spellID
|
||||
end)
|
||||
```
|
||||
|
||||
> **Additive to the vanilla `SPELLCAST_*` events.** The engine's own arg-less
|
||||
> `SPELLCAST_START` / `SPELLCAST_CHANNEL_UPDATE` / … still fire as before;
|
||||
> these `UNIT_`-prefixed events are the modern layer on top. The empowered-cast
|
||||
> events (`UNIT_SPELLCAST_EMPOWER_*`, a Dragonflight addition) are not
|
||||
> implemented — vanilla has no empowered casts.
|
||||
|
||||
## Expansion
|
||||
|
||||
Helpers shipped by modern Classic Era / Cata Classic for addons that
|
||||
@@ -3718,6 +3862,20 @@ fontstrings) always report `false`, since only frames can be dragged.
|
||||
if self:IsDragging() then ... end
|
||||
```
|
||||
|
||||
### `GetMouseFoci()`
|
||||
|
||||
Returns a table of the mouse-enabled frames under the cursor, from top
|
||||
to bottom. Index 1 is the frame that `GetMouseFocus()` returns, so
|
||||
`GetMouseFocus() == GetMouseFoci()[1]` is always true.
|
||||
|
||||
```lua
|
||||
local foci = GetMouseFoci()
|
||||
-- foci[1] == GetMouseFocus()
|
||||
for i = 1, table.getn(foci) do
|
||||
print(foci[i]:GetName() or "<anonymous>")
|
||||
end
|
||||
```
|
||||
|
||||
### `frame:SetShown(shown)`
|
||||
|
||||
`Show()` if `shown` is truthy, `Hide()` otherwise. Registered for
|
||||
@@ -4042,6 +4200,226 @@ contribute to the same pending window. This is acceptable for the
|
||||
addons that need per-call tracking should manage their own ticket
|
||||
state on top.
|
||||
|
||||
### `C_FriendList.GetNumWhoResults()`
|
||||
|
||||
Returns two numbers: how many /who results you can read, and a
|
||||
server-reported count.
|
||||
|
||||
```lua
|
||||
local shown, serverCount = C_FriendList.GetNumWhoResults()
|
||||
```
|
||||
|
||||
`shown` is the number of entries you can index with
|
||||
[`GetWhoInfo`](#c_friendlistgetwhoinfoindex). The server caps the list
|
||||
(50, or 49 on some cores), so `shown` is at most that. Run a `/who` (or
|
||||
[`SendWhoQueryByName`](#c_friendlistsendwhoquerybynamename)) first to
|
||||
populate the list.
|
||||
|
||||
`serverCount` is the second header value the server sends, and its
|
||||
meaning depends on the server core:
|
||||
|
||||
- Mangos and cmangos derived cores send the number of characters that
|
||||
matched your query.
|
||||
- Turtle WoW (tortoise-wow) sends the whole realm's online population
|
||||
when more than 49 players are online, ignoring your filter. Below
|
||||
that count it sends the match count.
|
||||
|
||||
Do not rely on one meaning across servers. On Turtle it is a live
|
||||
population counter for any query. On other cores it is a match total.
|
||||
|
||||
### `C_FriendList.GetWhoInfo(index)`
|
||||
|
||||
Returns a table for the /who result at `index` (1-based), or `nil` when
|
||||
the index is out of range.
|
||||
|
||||
```lua
|
||||
local info = C_FriendList.GetWhoInfo(1)
|
||||
-- {
|
||||
-- fullName = "Nadamage", fullGuildName = "",
|
||||
-- level = 26, raceStr = "Troll",
|
||||
-- classStr = "Mage", filename = "MAGE",
|
||||
-- area = "Hillsbrad Foothills",
|
||||
-- }
|
||||
```
|
||||
|
||||
Table fields:
|
||||
|
||||
- `fullName` — the character name.
|
||||
- `fullGuildName` — the guild name, or `""` when the player has no
|
||||
guild.
|
||||
- `level` — the character level.
|
||||
- `raceStr` — localized race name, or `nil` when unknown.
|
||||
- `classStr` — localized class name, or `nil` when unknown.
|
||||
- `filename` — locale-independent class token (`"WARRIOR"`, `"MAGE"`,
|
||||
…), or `nil` when unknown. This is the key for `RAID_CLASS_COLORS`.
|
||||
Vanilla's own `GetWhoInfo` does not provide it, which is the main
|
||||
reason to use this table form.
|
||||
- `area` — the zone name.
|
||||
|
||||
There is no `gender` field — vanilla's /who result stores no sex.
|
||||
|
||||
Vanilla also keeps the global `GetWhoInfo(index)`, which returns
|
||||
positional values and no class token. This namespaced form returns a
|
||||
table and adds `filename`.
|
||||
|
||||
### `C_FriendList.IsFriend(guid)`
|
||||
|
||||
Returns `true` if the player with the given GUID is on your friends
|
||||
list, `false` if not.
|
||||
|
||||
```lua
|
||||
C_FriendList.IsFriend(UnitGUID("target")) -- true if the target is a friend
|
||||
C_FriendList.IsFriend("0x00000000000ABCDE")
|
||||
```
|
||||
|
||||
`guid` is a GUID string — the `"0x…"` form that `UnitGUID` returns.
|
||||
You can also pass a plain character name, because vanilla's friends
|
||||
list is keyed by name (`AddFriend(name)`).
|
||||
|
||||
The check reads the engine's friends list directly. It matches the
|
||||
input GUID against each friend's stored GUID. The server sends that
|
||||
GUID for both online and offline friends. As a fallback it also
|
||||
compares character names, so the answer holds for any friend the
|
||||
client saw this session.
|
||||
|
||||
### `C_FriendList.IsIgnored(token)`
|
||||
|
||||
Returns `true` if the player is on your ignore list, `false` if not.
|
||||
|
||||
```lua
|
||||
C_FriendList.IsIgnored("Bob")
|
||||
C_FriendList.IsIgnored("0x00000000000ABCDE")
|
||||
```
|
||||
|
||||
`token` is a character name or a GUID string. The ignore list stores
|
||||
GUIDs, not names. A GUID always matches. A name matches only a player
|
||||
the client has seen this session — the same players `GetIgnoreName` can
|
||||
name — because the name comes from the client's name cache.
|
||||
|
||||
### `C_FriendList.IsIgnoredByGuid(guid)`
|
||||
|
||||
Returns `true` if the player with the given GUID is on your ignore
|
||||
list, `false` if not.
|
||||
|
||||
```lua
|
||||
C_FriendList.IsIgnoredByGuid(UnitGUID("target"))
|
||||
C_FriendList.IsIgnoredByGuid("0x00000000000ABCDE")
|
||||
```
|
||||
|
||||
`guid` is a GUID string — the `"0x…"` form that `UnitGUID` returns.
|
||||
This is the exact key the ignore list uses, so it works for every
|
||||
ignored player.
|
||||
|
||||
### `C_FriendList.GetNumFriends()`
|
||||
|
||||
Returns the number of players on your friends list. The vanilla global
|
||||
`GetNumFriends()` returns the same count — this is the modern
|
||||
namespaced form.
|
||||
|
||||
```lua
|
||||
C_FriendList.GetNumFriends() -- e.g. 12
|
||||
```
|
||||
|
||||
### `C_FriendList.GetNumOnlineFriends()`
|
||||
|
||||
Returns how many friends on your list are online. This is a subset of
|
||||
[`GetNumFriends`](#c_friendlistgetnumfriends), which counts online and
|
||||
offline friends together.
|
||||
|
||||
```lua
|
||||
C_FriendList.GetNumOnlineFriends() -- e.g. 3
|
||||
```
|
||||
|
||||
Vanilla stores no online count, so the value is the number of friends
|
||||
whose `connected` flag is set — the same flag
|
||||
[`GetFriendInfo`](#c_friendlistgetfriendinfoname) reports.
|
||||
|
||||
### `C_FriendList.GetFriendInfo(name)`
|
||||
|
||||
Returns a `FriendInfo` table for the friend with the given name, or
|
||||
`nil` if that name is not on your list. The name match is
|
||||
case-insensitive.
|
||||
|
||||
```lua
|
||||
local info = C_FriendList.GetFriendInfo("Sarahnity")
|
||||
-- {
|
||||
-- name = "Sarahnity", connected = true, level = 60,
|
||||
-- className = "Druid", classFilename = "DRUID", area = "Stratholme",
|
||||
-- guid = "0x000000003B9CB7BF",
|
||||
-- afk = false, dnd = false,
|
||||
-- mobile = false, referAFriend = false, rafLinkType = 0,
|
||||
-- }
|
||||
```
|
||||
|
||||
Table fields:
|
||||
|
||||
- `name` — the friend's character name.
|
||||
- `connected` — `true` when the friend is online.
|
||||
- `level` — the friend's level, or `0` when unknown.
|
||||
- `className` — localized class name, or `nil` when unknown.
|
||||
- `classFilename` — locale-independent class token (`"WARRIOR"`,
|
||||
`"MAGE"`, …), or `nil` when unknown. Present whenever `className`
|
||||
is. This is the key for `RAID_CLASS_COLORS` and other class tables,
|
||||
so you can color a friend's name without matching the localized
|
||||
class name.
|
||||
- `area` — localized zone name, resolved to the parent zone, or `nil`
|
||||
when unknown.
|
||||
- `guid` — the friend's GUID string.
|
||||
- `notes` — your note for this friend, or `nil` if none. Set it with
|
||||
[`SetFriendNotes`](#c_friendlistsetfriendnotesname-notes).
|
||||
- `afk` / `dnd` — the friend's status flags.
|
||||
- `mobile` / `referAFriend` / `rafLinkType` — always `false` / `false`
|
||||
/ `0`. Vanilla 1.12 has no mobile app, Recruit-A-Friend, or RAF link
|
||||
data.
|
||||
|
||||
Notes are a ClassicAPI addition — vanilla has no note field. ClassicAPI
|
||||
persists them per character in
|
||||
`WTF\Account\<acct>\<realm>\<char>\ClassicAPI_FriendNotes.txt`, local to
|
||||
this client (not stored on the server, not shared with other machines).
|
||||
|
||||
### `C_FriendList.GetFriendInfoByIndex(index)`
|
||||
|
||||
The same `FriendInfo` table, addressed by a 1-based list index instead
|
||||
of a name. Returns `nil` for an index below 1 or above
|
||||
[`GetNumFriends`](#c_friendlistgetnumfriends).
|
||||
|
||||
```lua
|
||||
for i = 1, C_FriendList.GetNumFriends() do
|
||||
local info = C_FriendList.GetFriendInfoByIndex(i)
|
||||
print(info.name, info.connected, info.className, info.area)
|
||||
end
|
||||
```
|
||||
|
||||
See [`C_FriendList.GetFriendInfo`](#c_friendlistgetfriendinfoname) for
|
||||
the field list.
|
||||
|
||||
### `C_FriendList.SetFriendNotes(name, notes)`
|
||||
|
||||
Sets your note for the friend with the given name. Pass an empty string
|
||||
or `nil` to clear it. There is no return value, and the name must
|
||||
already be on your friends list.
|
||||
|
||||
```lua
|
||||
C_FriendList.SetFriendNotes("Sarahnity", "raid healer")
|
||||
C_FriendList.SetFriendNotes("Sarahnity", "") -- clears the note
|
||||
```
|
||||
|
||||
The note is stored client-side and read back through the `notes` field
|
||||
of [`GetFriendInfo`](#c_friendlistgetfriendinfoname). Setting a note
|
||||
fires `FRIENDLIST_UPDATE` so the friends UI and note-aware addons
|
||||
refresh. See the persistence note under `GetFriendInfo`.
|
||||
|
||||
### `C_FriendList.SetFriendNotesByIndex(index, notes)`
|
||||
|
||||
The same as
|
||||
[`SetFriendNotes`](#c_friendlistsetfriendnotesname-notes), addressed by
|
||||
a 1-based list index instead of a name. An out-of-range index does
|
||||
nothing.
|
||||
|
||||
```lua
|
||||
C_FriendList.SetFriendNotesByIndex(1, "tank")
|
||||
```
|
||||
|
||||
## GameObject
|
||||
|
||||
The gameobject analog of [`C_CreatureInfo`](#creature) — reads the
|
||||
@@ -4958,6 +5336,32 @@ not modern's `ComboPoints` reuse of the same number.
|
||||
local rage = UnitPower("player", Enum.PowerType.Rage)
|
||||
```
|
||||
|
||||
### `Enum.SpellBookSpellBank`
|
||||
|
||||
Selects which spellbook a `C_SpellBook.*` slot query reads. Passed as the
|
||||
`spellBank` argument to
|
||||
[`C_SpellBook.GetSpellBookItemInfo`](#c_spellbookgetspellbookiteminfoslotindex-spellbank).
|
||||
|
||||
| Value | Field |
|
||||
|------:|----------|
|
||||
| `0` | `Player` |
|
||||
| `1` | `Pet` |
|
||||
|
||||
### `Enum.SpellBookItemType`
|
||||
|
||||
Categorizes a spellbook slot. Vanilla's spellbook only ever holds real
|
||||
spells, so `C_SpellBook.GetSpellBookItemInfo` returns `Spell` for a
|
||||
player-book slot and `PetAction` for a pet-book slot. The other values
|
||||
exist for signature parity with retail but never occur in 1.12.
|
||||
|
||||
| Value | Field | Notes |
|
||||
|------:|---------------|-------|
|
||||
| `0` | `None` | Empty slot. Never returned — an empty slot yields `nil` instead. |
|
||||
| `1` | `Spell` | A player-book spell. |
|
||||
| `2` | `FutureSpell` | A not-yet-learned trainer spell. Never occurs in 1.12. |
|
||||
| `3` | `PetAction` | A pet-book spell. |
|
||||
| `4` | `Flyout` | A flyout group — a later-expansion concept. Never occurs. |
|
||||
|
||||
## Glue
|
||||
|
||||
Backports of two `C_Glue` session helpers. `C_Glue` is registered on the glue
|
||||
@@ -8117,6 +8521,60 @@ MPQ will appear in both the loose and mpq lists).
|
||||
The vanilla legacy globals `GetNumMacroIcons` / `GetMacroIconInfo`
|
||||
remain available unchanged.
|
||||
|
||||
### `C_Macro.CreateMacro` / `C_Macro.EditMacro`
|
||||
|
||||
These functions create or edit a macro. You give the icon as a texture
|
||||
string — a full `Interface\Icons\<name>` path or a bare `<name>`
|
||||
basename — instead of a `GetMacroIconInfo` index.
|
||||
|
||||
```lua
|
||||
C_Macro.CreateMacro(name, iconTexture, body, isCharacterMacro) -> index
|
||||
C_Macro.EditMacro(index, name, iconTexture, body) -> index
|
||||
```
|
||||
|
||||
Why this exists: the vanilla `CreateMacro` / `EditMacro` globals take an
|
||||
icon index into the `GetMacroIconInfo` list. That list does not include
|
||||
`INV_*` item icons. As a result, the stock API cannot set an item icon,
|
||||
even though the engine stores and shows it correctly. These two functions
|
||||
write the icon basename directly. Any texture under `Interface\Icons\`,
|
||||
including an item icon, can be saved.
|
||||
|
||||
`CreateMacro(name, iconTexture, body, isCharacterMacro)`:
|
||||
- `name` — the macro name. This argument is necessary.
|
||||
- `iconTexture` — a string, for example `"INV_Sword_25"` or
|
||||
`"Interface\\Icons\\INV_Sword_25"`. The function removes the
|
||||
`Interface\Icons\` prefix, and adds it again when a caller reads the
|
||||
icon. A nil value sets no icon.
|
||||
- `body` — the macro text. A nil value sets an empty body.
|
||||
- `isCharacterMacro` — a true value puts the macro in the per-character
|
||||
tab. A false or nil value uses the general tab.
|
||||
- The function returns the 1-based index of the new macro. It returns nil
|
||||
when the list is full (18 macros per tab).
|
||||
|
||||
`EditMacro(index, name, iconTexture, body)`:
|
||||
- `index` — a 1-based slot number or a macro name.
|
||||
- `name` / `iconTexture` / `body` — a nil argument keeps that field
|
||||
unchanged.
|
||||
- The function returns the 1-based index of the macro. It returns nil when
|
||||
`index` does not match a macro that exists.
|
||||
|
||||
```lua
|
||||
-- item icon that legacy CreateMacro cannot set:
|
||||
local i = C_Macro.CreateMacro("Bag", "INV_Misc_Bag_08", "/say hi", false)
|
||||
-- change only the icon; name and body stay:
|
||||
C_Macro.EditMacro(i, nil, "INV_Potion_51", nil)
|
||||
-- edit by name:
|
||||
C_Macro.EditMacro("Bag", nil, nil, "/wave")
|
||||
```
|
||||
|
||||
The `iconTexture` argument must be a string. `CreateMacro` reads a number
|
||||
as no icon. `EditMacro` reads a number as no change. For the numeric-index
|
||||
path, use the legacy globals.
|
||||
|
||||
The legacy `CreateMacro` / `EditMacro` globals do not change. They stay
|
||||
index-only. String-icon callers use the `C_Macro` namespace instead.
|
||||
Edits persist across sessions, the same as edits in the Macro UI.
|
||||
|
||||
## Mail
|
||||
|
||||
Backports of modern WoW's mail-attachment link getters. Both follow
|
||||
@@ -9064,10 +9522,16 @@ sex, class) was reverse-engineered from the
|
||||
### `UnitNameFromGUID(guid)`
|
||||
|
||||
Returns `name, realm` for the player identified by `guid`, or `nil`
|
||||
if no player with that GUID has been encountered yet. Same lookup
|
||||
chain as [`GetPlayerInfoByGUID`](#getplayerinfobyguidguid) (engine
|
||||
NameCache, persistent on-disk fallback when enabled) — just narrower
|
||||
return shape for callers that only need the name.
|
||||
if no player with that GUID has been encountered yet.
|
||||
|
||||
It tries three sources in order:
|
||||
|
||||
1. The object manager — any currently-synced unit (a player you can
|
||||
see, your target, a party or raid member, an NPC in range).
|
||||
2. Your friends list — it keeps name and GUID for online and offline
|
||||
friends, so a friend resolves even when not synced and never seen
|
||||
in chat. Always on.
|
||||
3. The persistent on-disk name cache, when you have enabled it.
|
||||
|
||||
```lua
|
||||
local name, realm = UnitNameFromGUID(UnitGUID("target"))
|
||||
@@ -10140,6 +10604,21 @@ Reads `Spell.dbc` directly (`EffectMechanic[3]` at `+0x13C`), so it covers
|
||||
every spell the client knows — not just the player's spellbook — with no
|
||||
caching or network round-trip.
|
||||
|
||||
### `C_Spell.GetSpellDispelType(spellID)`
|
||||
|
||||
Returns the spell's dispel type from `Spell.dbc`: `1` = Magic,
|
||||
`2` = Curse, `3` = Disease, `4` = Poison. Returns `0` for a spell
|
||||
that nothing can dispel, and for an unknown spell.
|
||||
|
||||
```lua
|
||||
C_Spell.GetSpellDispelType(118) -- Polymorph → 1 (Magic)
|
||||
C_Spell.GetSpellDispelType(980) -- Curse of Agony → 2 (Curse)
|
||||
C_Spell.GetSpellDispelType(133) -- Fireball → 0 (none)
|
||||
```
|
||||
|
||||
Use the number to show what removes an effect, or to filter auras by
|
||||
dispel type. The values match the `DISPEL_*` set the server uses.
|
||||
|
||||
### `C_Spell.GetSpellRadius(spellID)` / `GetSpellRadius(slot, bookType)`
|
||||
|
||||
Returns `(baseRadius, modifiedRadius)` — the spell's AOE radius in yards,
|
||||
@@ -10624,15 +11103,50 @@ C_Spell.IsRangedAutoAttackSpell(5019) -- true (Shoot wand)
|
||||
C_Spell.IsRangedAutoAttackSpell(6603) -- false (melee)
|
||||
```
|
||||
|
||||
Tests `Spell.dbc.Attributes & 0x02` (the `SPELL_ATTR_AUTO_REPEAT`
|
||||
flag) — verified empirically against in-game spell data. Auto Shot
|
||||
(`0x00050012`) and Shoot wand (`0x00000012`) share bit 1; Heroic
|
||||
Strike (`0x00050014`) and other ranged abilities don't. Naturally
|
||||
covers any future auto-repeating spell a private server might add.
|
||||
Tests the auto-repeat flag in `Spell.dbc.AttributesEx2` (bit `0x20`).
|
||||
Only Auto Shot and Shoot carry it. The on-cast ranged abilities —
|
||||
Aimed Shot and Multi-Shot — set the general `RANGED` bit but not the
|
||||
auto-repeat flag, so they return `false`. The flag also covers any
|
||||
future auto-repeating spell a private server adds. Verified against
|
||||
`Spell.dbc`.
|
||||
|
||||
See [`C_SpellBook.IsRangedAutoAttackSpellBookItem`](#c_spellbookisrangedautoattackspellbookitemslot-booktype)
|
||||
for the spellbook-slot variant.
|
||||
|
||||
### `C_Spell.IsNextMeleeSpell(spellID)`
|
||||
|
||||
Returns `true` if the spell is an "on next swing" melee ability.
|
||||
These abilities queue and land on your next weapon swing instead of
|
||||
an instant cast (Heroic Strike, Cleave, Maul, Raptor Strike).
|
||||
|
||||
```lua
|
||||
C_Spell.IsNextMeleeSpell(78) -- Heroic Strike → true
|
||||
C_Spell.IsNextMeleeSpell(6807) -- Maul → true
|
||||
C_Spell.IsNextMeleeSpell(116) -- Frostbolt → false
|
||||
```
|
||||
|
||||
Tests the two on-next-swing bits in `Spell.dbc.Attributes`
|
||||
(`0x04` and `0x400`).
|
||||
|
||||
### `C_Spell.ResetsMeleeSwing(spellID)`
|
||||
|
||||
Returns `true` if casting the spell resets your melee swing timer.
|
||||
Cast-time spells reset it when you cast them (Fireball, Frostbolt,
|
||||
Polymorph, wand Shoot). Instant abilities like Sinister Strike do not,
|
||||
so a rogue can weave them between swings.
|
||||
|
||||
```lua
|
||||
C_Spell.ResetsMeleeSwing(133) -- Fireball → true
|
||||
C_Spell.ResetsMeleeSwing(1752) -- Sinister Strike → false
|
||||
C_Spell.ResetsMeleeSwing(1464) -- Slam → false (flagged "no reset")
|
||||
```
|
||||
|
||||
Mirrors the server's `IsMeleeAttackResetSpell`: `true` when the
|
||||
spell's `InterruptFlags` has the auto-attack bit (`0x08`) and its
|
||||
`AttributesEx2` does not have the "no reset auto actions" bit
|
||||
(`0x20000`). Both fields come from `Spell.dbc`. Slam carries that
|
||||
second bit, so it keeps its swing.
|
||||
|
||||
### `C_Item.GetWeaponEnchantInfo()`
|
||||
|
||||
Returns the modern 12-tuple matching WotLK+'s extended
|
||||
@@ -11126,131 +11640,6 @@ otherwise it falls back to `name`/`displayName`/`textureID`/`spellID` with
|
||||
**`nil` times**. The player path is unchanged (full timing). Same
|
||||
placeholder fields as `C_Spell.UnitCastingInfo`.
|
||||
|
||||
### `UNIT_SPELLCAST_*` events
|
||||
|
||||
Backport of the TBC+ cast/channel events to 1.12, for the **local player and
|
||||
other units**. Ported cast-bar / rotation addons (anything written against
|
||||
the modern signature) register these instead of vanilla's arg-less
|
||||
`SPELLCAST_*` events and read `unit, castGUID, spellID` directly. Thirteen
|
||||
events are provided; six also fire for non-player units:
|
||||
|
||||
| Event | Fires when | Units | Args |
|
||||
|-------|-----------|-------|------|
|
||||
| `UNIT_SPELLCAST_SENT` | `CMSG_CAST_SPELL` leaves the client (earliest point) | player | `unit, target, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_START` | a cast-time spell begins | all | `unit, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_STOP` | a cast-time spell ends (any reason) | all | same |
|
||||
| `UNIT_SPELLCAST_DELAYED` | pushback extends the cast | player | same |
|
||||
| `UNIT_SPELLCAST_SUCCEEDED` | the spell goes off (`SMSG_SPELL_GO`) — incl. instants | all | same |
|
||||
| `UNIT_SPELLCAST_INTERRUPTED` | a started **cast** is interrupted (kick, movement, LoS) — never for channels | all | same |
|
||||
| `UNIT_SPELLCAST_FAILED` | a cast is rejected before it starts (range, mana, cooldown) with an error shown | player | same |
|
||||
| `UNIT_SPELLCAST_FAILED_QUIET` | a cast fails with **no** error shown (spammy retry, reticle cancel, …) | player | same |
|
||||
| `UNIT_SPELLCAST_CHANNEL_START` | a channel begins | all | same |
|
||||
| `UNIT_SPELLCAST_CHANNEL_UPDATE` | pushback shortens a channel | player | same |
|
||||
| `UNIT_SPELLCAST_CHANNEL_STOP` | a channel ends | all | same |
|
||||
| `UNIT_SPELLCAST_RETICLE_TARGET` | a ground-target reticle appears (AoE placement — Blizzard, Flare, …) | player | `unit, "", spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_RETICLE_CLEAR` | the reticle is placed or cancelled | player | `unit, "", spellID, spellName, rank` |
|
||||
|
||||
`unit` (arg1) is the token of the casting unit — `"player"` for your own
|
||||
casts, or a unit token (`"target"`, `"focus"`, `"party3"`, `"nameplate2"`,
|
||||
`"pet"`, `"mouseover"`, …) for another unit. `spellName` / `rank` are
|
||||
ClassicAPI tail extensions (modern stops at `spellID`); addons reading only
|
||||
the first three positional args are unaffected.
|
||||
|
||||
**Per-token fan-out.** A caster GUID can map to several tokens at once (your
|
||||
`target` is also `party2` and `nameplate1`). Like retail, the event fires
|
||||
**once per token** currently pointing at the caster, so a `target`-frame
|
||||
cast bar, a `party2` frame, and a nameplate cast bar each get their own
|
||||
event with the same castGUID. Tokens are resolved fresh at fire time (they
|
||||
shift frame-to-frame). If you run **SuperWoW**, its raw-GUID token (`"0x…"`)
|
||||
is deliberately filtered out — only standard tokens are fanned out.
|
||||
|
||||
**Non-player limits.** Only the six events above fire for other units, and
|
||||
they're **best-effort** — driven purely by the packets an observer receives:
|
||||
- `SENT` never fires (only your own outgoing casts are visible).
|
||||
- `DELAYED` / `CHANNEL_UPDATE` never fire (pushback is sent only to the
|
||||
caster, so another unit's bar can't stretch/shrink from damage).
|
||||
- `FAILED` never fires (a pre-cast requirement failure is client-local).
|
||||
- A remote cast/channel only has timing from the moment its
|
||||
`SMSG_SPELL_START` was observed; casters who were already casting when
|
||||
they came into range have no start time.
|
||||
|
||||
**Channels never fire `INTERRUPTED`.** Retail emits only `CHANNEL_STOP` when a
|
||||
channel ends, whether it completed or was cut short (verified against retail),
|
||||
so ClassicAPI matches that for both the player and other units. `INTERRUPTED`
|
||||
is a cast-only event.
|
||||
|
||||
**Reticle events** fire for ground-targeted (AoE) spells only, always for the
|
||||
player: `RETICLE_TARGET` when the placement reticle comes up, `RETICLE_CLEAR`
|
||||
when it's placed or cancelled. There's no cast yet, so the castGUID slot
|
||||
(arg2) is empty — retail pushes `nil` there, but the engine's event
|
||||
dispatcher can't emit a `nil` mid-argument-list, so ClassicAPI pushes `""`
|
||||
instead. `unit` (arg1) and `spellID` (arg3) are exact; arg2 is the only
|
||||
difference and is inconsequential for a reticle.
|
||||
|
||||
**castGUID.** A synthesized string in the modern shape
|
||||
`Cast-<type>-<serverID>-<instanceID>-<zoneUID>-<spellID>-<castUID>`. Vanilla
|
||||
can't know server / instance / zone, so those three fields are `0`; the
|
||||
load-bearing parts are the `spellID` (field 6, which addons `strsplit("-")`
|
||||
out) and a unique-per-cast `castUID` (field 7). **Every event of one cast
|
||||
carries the same castGUID**, so `SENT` → `START`/`CHANNEL_START` →
|
||||
`SUCCEEDED` → `STOP`/`CHANNEL_STOP` all pair up — including across the caster
|
||||
and observers (they converge on the same value), and a chained same-spell
|
||||
recast gets its own castUID. The `type` and `castUID` follow the
|
||||
[spell-cast-GUID spec](https://warcraft.wiki.gg/wiki/GUID#Cast):
|
||||
- **Type 3** (real casts — the common case): `castUID` is time-based — the
|
||||
low 23 bits are the cast's UNIX-epoch second, the higher bits a per-second
|
||||
counter.
|
||||
- **Type 2** (`UNIT_SPELLCAST_FAILED` — a local-only cast that never reached
|
||||
the server): `castUID` is a plain locally-incrementing integer.
|
||||
|
||||
**Ordering** matches modern:
|
||||
|
||||
- Cast-time spell: `SENT → START → SUCCEEDED → STOP`.
|
||||
- Channel: `SENT → CHANNEL_START → SUCCEEDED → CHANNEL_STOP` (CHANNEL_START
|
||||
before SUCCEEDED, as on retail).
|
||||
- Instant: `SENT → SUCCEEDED`.
|
||||
|
||||
**INTERRUPTED vs FAILED vs FAILED_QUIET** follow modern's split: a spell that
|
||||
never started (out of range, not enough mana, on cooldown, LoS to a target)
|
||||
fires `FAILED`, except for a fixed whitelist of "quiet" `SpellCastResult`
|
||||
codes that fire `FAILED_QUIET` instead — `SPELL_IN_PROGRESS` (casting while
|
||||
already casting / a spell-queue rejection), `DONT_REPORT` (fake fails, a
|
||||
cancelled ground reticle), and `CHARMED`. That whitelist mirrors the 3.3.5
|
||||
client's own unit-spellcast dispatch, mapped to vanilla's `SpellCastResult`
|
||||
enum. A spell that was *already casting* and gets stopped (an enemy kick,
|
||||
moving to cancel, breaking LoS mid-cast) fires `INTERRUPTED`. Holding
|
||||
the cast key while running fires `INTERRUPTED` repeatedly (once per retry),
|
||||
each reusing the interrupted cast's castGUID — matching retail.
|
||||
|
||||
**Channel pushback (player).** Taking damage while channeling shortens the
|
||||
channel in vanilla; `CHANNEL_UPDATE` fires on each hit and
|
||||
[`C_Spell.UnitChannelInfo`](#c_spellunitchannelinfounit--c_spellchannelinfo)'s
|
||||
`endTimeMs` re-anchors to the server's new remaining time, so cast bars
|
||||
shrink correctly. (The event carries no time — like retail it's a "re-read
|
||||
now" trigger; timing is read back from `UnitChannelInfo`.)
|
||||
|
||||
Every fire is gated on whether any frame is registered for that event, so
|
||||
the whole system costs one pointer-compare per state transition when no
|
||||
addon uses it (no arg synthesis, no DBC lookups, no per-token fan-out).
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
for _, e in ipairs({
|
||||
"UNIT_SPELLCAST_START", "UNIT_SPELLCAST_STOP",
|
||||
"UNIT_SPELLCAST_SUCCEEDED", "UNIT_SPELLCAST_CHANNEL_START",
|
||||
}) do f:RegisterEvent(e) end
|
||||
f:SetScript("OnEvent", function()
|
||||
-- vanilla passes event/arg1/... as globals, not function params
|
||||
if arg1 == "target" then print(event, arg3) end -- arg3 = spellID
|
||||
end)
|
||||
```
|
||||
|
||||
> **Additive to the vanilla `SPELLCAST_*` events.** The engine's own arg-less
|
||||
> `SPELLCAST_START` / `SPELLCAST_CHANNEL_UPDATE` / … still fire as before;
|
||||
> these `UNIT_`-prefixed events are the modern layer on top. The empowered-cast
|
||||
> events (`UNIT_SPELLCAST_EMPOWER_*`, a Dragonflight addition) are not
|
||||
> implemented — vanilla has no empowered casts.
|
||||
|
||||
### `C_Spell.GetSpellLevelInfo(spellID)`
|
||||
|
||||
Returns the raw `Spell.dbc` level fields for a spell:
|
||||
@@ -11337,6 +11726,40 @@ end
|
||||
Equivalent to the legacy function of the same name introduced in 3.0
|
||||
(later renamed to `FindSpellBookSlotBySpellID` in 5.x).
|
||||
|
||||
### `C_SpellBook.GetSpellBookItemInfo(slotIndex, spellBank)`
|
||||
|
||||
Returns a table describing the spell in a spellbook slot. `slotIndex` is
|
||||
1-based across the whole book. `spellBank` picks the book —
|
||||
[`Enum.SpellBookSpellBank.Player`](#enumspellbookspellbank) (`0`) or
|
||||
`Enum.SpellBookSpellBank.Pet` (`1`).
|
||||
|
||||
```lua
|
||||
local info = C_SpellBook.GetSpellBookItemInfo(1, Enum.SpellBookSpellBank.Player)
|
||||
-- info.name, info.spellID, info.itemType, info.isPassive, info.iconID, ...
|
||||
```
|
||||
|
||||
Table fields:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `itemType` | [`Enum.SpellBookItemType`](#enumspellbookitemtype) | `Spell` (1) for the player book, `PetAction` (3) for the pet book. |
|
||||
| `actionID` | number | The spellID. Equals `spellID` — vanilla has no spell-override system. |
|
||||
| `spellID` | number | The spellID in the slot. |
|
||||
| `name` | string | The localized spell name. |
|
||||
| `subName` | string | The rank text (e.g. `"Rank 3"`), or `""` when the spell has no rank. |
|
||||
| `iconID` | string | The icon **path** — feed it straight to `texture:SetTexture(...)`. |
|
||||
| `isPassive` | boolean | `true` for a passive spell. |
|
||||
| `isOffSpec` | boolean | Always `false`. |
|
||||
|
||||
Returns `nil` for an empty or out-of-range slot, matching the modern API.
|
||||
|
||||
> **Deviations from retail, forced by 1.12's data.** `iconID` is a
|
||||
> texture path, not a `fileID` (vanilla has no fileID system) — the same
|
||||
> deviation as [`C_Spell.GetSpellInfo`](#c_spellgetspellinfospellid).
|
||||
> `skillLineIndex` is not returned (`nil`); spellbook tabs aren't
|
||||
> SkillLines. `isOffSpec` is always `false` — vanilla has no
|
||||
> specializations.
|
||||
|
||||
### `C_SpellBook.GetSpellLevelLearned(spellID)`
|
||||
|
||||
Returns the level at which a spell becomes available — the
|
||||
@@ -13645,6 +14068,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`
|
||||
@@ -13724,7 +14172,7 @@ is the escape hatch for *other* custom servers with the same class of
|
||||
gap — call it from your own config or addon; a registered override
|
||||
beats both the DBC and the built-in values.
|
||||
|
||||
### `C_UnitAuras.RegisterAuraDurationModifier(triggerSpellID, affectedFamily, affectedFamilyFlags, affectedIcon, op [, valueSeconds])`
|
||||
### `C_UnitAuras.RegisterAuraDurationModifierByTrigger(triggerFamily, triggerSchool, affectedFamily, affectedFamilyFlags, affectedIcon, op [, valueSeconds])`
|
||||
|
||||
ClassicAPI extension for **server-side DoT-duration changes on another
|
||||
unit that the client is never told about**. Returns `true` on success.
|
||||
@@ -13743,8 +14191,9 @@ trigger lands:
|
||||
|
||||
| Arg | Meaning |
|
||||
|-----|---------|
|
||||
| `triggerSpellID` | The spell whose cast triggers the change (match one rank per call — use the exact IDs the server binds). |
|
||||
| `affectedFamily` | `SpellFamilyName` of the affected aura (e.g. `5` warlock, `11` shaman, `7` druid). |
|
||||
| `triggerFamily` | `SpellFamilyName` of the triggering cast (e.g. `6` priest). |
|
||||
| `triggerSchool` | School index the trigger must be (`0` physical … `2` fire, `5` shadow, `6` arcane), or `< 0` for any. Matching by family + school covers a whole class of spells at once — every rank, plus server-added ones — rather than a named ability. |
|
||||
| `affectedFamily` | `SpellFamilyName` of the affected aura (e.g. `5` warlock, `11` shaman, `6` priest). |
|
||||
| `affectedFamilyFlags` | A `SpellFamilyFlags` bitmask; the affected aura matches if it overlaps. Family + flag is rank-proof (covers every rank at once). |
|
||||
| `affectedIcon` | `SpellIconID` the affected aura must have, or `0` to match any. |
|
||||
| `op` | `"refresh"` (reset to full duration), `"reduce"` (subtract `valueSeconds`, removing the aura if it would go non-positive), `"set"` (to `valueSeconds`), `"remove"`. |
|
||||
@@ -13754,34 +14203,26 @@ The rule only fires for the aura **cast by the same unit** as the trigger
|
||||
(these mechanics act on the caster's own DoT), and misses are excluded for
|
||||
free (a missed trigger isn't in the packet's hit list).
|
||||
|
||||
**`C_UnitAuras.RegisterAuraDurationModifierByTrigger(triggerFamily, triggerSchool, affectedFamily, affectedFamilyFlags, affectedIcon, op [, valueSeconds])`**
|
||||
is the same, but matches the *trigger* by `SpellFamilyName` + school index
|
||||
(`0` physical … `2` fire, `5` shadow, `6` arcane; `< 0` = any) instead of an
|
||||
exact spellID — one rule covers a whole class of spells (every rank, plus
|
||||
server-added ones). Use it when the trigger is a category rather than a named
|
||||
ability.
|
||||
Rules keyed to an **exact trigger spellID** (rather than a family + school
|
||||
category) are registered inside the DLL instead. `!!!ClassicAPI`'s built-in
|
||||
Turtle mods — Conflagrate shaving 3s off the caster's Immolate, Molten Blast
|
||||
refreshing the caster's Flame Shock — live in `src/turtle/DurationMods.cpp`,
|
||||
and Carnage's roll-gated Rip/Rake refresh in `src/turtle/Carnage.cpp`.
|
||||
|
||||
`!!!ClassicAPI` ships a default Turtle ruleset
|
||||
([Util/AuraDurationModifiers.lua](../AddOns/!!!ClassicAPI/Util/AuraDurationModifiers.lua)):
|
||||
Conflagrate shaves 3s off the caster's Immolate, Molten Blast refreshes the
|
||||
caster's Flame Shock, and — for a 5/5 Shadow Weaving priest (talent `15334`,
|
||||
where the proc is 100%) — any shadow-school priest cast refreshes the target's
|
||||
Shadow Vulnerability (`15258`). Deliberately excluded: probabilistic effects
|
||||
(Carnage's roll-gated Rip/Rake refresh, and Shadow Weaving below 5/5) — the
|
||||
client can't see the server's roll, so inferring them would show wrong timers.
|
||||
Two accepted best-effort caveats on the Shadow Weaving rule: DoT *ticks*
|
||||
(SW:P / Devouring Plague) also refresh it server-side but emit no cast packet,
|
||||
so a pure DoT-only phase under-counts slightly; and a full-consume Conflagrate
|
||||
variant on other servers *removes* Immolate, which ClassicAPI already evicts
|
||||
via the normal removal path (so the reduce rule is harmless there).
|
||||
The one rule registered from Lua
|
||||
([Util/AuraDurationModifiers.lua](../AddOns/!!!ClassicAPI/Util/AuraDurationModifiers.lua))
|
||||
is Shadow Weaving: for a 5/5 priest (talent `15334`, where the proc is 100%),
|
||||
any shadow-school priest cast refreshes the target's Shadow Vulnerability
|
||||
(`15258`). Below 5/5 it's roll-gated and deliberately not inferred — the client
|
||||
can't see the server's roll, so inferring it would show wrong timers. One
|
||||
accepted best-effort caveat: DoT *ticks* (SW:P / Devouring Plague) also refresh
|
||||
it server-side but emit no cast packet, so a pure DoT-only phase under-counts
|
||||
slightly.
|
||||
|
||||
```lua
|
||||
-- Flags are hex; Lua 5.0 has no 0x literals, so use tonumber(hex, 16).
|
||||
-- Conflagrate (Rank 4) shaves 3s off the caster's Immolate (warlock, flag 0x4):
|
||||
C_UnitAuras.RegisterAuraDurationModifier(18932, 5, tonumber("4", 16), 0, "reduce", 3)
|
||||
-- Molten Blast refreshes the caster's Flame Shock (shaman, flag 0x10000000):
|
||||
C_UnitAuras.RegisterAuraDurationModifier(36916, 11, tonumber("10000000", 16), 0, "refresh")
|
||||
-- Any priest (6) shadow-school (5) cast refreshes Shadow Vulnerability (15258):
|
||||
-- Any priest (6) shadow-school (5) cast refreshes Shadow Vulnerability
|
||||
-- (affected priest 6, flag 0x4000000, icon 9):
|
||||
C_UnitAuras.RegisterAuraDurationModifierByTrigger(6, 5, 6, tonumber("4000000", 16), 9, "refresh")
|
||||
```
|
||||
|
||||
|
||||
+181
-27
@@ -198,10 +198,23 @@ enum Offsets {
|
||||
// Returns an int the caller (Script_GameTooltip_SetUnit) tests, so the
|
||||
// co-hook must forward the original's return value.
|
||||
// - GameObject (0x0052aa20, __thiscall(self, guid*), RET 4) — the GO
|
||||
// hover populator; resolves the GO (OBJ_TYPE_GAMEOBJECT) and writes the
|
||||
// hover populator; resolves the GO (TYPEMASK_GAMEOBJECT) and writes the
|
||||
// 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
|
||||
@@ -1056,6 +1069,10 @@ enum Offsets {
|
||||
// derive `dispelName` from `Spell.dbc[+0x10]` (see
|
||||
// `OFF_SPELL_DISPEL_TYPE` below), not from the flags nibble.
|
||||
OFF_UNIT_FIELD_AURA = 0xA4, // u32 spell ID per slot, 48 slots total
|
||||
// UNIT_AURA event id for FUN_UNIT_EVENT_BROADCAST. The per-token unit
|
||||
// events use field-index == event-id; the aura field is index 0x29
|
||||
// (OFF_UNIT_FIELD_AURA 0xA4 >> 2), per Unit::TokenObserver's watched set.
|
||||
UNIT_EVENT_UNIT_AURA = 0x29,
|
||||
OFF_UNIT_FIELD_AURAFLAGS = 0x164, // 4 bits per aura, 2 per byte, covers all 48
|
||||
OFF_UNIT_FIELD_AURALEVELS = 0x17C, // u8 caster level per aura, 48 bytes
|
||||
OFF_UNIT_FIELD_AURAAPPLICATIONS = 0x1AC, // u8 (stacks-1) per aura, display value = byte+1
|
||||
@@ -1498,6 +1515,16 @@ enum Offsets {
|
||||
// cursor is over no mouse-enabled frame. `Frame::Attributes` polls this to
|
||||
// drive the `mouseover` override from the hovered frame's `unit` attribute.
|
||||
OFF_UI_CONTEXT_MOUSE_FOCUS = 0x7C,
|
||||
// Head of the engine's frame list (`*(*VAR_UI_CONTEXT_PTR + 0xCC4)`),
|
||||
// and each frame CObject's next-in-enumeration pointer at +0x308 — the
|
||||
// list `Script_EnumerateFrames` (0x00705F60) walks. A node with its low
|
||||
// bit set (or null) is the end sentinel. `Frame::MouseFoci` walks it to
|
||||
// build the GetMouseFoci stack.
|
||||
OFF_UI_CONTEXT_FRAME_LIST_HEAD = 0xCC4,
|
||||
OFF_FRAME_ENUM_NEXT = 0x308,
|
||||
// Frame level, a plain int at CObject +0xC4 (the value `Script_GetFrameLevel`
|
||||
// at 0x007744A0 pushes as `frame[0x31]`). Read directly for stack ordering.
|
||||
OFF_FRAME_LEVEL = 0xC4,
|
||||
FUN_SCRIPT_FRAME_SHOW = 0x00775750,
|
||||
FUN_SCRIPT_FRAME_HIDE = 0x00775810,
|
||||
FUN_SCRIPT_FRAME_SETMINRESIZE = 0x00776020,
|
||||
@@ -1508,6 +1535,14 @@ enum Offsets {
|
||||
// it so a unit-attributed frame registers as the mouse-focus (bare frames
|
||||
// otherwise never hover). Standard `int __fastcall(void *L)` Script_* shape.
|
||||
FUN_SCRIPT_FRAME_ENABLEMOUSE = 0x00777070,
|
||||
// Frame predicates used by `Frame::MouseFoci` to filter the enumeration to
|
||||
// the interactive stack (visible + mouse-enabled). Standard Script_* shape
|
||||
// `int __fastcall(void *L)`, self at Lua idx 1, result pushed at idx 2.
|
||||
FUN_SCRIPT_FRAME_IS_VISIBLE = 0x007758D0,
|
||||
FUN_SCRIPT_FRAME_IS_MOUSE_ENABLED = 0x00777130,
|
||||
// `frame:GetFrameStrata()` — pushes the strata name string; ranked for
|
||||
// stack ordering (`Frame::MouseFoci`).
|
||||
FUN_SCRIPT_FRAME_GET_STRATA = 0x007742A0,
|
||||
// Button OnClick dispatcher — `__thiscall(button, buttonCode)` at
|
||||
// 0x00779540, invoking the button's OnClick slot `[button+0x4CC]`. NOTE: do
|
||||
// NOT MinHook it — SuperWoW's click-casting inline-hooks the same prologue
|
||||
@@ -1864,7 +1899,7 @@ enum Offsets {
|
||||
// gated on the bank-open globals). Returns the CGContainer GUID of the bag
|
||||
// equipped in that slot, or 0 if none. This is the internal PackBagSlot
|
||||
// uses to resolve bags 1..4: get the bag GUID here, resolve the container
|
||||
// via FUN_OBJECT_RESOLVE_BY_GUID(OBJ_TYPE_CONTAINER, guid), then call the
|
||||
// via FUN_OBJECT_RESOLVE_BY_GUID(TYPEMASK_CONTAINER, guid), then call the
|
||||
// container's vtable[+OFF_CONTAINER_GET_INVENTORY] to get the inventory
|
||||
// object GetItemBySlot indexes. Lets us enumerate bag contents in pure C++
|
||||
// without PackBagSlot's Lua-stack coupling. (Reads GUID arrays at
|
||||
@@ -1920,16 +1955,14 @@ enum Offsets {
|
||||
//
|
||||
// Type values: 2 = item (returns CGItem*), 4 = container/bag
|
||||
// (returns CGContainer*). Engine passes `"ItemMgr"` as debugName
|
||||
// and `0x172` as priority for both call sites we've decoded.
|
||||
// and `0x172` as priority for both call sites we've decoded. Some engine
|
||||
// sites type it as `(u32 typeMask, void *unused, u64 guid, int)` —
|
||||
// ABI-equivalent (the u64 occupies the same two stack dwords as
|
||||
// guidLo/guidHi). All our callers go through `Object::ByGuid`
|
||||
// (object/Resolve.h), which wraps this single address.
|
||||
FUN_OBJECT_RESOLVE_BY_GUID = 0x00468460,
|
||||
// The type arg is a bitmask of object-type bits, not an enum
|
||||
// index — `1<<1` for items, `1<<2` for containers, `1<<3` for
|
||||
// units, matching what `FUN_00529FE0` passes for SetUnit (`ECX=8`).
|
||||
OBJ_TYPE_ITEM = 2,
|
||||
OBJ_TYPE_CONTAINER = 4,
|
||||
OBJ_TYPE_UNIT = 8,
|
||||
OBJ_TYPE_PLAYER = 0x10, // what the engine's bag observer FUN_004F8DB0 passes
|
||||
OBJ_TYPE_GAMEOBJECT = 0x20, // 1<<5; passed by FUN_0052AA20 (hover-tooltip populator).
|
||||
// The type arg is a bitmask of object-type bits (see the TYPEMASK_*
|
||||
// block below), not an enum index.
|
||||
|
||||
// `CGObject::GetName` — returns the display-name `const char *` for
|
||||
// a resolved CGObject (CGUnit / CGPlayer / CGCreature). Internally
|
||||
@@ -2641,6 +2674,10 @@ enum Offsets {
|
||||
VAR_AREATABLE_RECORDS = 0x00C0E048,
|
||||
VAR_AREATABLE_COUNT = 0x00C0E04C,
|
||||
OFF_AREATABLE_NAMES = 0x2C,
|
||||
// Parent AreaTable id @ +0x08 — a sub-area's enclosing zone (e.g. Goldshire
|
||||
// → Elwynn Forest). GetFriendInfo resolves it to name the zone, not the
|
||||
// sub-area. 0 for a top-level zone.
|
||||
OFF_AREATABLE_PARENT_ID = 0x08,
|
||||
// AreaBit @ +0x0C: index into the player's explored-areas bitfield
|
||||
// (OFF_PLAYER_EXPLORED_BITS). Gate @ +0x28: when < 0 the area needs no
|
||||
// exploration (always counts explored); when >= 0 the AreaBit check
|
||||
@@ -2811,15 +2848,11 @@ enum Offsets {
|
||||
// each callback invocation, guidLo/guidHi pushed as 8 stack bytes.
|
||||
FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS = 0x00468380,
|
||||
|
||||
// GUID → CGObject resolver. __fastcall with this signature:
|
||||
// CGObject *(__fastcall *)(uint32_t typeMask, void *unused,
|
||||
// uint64_t guid, int unused2);
|
||||
// typeMask filters to specific object types; returns NULL if the
|
||||
// GUID isn't loaded or its type doesn't match the mask.
|
||||
FUN_CLNT_OBJ_MGR_OBJECT_PTR = 0x00468460,
|
||||
|
||||
// Type-mask flags accepted by FUN_CLNT_OBJ_MGR_OBJECT_PTR. Single-bit
|
||||
// flags can be OR'd together.
|
||||
// Type-mask flags accepted by FUN_OBJECT_RESOLVE_BY_GUID (and the SetUnit
|
||||
// path FUN_00529FE0, which passes ECX=8 for UNIT). A bitmask of
|
||||
// object-type bits, not an enum index — single-bit flags OR together.
|
||||
// PLAYER (0x10) is what the bag observer FUN_004F8DB0 passes; GAMEOBJECT
|
||||
// (0x20) what the hover-tooltip populator FUN_0052AA20 passes.
|
||||
TYPEMASK_OBJECT = 0x01,
|
||||
TYPEMASK_ITEM = 0x02,
|
||||
TYPEMASK_CONTAINER = 0x04,
|
||||
@@ -3064,11 +3097,16 @@ enum Offsets {
|
||||
VAR_FACTION_COLLAPSED_BITMASK = 0x0084A0A4,
|
||||
MAX_FACTION_HEADERS = 32,
|
||||
|
||||
// Who-query (the /who system).
|
||||
// The social singleton (0x00C28168) — one object shared by the friend
|
||||
// list, ignore list, and /who system. Layout: friend entries inline from
|
||||
// offset 0 (stride 0x20, up to 50; each is {connected@+0x00, name char*
|
||||
// @+0x04, GUID u64 @+0x08, level@+0x10, area@+0x14, class@+0x18}), the
|
||||
// 25-entry ignore-GUID table at +0x650, and the who-query state. Read by
|
||||
// GetNumFriends/GetFriendInfo, GetNumIgnores, and Script_SendWho.
|
||||
//
|
||||
// `Script_SendWho` (0x005AD3B0) is a 32-byte wrapper:
|
||||
// - validate arg1 is a string via lua_isstring
|
||||
// - load WhoSystem singleton from `[VAR_WHO_SYSTEM]`
|
||||
// - load the singleton from `[VAR_SOCIAL_SYSTEM]`
|
||||
// - if non-NULL, lua_tostring(L, 1) for the query string
|
||||
// - tail-call `FUN_WHO_SYSTEM_SEND_QUERY` with
|
||||
// `__thiscall(this = WhoSystem, queryStr)`
|
||||
@@ -3089,9 +3127,13 @@ enum Offsets {
|
||||
// Server-side cooldown for CMSG_WHO is ~5 seconds — a faster
|
||||
// client gets silent-dropped, so any client-side gating just
|
||||
// matches that.
|
||||
VAR_WHO_SYSTEM = 0x00C28168,
|
||||
VAR_SOCIAL_SYSTEM = 0x00C28168,
|
||||
VAR_WHO_TO_UI_FLAG = 0x00C2A12C,
|
||||
FUN_WHO_SYSTEM_SEND_QUERY = 0x005AEBB0,
|
||||
// Friend count: `__fastcall(socialSingleton) -> uint` — walks the inline
|
||||
// entries and returns the number of populated friend slots. Backs
|
||||
// GetNumFriends and C_FriendList.IsFriend.
|
||||
FUN_FRIEND_LIST_COUNT = 0x005AE490,
|
||||
|
||||
// SMSG_WHO opcode handler — opcode 0x63 (99) per the registration
|
||||
// in `FUN_005adc50`: `FUN_005ab650(99, FUN_005adf60, 0)`. Reads
|
||||
@@ -3105,6 +3147,25 @@ enum Offsets {
|
||||
// user actually wanted both.
|
||||
FUN_SMSG_WHO_RESPONSE = 0x005ADF60,
|
||||
|
||||
// /who result list — the buffer `Script_GetWhoInfo` (0x005AD6E0) and
|
||||
// `Script_GetNumWhoResults` (0x005AD690) read. An array of fixed-size
|
||||
// entries at `VAR_WHO_RESULTS`, stride `WHO_RESULT_STRIDE`; the field
|
||||
// offsets below are all relative to an entry. `name`/`guildName` are
|
||||
// inline C strings; `race`/`class` index ChrRaces/ChrClasses.dbc and
|
||||
// `zone` indexes AreaTable.dbc (own name, not parent-resolved).
|
||||
// `VAR_WHO_RESULT_COUNT` is the displayed count (the first of the two
|
||||
// values GetNumWhoResults returns; the second, total matches, is at +4).
|
||||
// Verified by decompiling both accessors.
|
||||
VAR_WHO_RESULTS = 0x00C281B8,
|
||||
VAR_WHO_RESULT_COUNT = 0x00C2A120,
|
||||
WHO_RESULT_STRIDE = 0xA0,
|
||||
OFF_WHO_NAME = 0x00, // char[] inline
|
||||
OFF_WHO_GUILD = 0x30, // char[] inline
|
||||
OFF_WHO_LEVEL = 0x90, // i32
|
||||
OFF_WHO_RACE = 0x94, // i32 → ChrRaces.dbc
|
||||
OFF_WHO_CLASS = 0x98, // i32 → ChrClasses.dbc
|
||||
OFF_WHO_ZONE = 0x9C, // i32 → AreaTable.dbc
|
||||
|
||||
// Per-faction current standing — `__fastcall(ecx = factionID) → int`.
|
||||
// Returns `base + delta` where the two values are stored at
|
||||
// `+0x08` / `+0x0C` of the player's rep slot (slot stride 0x10,
|
||||
@@ -3407,6 +3468,54 @@ enum Offsets {
|
||||
// without re-parsing the body. See `Macro::Spell::Script_GetMacroSpell`.
|
||||
FUN_MACRO_SLOT_TO_ENTRY = 0x004F0E40,
|
||||
|
||||
// Macro create/edit workers — back `C_Macro.CreateMacro` /
|
||||
// `C_Macro.EditMacro` (see [[src/macro/Edit.cpp]]). Both store the
|
||||
// icon as a BARE basename at `entry + OFF_MACRO_ICON` (256 bytes,
|
||||
// no validation); `Script_GetMacroInfo` re-prefixes it as
|
||||
// `Interface\Icons\%s` on read, which is why any texture that lives
|
||||
// under `Interface\Icons\` — including index-less `INV_*` item icons
|
||||
// — round-trips even though vanilla's index-based UI can't pick it.
|
||||
//
|
||||
// Create — `__fastcall(char *name /*ECX*/, char *iconBasename /*EDX,
|
||||
// may be null*/, char *body /*may be null*/, uint local, int
|
||||
// perCharacter) -> uint macroID`. SStrCopys name→`+0x24` (0x40),
|
||||
// icon→`+0x64` (0x100) when non-null, body via `FUN_004f1550`
|
||||
// (`+0x164`), runs the primary-spell parser `FUN_004EFE00`, sets the
|
||||
// dirty flag `DAT_00bdcbf8=1`, and refreshes the UI. Returns 0 on
|
||||
// failure (empty name, or the 18-per-scope list is full). Pass
|
||||
// `local=0` to match the legacy `CreateMacro` default.
|
||||
FUN_MACRO_CREATE = 0x004F1010,
|
||||
|
||||
// Edit — `__fastcall(uint macroID, char *name, char *iconBasename,
|
||||
// char *body, uint flag)`. Hash-finds the entry by macroID (no-op if
|
||||
// absent); each of name/icon/body is written only when its pointer is
|
||||
// non-null (so nil = leave that field unchanged), then reparse + dirty
|
||||
// flag + UI refresh, exactly like the create worker. It ALWAYS writes
|
||||
// `entry + OFF_MACRO_LOCAL_FLAG = flag`, so read the current flag and
|
||||
// pass it back to leave it unchanged. Chosen over replicating the
|
||||
// field writes because it also fires the action-button refresh
|
||||
// (`FUN_004e5cc0`/`FUN_004e5c00`) that direct writes would miss.
|
||||
FUN_MACRO_EDIT = 0x004F1280,
|
||||
|
||||
// macroID → 0-based slot (linear scan of VAR_MACRO_SLOT_MAP);
|
||||
// `0xFFFFFFFF` on miss. `slot = FUN_MACRO_ID_TO_SLOT(macroID) + 1`
|
||||
// is how the legacy `CreateMacro` wrapper turns the worker's return
|
||||
// into the 1-based index it hands back to Lua.
|
||||
FUN_MACRO_ID_TO_SLOT = 0x004F0EC0,
|
||||
|
||||
// Macro name → 0-based slot; `0xFFFFFFFF` on miss (the resolver
|
||||
// behind `GetMacroIndexByName`). Feeding its miss value straight to
|
||||
// `FUN_MACRO_SLOT_TO_ENTRY` yields NULL (bounds-rejected), so the
|
||||
// by-name and by-index EditMacro paths share one validation.
|
||||
FUN_MACRO_NAME_TO_SLOT = 0x004F0FA0,
|
||||
|
||||
// MacroEntry field offsets. Name/body are handled by the workers; the
|
||||
// icon offset is documentation, and the local flag must be read to be
|
||||
// preserved across an EditMacro that doesn't mean to change it.
|
||||
OFF_MACRO_NAME = 0x24, // char[0x40] inline
|
||||
OFF_MACRO_ICON = 0x64, // char[0x100] inline bare basename
|
||||
OFF_MACRO_LOCAL_FLAG = 0x20, // uint32 `local` flag (echoed by GetMacroInfo)
|
||||
|
||||
// Macro-icon database. Populated lazily by `FUN_LOAD_MACRO_ICONS`
|
||||
// on the first `GetNumMacroIcons` call — enumerates `Interface\Icons\`
|
||||
// for `*.blp` files plus a wildcard match, sorts, and de-dupes. Each
|
||||
@@ -4401,6 +4510,14 @@ enum Offsets {
|
||||
// No `%b` for boolean — pass `%d` with 0/1; the engine has no native
|
||||
// bool concept here. No bounds check on eventID; pass valid indices.
|
||||
FUN_FIRE_EVENT = 0x00703F50,
|
||||
// Per-token unit-event broadcast. Fires unit event `eventCode` for every
|
||||
// unit token currently mapping to `*guid` (target / party / raid /
|
||||
// nameplateN / mouseover / focus), via the engine's own GUID->token reverse
|
||||
// map -- the same path the descriptor-field observers drive on a real field
|
||||
// change. `void __fastcall(uint64_t *guid, uint32_t eventCode)`; verified
|
||||
// against nampower's SendUnitSignal (0x00515e50). `eventCode` is the unit
|
||||
// field index, which doubles as the event id (see UNIT_EVENT_UNIT_AURA).
|
||||
FUN_UNIT_EVENT_BROADCAST = 0x00515E50,
|
||||
// Event name strings live in `.data` (mostly clustered around
|
||||
// 0x00851000..0x00855000); event-name string pointers also reach into
|
||||
// `.rdata` (starts at 0x007FF000) for some entries. Bound the dereference
|
||||
@@ -5649,6 +5766,7 @@ enum Offsets {
|
||||
// ENCHANT_ITEM / _TEMPORARY (enchants), and LEARN_SPELL (Turtle's custom
|
||||
// recipes, e.g. "Steel Plate Boots" = effect 36).
|
||||
OFF_SPELL_RECORD_EFFECT = 0xF4, // int32[3]
|
||||
SPELL_EFFECT_SCHOOL_DAMAGE = 2, // direct damage (verified vs server SpellDefines.h)
|
||||
SPELL_EFFECT_CREATE_ITEM = 24,
|
||||
SPELL_EFFECT_LEARN_SPELL = 36,
|
||||
SPELL_EFFECT_ENCHANT_ITEM = 53,
|
||||
@@ -5680,14 +5798,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
|
||||
@@ -5709,6 +5859,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,
|
||||
|
||||
+52
-10
@@ -60,6 +60,13 @@ namespace {
|
||||
constexpr const char *kAddonName = "!!!ClassicAPI";
|
||||
constexpr const char *kAddonTocFile = "!!!ClassicAPI.toc";
|
||||
|
||||
// Developer marker. When this file exists in the on-disk addon folder,
|
||||
// the disk copy wins unconditionally (see `DiskHasDevMarker` /
|
||||
// `DecideSource`). It is gitignored, excluded from the embed, and the
|
||||
// release ships only the DLL — so it can only exist where a developer
|
||||
// deliberately created it, never in a normal user's install.
|
||||
constexpr const char *kDevMarkerFile = ".classicapi-dev";
|
||||
|
||||
// `Interface\AddOns\!!!ClassicAPI\` — the prefix the engine
|
||||
// constructs for any of our addon's files. Comparing case-
|
||||
// insensitively because the TOC parser uses `Interface\AddOns\%s\…`
|
||||
@@ -156,17 +163,20 @@ bool ExtractTocVersion(const char *content, size_t size,
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns -1/0/+1 for `a < b` / `a == b` / `a > b`. "DEV" is a
|
||||
// sentinel meaning "newer than any real release" — local dev builds
|
||||
// always win against installed-on-disk versions. Otherwise the two
|
||||
// strings are walked as dot-separated numeric semver components
|
||||
// Returns -1/0/+1 for `a < b` / `a == b` / `a > b`. "DEV" is the
|
||||
// local-build sentinel: two DEV builds are equal, but a DEV build sorts
|
||||
// BELOW any real release. Dev-over-release precedence is signalled
|
||||
// explicitly by the `.classicapi-dev` marker now — NOT by the toc
|
||||
// version — so a stray `## Version: DEV` disk copy (repo clone / old
|
||||
// bundle) never silently shadows a newer embedded release. Otherwise
|
||||
// the two strings are walked as dot-separated numeric semver components
|
||||
// (`1.2` < `1.10`).
|
||||
int CompareVersions(const char *a, const char *b) {
|
||||
const bool aDev = std::strcmp(a, "DEV") == 0;
|
||||
const bool bDev = std::strcmp(b, "DEV") == 0;
|
||||
if (aDev && bDev) return 0;
|
||||
if (aDev) return 1;
|
||||
if (bDev) return -1;
|
||||
if (aDev) return -1;
|
||||
if (bDev) return 1;
|
||||
while (*a || *b) {
|
||||
int va = 0, vb = 0;
|
||||
while (*a >= '0' && *a <= '9') { va = va * 10 + (*a - '0'); ++a; }
|
||||
@@ -203,16 +213,48 @@ void EnsureEmbeddedVersionExtracted() {
|
||||
}
|
||||
}
|
||||
|
||||
// True iff the on-disk addon folder contains the `.classicapi-dev`
|
||||
// marker. Read via the ORIGINAL `FUN_FILE_READ` (bypassing our embed
|
||||
// hook), so it reflects a real file on disk / in an MPQ — never an
|
||||
// embedded copy. That makes it a clean "is this a developer working
|
||||
// tree" signal: the marker is gitignored + excluded from the embed and
|
||||
// the release ships only the DLL, so a normal user's install can't
|
||||
// carry it.
|
||||
bool DiskHasDevMarker() {
|
||||
char fullPath[256];
|
||||
std::snprintf(fullPath, sizeof(fullPath), "%s%s",
|
||||
kAddonPathPrefix, kDevMarkerFile);
|
||||
void *buf = nullptr;
|
||||
size_t size = 0;
|
||||
const int ok = FileRead_o(0, fullPath, &buf, &size, 1, 1, 0);
|
||||
if (ok == 0 || buf == nullptr)
|
||||
return false;
|
||||
auto SMemFree = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
SMemFree(buf, __FILE__, __LINE__, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decide whether disk or embedded should serve all reads for this
|
||||
// addon. Called on the first FileRead_h call with a matching prefix.
|
||||
// Reads the on-disk TOC via the original `FUN_FILE_READ` (bypassing
|
||||
// our hook), parses its version, compares to the embedded version,
|
||||
// and caches the winner. If disk has no TOC at all, embedded wins by
|
||||
// default.
|
||||
// The `.classicapi-dev` marker forces disk unconditionally (explicit
|
||||
// developer override). Otherwise reads the on-disk TOC via the original
|
||||
// `FUN_FILE_READ` (bypassing our hook), parses its version, compares to
|
||||
// the embedded version, and caches the newer. If disk has no TOC at
|
||||
// all, embedded wins by default.
|
||||
void DecideSource() {
|
||||
if (g_source != Source::Undecided) return;
|
||||
EnsureEmbeddedVersionExtracted();
|
||||
|
||||
// Explicit developer override: disk wins regardless of version. This
|
||||
// is how a dev keeps disk precedence when running local Lua against a
|
||||
// *released* DLL (where the embedded version would otherwise be newer
|
||||
// and win). Dev intent lives in this marker, decoupled from the toc
|
||||
// version — so users never get silently shadowed by a stray copy.
|
||||
if (DiskHasDevMarker()) {
|
||||
g_source = Source::Disk;
|
||||
return;
|
||||
}
|
||||
|
||||
char fullPath[256];
|
||||
std::snprintf(fullPath, sizeof(fullPath), "%s%s",
|
||||
kAddonPathPrefix, kAddonTocFile);
|
||||
|
||||
@@ -55,8 +55,10 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/Swap.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
@@ -97,27 +99,15 @@ Job g_job{};
|
||||
|
||||
// --- small CGItem / cache readers ---------------------------------------
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
int CGItemCount(const uint8_t *item) {
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
auto *desc = Item::ObjectFields(item);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const int *>(desc + Offsets::OFF_DESCRIPTOR_STACK_COUNT);
|
||||
}
|
||||
|
||||
uint64_t CGItemGuid(const uint8_t *item) {
|
||||
auto *inst = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
auto *inst = Item::InstanceBlock(item);
|
||||
if (inst == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(inst + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
@@ -188,7 +178,7 @@ bool BagIsGeneral(int bag) {
|
||||
const int id = Item::ID::FromCGItem(bagItem);
|
||||
if (id == 0)
|
||||
return false;
|
||||
const uint8_t *rec = PeekItemRecord(static_cast<uint32_t>(id));
|
||||
const uint8_t *rec = Item::PeekRecord(static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
return *reinterpret_cast<const uint32_t *>(
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
+36
-12
@@ -57,28 +57,52 @@ uint8_t CurrentComboPoints() {
|
||||
return *(info + Offsets::OFF_CGPLAYER_COMBO_POINTS);
|
||||
}
|
||||
|
||||
// One cast in flight at a time is the practical case (a second finisher
|
||||
// can't be cast until the server consumes the first one's combo points),
|
||||
// so a single slot with a spellID match and a short TTL suffices.
|
||||
// Keyed by spellID, because a finisher's own SMSG_SPELL_GO is not reliably
|
||||
// the next one to arrive after its cast. A talent that procs an aura on the
|
||||
// finisher (Turtle's rogue Taste for Blood casts spell 52529 on the player)
|
||||
// lands its SPELL_GO first, and every player-cast aura spell reaches
|
||||
// TryComboScaledMs. A single slot -- or a lookup that cleared the slot on a
|
||||
// spellID mismatch -- therefore lost the snapshot to the proc, and the
|
||||
// finisher fell back to its 0-CP base duration. Only the matching entry is
|
||||
// ever consumed; a miss leaves every other capture intact.
|
||||
struct Capture {
|
||||
uint32_t spellId;
|
||||
uint8_t comboPoints;
|
||||
uint32_t capturedMs;
|
||||
};
|
||||
Capture g_capture{0, 0, 0};
|
||||
constexpr int kCaptureCount = 8;
|
||||
Capture g_captures[kCaptureCount];
|
||||
int g_captureCursor = 0;
|
||||
|
||||
// Worst-case send -> SMSG_SPELL_GO roundtrip. Past this the snapshot is
|
||||
// stale (the cast failed / was rejected) and must not scale a later cast.
|
||||
constexpr uint32_t kCaptureTtlMs = 2000;
|
||||
|
||||
void RememberCapture(uint32_t spellId, uint8_t comboPoints, uint32_t nowMs) {
|
||||
for (auto &c : g_captures) {
|
||||
if (c.spellId == spellId) { // re-cast before the first resolved
|
||||
c.comboPoints = comboPoints;
|
||||
c.capturedMs = nowMs;
|
||||
return;
|
||||
}
|
||||
}
|
||||
g_captures[g_captureCursor] = {spellId, comboPoints, nowMs};
|
||||
g_captureCursor = (g_captureCursor + 1) % kCaptureCount;
|
||||
}
|
||||
|
||||
int ConsumeCapturedCP(uint32_t spellId) {
|
||||
const Capture c = g_capture;
|
||||
g_capture = {0, 0, 0};
|
||||
if (c.spellId != spellId || c.comboPoints == 0)
|
||||
return 0;
|
||||
if (NowMs() - c.capturedMs >= kCaptureTtlMs)
|
||||
return 0;
|
||||
return c.comboPoints;
|
||||
for (auto &c : g_captures) {
|
||||
if (c.spellId != spellId)
|
||||
continue;
|
||||
const Capture got = c;
|
||||
c = {0, 0, 0};
|
||||
if (got.comboPoints == 0)
|
||||
return 0;
|
||||
if (NowMs() - got.capturedMs >= kCaptureTtlMs)
|
||||
return 0;
|
||||
return got.comboPoints;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Co-hook on the NetClient send — the one funnel every outgoing CMSG
|
||||
@@ -94,7 +118,7 @@ void OnSend(uint32_t opcode, Net::CDataStore *packet) {
|
||||
return;
|
||||
const uint32_t spellId = Net::Read<uint32_t>(packet);
|
||||
if (spellId != 0)
|
||||
g_capture = {spellId, CurrentComboPoints(), NowMs()};
|
||||
RememberCapture(spellId, CurrentComboPoints(), NowMs());
|
||||
}
|
||||
|
||||
const Net::SendObserver::AutoSubscribe _sendSub{&OnSend};
|
||||
|
||||
+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
|
||||
|
||||
+214
-70
@@ -21,6 +21,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "net/PacketReader.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "player/StatSignal.h"
|
||||
#include "spell/CastEvents.h"
|
||||
#include "spell/Lookup.h"
|
||||
@@ -130,13 +131,64 @@ struct Entry {
|
||||
uint32_t spellId;
|
||||
uint32_t expirationMs; // 0 = infinite / unknown duration
|
||||
uint32_t durationMs; // applied duration (incl. caster mods); 0 = none
|
||||
uint32_t stampMs; // last write time; EvictAbsent grace (see below)
|
||||
int8_t kind; // Kind; descriptor-slot-derived, KIND_UNKNOWN if only seen via SpellGo
|
||||
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
|
||||
// an aura the descriptor doesn't list yet. `EvictAbsent` (run from a query's
|
||||
// ReconcileCache) must not drop such a fresh entry — doing so discards the
|
||||
// caster we just captured, which OnAuraAdded then recreates caster-less
|
||||
// (verified: druid buff → sourceGUID lost). Entries younger than this window
|
||||
// are exempt from EvictAbsent; a genuinely-removed aura is far older and still
|
||||
// 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;
|
||||
constexpr int kUnitOrPlayerMask =
|
||||
(1 << Offsets::OBJECT_TYPE_UNIT) | (1 << Offsets::OBJECT_TYPE_PLAYER);
|
||||
const auto *unit = static_cast<const uint8_t *>(
|
||||
Object::ByGuid(kUnitOrPlayerMask, guid, nullptr, 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,
|
||||
@@ -149,9 +201,12 @@ void Store(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
if (targetGuid == 0 || spellId == 0)
|
||||
return;
|
||||
|
||||
const uint32_t now = NowMs();
|
||||
|
||||
// Update an existing entry for this exact aura instance.
|
||||
for (auto &e : g_cache) {
|
||||
if (e.used && e.targetGuid == targetGuid && e.spellId == spellId) {
|
||||
e.stampMs = now; // refresh EvictAbsent grace on any touch
|
||||
// Learn the classification whenever a slot-derived kind arrives —
|
||||
// independent of caster/timing ownership, and never downgrade a
|
||||
// known kind back to unknown.
|
||||
@@ -166,19 +221,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.
|
||||
const uint32_t now = NowMs();
|
||||
// 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,
|
||||
kind, true};
|
||||
now, kind, true};
|
||||
return;
|
||||
}
|
||||
}
|
||||
Entry &slot = g_cache[g_writeCursor];
|
||||
g_writeCursor = (g_writeCursor + 1) % kCacheSize;
|
||||
slot = {targetGuid, casterGuid, spellId, expirationMs, durationMs, 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 ---------------------------
|
||||
@@ -245,15 +325,18 @@ void Evict(uint64_t targetGuid, uint32_t spellId) {
|
||||
// notifies the aura's target-if-a-player (self-scoped), never the observing
|
||||
// caster; on a mob the packet isn't even built. We can't hear the *change*,
|
||||
// but we DO see the TRIGGERING cast's SMSG_SPELL_GO, so we mirror the
|
||||
// server's edit on the cached entry. Rules come from Lua
|
||||
// (`C_UnitAuras.RegisterAuraDurationModifier`), so the server-specific set
|
||||
// lives in the addon, not the DLL. Turtle examples:
|
||||
// server's edit on the cached entry. Rules are registered two ways: the
|
||||
// family/school-matched form from Lua (`RegisterAuraDurationModifierByTrigger`,
|
||||
// e.g. Shadow Weaving), and the exact-spellID form from C++ (`AddDurationMod`),
|
||||
// which src/turtle uses for the server's built-in mods. Turtle examples
|
||||
// (registered in src/turtle/DurationMods.cpp):
|
||||
// Conflagrate -> Immolate: reduce 3s (Immolate keeps ticking, -3s)
|
||||
// Molten Blast -> Flame Shock: refresh (RefreshHolder → reset to max)
|
||||
// A trigger is matched by exact spellID (from the server's script binding,
|
||||
// stable across ranks); the affected aura by SpellFamilyName + a family-flag
|
||||
// overlap (+ optional icon) — rank-proof, exactly how the server's scripts
|
||||
// find it. Conflagrate's *full*-consume path removes Immolate, which clears
|
||||
// A trigger is matched either by exact spellID (from the server's script
|
||||
// binding, stable across ranks) or by SpellFamilyName + school; the affected
|
||||
// aura by SpellFamilyName + a family-flag overlap (+ optional icon) — rank-proof,
|
||||
// exactly how the server's scripts find it. Conflagrate's *full*-consume path
|
||||
// removes Immolate, which clears
|
||||
// the descriptor slot → OnAuraRemoved already handles it; the reduce rule
|
||||
// covers the keep-ticking case. Probabilistic refreshes (Carnage's roll) are
|
||||
// deliberately NOT shipped as defaults — the client can't know the server's
|
||||
@@ -275,23 +358,24 @@ constexpr int kMaxMods = 128;
|
||||
DurationMod g_mods[kMaxMods];
|
||||
int g_modCount = 0;
|
||||
|
||||
bool AffectedMatches(const uint8_t *rec, const DurationMod &m) {
|
||||
if (rec == nullptr)
|
||||
// The affected-aura selector alone, shared with the Lua-facing by-family
|
||||
// refresh so the two cannot disagree about what they match.
|
||||
bool AffectedMatchesRaw(const uint8_t *rec, uint32_t family, uint64_t mask,
|
||||
uint32_t icon) {
|
||||
if (!Spell::Lookup::IsFitToFamily(rec, family, mask))
|
||||
return false;
|
||||
if (*reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_FAMILY_NAME) != m.affectedFamily)
|
||||
return false;
|
||||
const uint64_t flags = *reinterpret_cast<const uint64_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_FAMILY_FLAGS);
|
||||
if ((flags & m.affectedMask) == 0)
|
||||
return false;
|
||||
if (m.affectedIcon != 0 &&
|
||||
if (icon != 0 &&
|
||||
*reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_ICON_ID) != m.affectedIcon)
|
||||
rec + Offsets::OFF_SPELL_RECORD_ICON_ID) != icon)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AffectedMatches(const uint8_t *rec, const DurationMod &m) {
|
||||
return AffectedMatchesRaw(rec, m.affectedFamily, m.affectedMask,
|
||||
m.affectedIcon);
|
||||
}
|
||||
|
||||
// A rule's trigger matches either by exact spellID, or (triggerSpellId == 0)
|
||||
// by the cast spell's SpellFamilyName + school index — one rule then covers
|
||||
// every rank / server-added spell of a class's school (e.g. any priest
|
||||
@@ -312,26 +396,62 @@ bool TriggerMatches(const DurationMod &m, uint32_t triggerSpellId,
|
||||
return true;
|
||||
}
|
||||
|
||||
// A cache duration edit has no engine packet behind it, so nothing re-fires
|
||||
// UNIT_AURA and event-driven consumers (aura-bar addons that read the duration
|
||||
// once and count down locally) never see the change. Re-broadcast UNIT_AURA for
|
||||
// the affected unit so they re-read. Deferred to the world tick, not fired
|
||||
// inline: the callers run inside the SMSG_SPELL_GO subscriber, and firing a
|
||||
// unit event there would run addon Lua mid-packet (the re-entrancy class this
|
||||
// codebase keeps getting bitten by). The tick flush is the context the engine's
|
||||
// own UNIT_AURA fires are safe in. Repeats within a frame are coalesced.
|
||||
constexpr int kPendingSignalMax = 32;
|
||||
uint64_t g_pendingSignals[kPendingSignalMax];
|
||||
int g_pendingSignalCount = 0;
|
||||
|
||||
void SignalAuraChanged(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return;
|
||||
for (int i = 0; i < g_pendingSignalCount; ++i)
|
||||
if (g_pendingSignals[i] == guid)
|
||||
return;
|
||||
if (g_pendingSignalCount < kPendingSignalMax)
|
||||
g_pendingSignals[g_pendingSignalCount++] = guid;
|
||||
}
|
||||
|
||||
void FlushAuraSignals() {
|
||||
using Broadcast_t = void(__fastcall *)(uint64_t *guid, uint32_t eventCode);
|
||||
auto broadcast = reinterpret_cast<Broadcast_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_UNIT_EVENT_BROADCAST));
|
||||
for (int i = 0; i < g_pendingSignalCount; ++i)
|
||||
broadcast(&g_pendingSignals[i], Offsets::UNIT_EVENT_UNIT_AURA);
|
||||
g_pendingSignalCount = 0;
|
||||
}
|
||||
|
||||
void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
switch (m.op) {
|
||||
case MOD_REFRESH:
|
||||
if (e.durationMs > 0)
|
||||
if (e.durationMs > 0) {
|
||||
e.expirationMs = now + e.durationMs; // RefreshHolder → reset to max
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
}
|
||||
break;
|
||||
case MOD_SET:
|
||||
e.durationMs = static_cast<uint32_t>(m.valueMs);
|
||||
e.expirationMs = now + static_cast<uint32_t>(m.valueMs);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
break;
|
||||
case MOD_REDUCE:
|
||||
if (e.expirationMs != 0) {
|
||||
if (e.expirationMs > now + static_cast<uint32_t>(m.valueMs))
|
||||
if (e.expirationMs > now + static_cast<uint32_t>(m.valueMs)) {
|
||||
e.expirationMs -= static_cast<uint32_t>(m.valueMs);
|
||||
else
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
} else {
|
||||
e.used = false; // shaved to/past now → server removes it
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MOD_REMOVE:
|
||||
e.used = false;
|
||||
e.used = false; // removal — OnAuraRemoved / descriptor path fires UNIT_AURA
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -414,40 +534,6 @@ int OpFromString(const char *s) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// `C_UnitAuras.RegisterAuraDurationModifier(triggerSpellID, affectedFamily,
|
||||
// affectedFamilyFlags, affectedIcon, op [, valueSeconds])` -> bool.
|
||||
// op: "refresh" | "reduce" | "set" | "remove". valueSeconds used by
|
||||
// reduce/set. affectedIcon 0 = match any icon. Re-registering an identical
|
||||
// (trigger, family, flags, icon) tuple replaces its op/value.
|
||||
int __fastcall Script_RegisterAuraDurationModifier(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2) ||
|
||||
!Game::Lua::IsNumber(L, 3) || !Game::Lua::IsNumber(L, 4) ||
|
||||
!Game::Lua::IsString(L, 5)) {
|
||||
Game::Lua::Error(
|
||||
L, "Usage: C_UnitAuras.RegisterAuraDurationModifier(triggerSpellID, "
|
||||
"affectedFamily, affectedFamilyFlags, affectedIcon, op[, "
|
||||
"valueSeconds])");
|
||||
return 0;
|
||||
}
|
||||
const auto trigger = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
|
||||
const auto family = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
const auto mask = static_cast<uint64_t>(Game::Lua::ToNumber(L, 3));
|
||||
const auto icon = static_cast<uint32_t>(Game::Lua::ToNumber(L, 4));
|
||||
const int op = OpFromString(Game::Lua::ToString(L, 5));
|
||||
const int32_t valueMs =
|
||||
Game::Lua::IsNumber(L, 6)
|
||||
? static_cast<int32_t>(Game::Lua::ToNumber(L, 6) * 1000.0)
|
||||
: 0;
|
||||
if (op < 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBool(L, RegisterDurationMod(trigger, /*triggerFamily*/ 0,
|
||||
/*triggerSchool*/ -1, family, mask,
|
||||
icon, op, valueMs));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_UnitAuras.RegisterAuraDurationModifierByTrigger(triggerFamily,
|
||||
// triggerSchool, affectedFamily, affectedFamilyFlags, affectedIcon, op
|
||||
// [, valueSeconds])` -> bool. Like the above, but the trigger is matched
|
||||
@@ -485,8 +571,6 @@ int __fastcall Script_RegisterAuraDurationModifierByTrigger(void *L) {
|
||||
}
|
||||
|
||||
void RegisterDurationModLua() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "RegisterAuraDurationModifier",
|
||||
&Script_RegisterAuraDurationModifier);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras",
|
||||
"RegisterAuraDurationModifierByTrigger",
|
||||
&Script_RegisterAuraDurationModifierByTrigger);
|
||||
@@ -534,8 +618,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.
|
||||
@@ -543,6 +637,9 @@ void OnWorldTick() {
|
||||
if (s.used && now - s.touchMs > kGroupSnapshotTtlMs)
|
||||
s.used = false;
|
||||
}
|
||||
// Re-fire UNIT_AURA for units whose cached duration a mod/refresh changed
|
||||
// this frame (see SignalAuraChanged).
|
||||
FlushAuraSignals();
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
@@ -772,12 +869,59 @@ bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AddDurationMod(uint32_t triggerSpellId, uint32_t affectedFamily,
|
||||
uint64_t affectedMask, uint32_t affectedIcon, int op,
|
||||
int32_t valueMs) {
|
||||
// Exact-spellID trigger (triggerFamily 0, triggerSchool -1).
|
||||
return RegisterDurationMod(triggerSpellId, /*triggerFamily*/ 0,
|
||||
/*triggerSchool*/ -1, affectedFamily, affectedMask,
|
||||
affectedIcon, op, valueMs);
|
||||
}
|
||||
|
||||
uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
uint64_t mask, uint32_t icon,
|
||||
uint64_t casterGuid) {
|
||||
if (unitGuid == 0 || mask == 0 || casterGuid == 0)
|
||||
return 0;
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used || e.targetGuid != unitGuid)
|
||||
continue;
|
||||
// Caller's own aura only, scoped as ApplyDurationModifiers scopes a
|
||||
// rule; a caster-less entry is claimed rather than skipped, same as
|
||||
// there.
|
||||
if (e.casterGuid != 0 && e.casterGuid != casterGuid)
|
||||
continue;
|
||||
if (!AffectedMatchesRaw(
|
||||
Spell::Lookup::RecordForID(static_cast<int>(e.spellId)), family,
|
||||
mask, icon))
|
||||
continue;
|
||||
if (e.durationMs == 0)
|
||||
continue; // never invent a duration
|
||||
if (e.casterGuid == 0)
|
||||
e.casterGuid = casterGuid;
|
||||
// The applied duration, not a recomputed one (the server's
|
||||
// RefreshHolder): a Rip cast at 3 combo points returns to its own
|
||||
// duration, not a five-point one.
|
||||
e.expirationMs = now + e.durationMs;
|
||||
e.stampMs = now;
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
return e.spellId;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void EvictAbsent(uint64_t unitGuid, const uint32_t *presentSpellIds, int count) {
|
||||
if (unitGuid == 0 || presentSpellIds == nullptr || count <= 0)
|
||||
return;
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used || e.targetGuid != unitGuid)
|
||||
continue;
|
||||
// Fresh SpellGo capture the descriptor hasn't synced yet — see
|
||||
// kEvictGraceMs. Don't evict it; that's the sourceGUID-loss race.
|
||||
if (now - e.stampMs < kEvictGraceMs)
|
||||
continue;
|
||||
bool present = false;
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (presentSpellIds[i] == e.spellId) {
|
||||
|
||||
@@ -42,6 +42,34 @@ namespace Aura::Source {
|
||||
bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
|
||||
uint32_t *outExpirationMs, uint32_t *outDurationMs);
|
||||
|
||||
// Refreshes `casterGuid`'s aura on `unitGuid` matching the same selector the
|
||||
// duration rules use — SpellFamilyName + family-flag overlap + optional icon
|
||||
// (0 = any) — to a full duration from now. Returns the spellID refreshed, or 0
|
||||
// if nothing matched. For a mechanic whose duration edit reaches the client
|
||||
// too late to be attributed to a cast packet, so no rule can express it.
|
||||
uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
uint64_t mask, uint32_t icon,
|
||||
uint64_t casterGuid);
|
||||
|
||||
// Op codes for AddDurationMod (mirror the Lua op strings).
|
||||
enum DurationModOp {
|
||||
DURATION_MOD_REFRESH = 0,
|
||||
DURATION_MOD_REDUCE = 1,
|
||||
DURATION_MOD_SET = 2,
|
||||
DURATION_MOD_REMOVE = 3,
|
||||
};
|
||||
|
||||
// Register a server duration-modifier rule from C++. The trigger is matched by
|
||||
// exact `triggerSpellId`; the affected aura by SpellFamilyName + a family-flag
|
||||
// overlap (+ optional `affectedIcon`, 0 = any). `valueMs` is the reduce/set
|
||||
// amount in milliseconds (ignored by refresh/remove). Used by src/turtle
|
||||
// modules for the server's built-in mods (the family/school-matched variant is
|
||||
// still Lua-registerable via C_UnitAuras.RegisterAuraDurationModifierByTrigger).
|
||||
// Returns false on bad input or a full table.
|
||||
bool AddDurationMod(uint32_t triggerSpellId, uint32_t affectedFamily,
|
||||
uint64_t affectedMask, uint32_t affectedIcon, int op,
|
||||
int32_t valueMs);
|
||||
|
||||
// One cached aura, as returned by `Enumerate`.
|
||||
struct CachedAura {
|
||||
uint32_t spellId;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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
|
||||
|
||||
// Tiny ASCII string helpers. Header-only — no locale, no allocation, safe from
|
||||
// any context (hook callbacks, DBC walks). Use these instead of hand-rolling
|
||||
// yet another case-insensitive compare.
|
||||
|
||||
namespace Ascii {
|
||||
|
||||
// Fold a single ASCII char to lower case (non-letters unchanged).
|
||||
inline char ToLower(char c) {
|
||||
return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
|
||||
}
|
||||
|
||||
// Case-insensitive full-string equality over ASCII. Two null pointers compare
|
||||
// equal; a null vs non-null does not.
|
||||
inline bool EqualCI(const char *a, const char *b) {
|
||||
if (a == nullptr || b == nullptr)
|
||||
return a == b;
|
||||
for (; *a && *b; ++a, ++b)
|
||||
if (ToLower(*a) != ToLower(*b))
|
||||
return false;
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
} // namespace Ascii
|
||||
+8
-30
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Names.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -22,21 +23,11 @@ namespace Classes::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
const uint8_t *const *Records() {
|
||||
return *reinterpret_cast<const uint8_t *const *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CHRCLASSES_RECORDS));
|
||||
}
|
||||
|
||||
int Count() {
|
||||
return *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CHRCLASSES_COUNT));
|
||||
}
|
||||
|
||||
int LocaleIndex() {
|
||||
return *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
}
|
||||
|
||||
// `FillLocalizedClassList(table [, isFemale]) → table` — fills the
|
||||
// passed-in table with `[classToken] = localizedName` pairs for every
|
||||
// class in `ChrClasses.dbc`, then returns the same table for chaining.
|
||||
@@ -58,15 +49,7 @@ int __fastcall Script_FillLocalizedClassList(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto *records = Records();
|
||||
if (records == nullptr) {
|
||||
// DBC not loaded yet — return the table unchanged. Matches
|
||||
// modern behavior of "fill what's available, don't crash".
|
||||
Game::Lua::SetTop(L, 1);
|
||||
return 1;
|
||||
}
|
||||
const int count = Count();
|
||||
const int locale = LocaleIndex();
|
||||
|
||||
// Drop the optional `isFemale` arg (and any further junk) so the
|
||||
// table sits alone at idx 1. Each loop iteration pushes key+value
|
||||
@@ -74,19 +57,14 @@ int __fastcall Script_FillLocalizedClassList(void *L) {
|
||||
// [table] before the next iteration.
|
||||
Game::Lua::SetTop(L, 1);
|
||||
for (int i = 1; i <= count; i++) {
|
||||
const uint8_t *record = records[i];
|
||||
if (record == nullptr)
|
||||
// DBC::* are null-safe (empty result if ChrClasses.dbc isn't loaded)
|
||||
// and skip empty strings, so a sparse/absent class id just falls
|
||||
// through — same "fill what's available, don't crash" behavior.
|
||||
const char *token = DBC::ClassToken(static_cast<uint32_t>(i));
|
||||
if (token == nullptr)
|
||||
continue;
|
||||
|
||||
const char *token = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_CHRCLASSES_FILENAME);
|
||||
if (token == nullptr || *token == '\0')
|
||||
continue;
|
||||
|
||||
const char *const *names = reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_CHRCLASSES_NAMES);
|
||||
const char *name = names[locale];
|
||||
if (name == nullptr || *name == '\0')
|
||||
const char *name = DBC::ClassName(static_cast<uint32_t>(i));
|
||||
if (name == nullptr)
|
||||
continue;
|
||||
|
||||
Game::Lua::PushString(L, token);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -23,21 +24,6 @@ namespace Container::FreeSlots {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
// Same `Peek the item cache without firing a query` shape as
|
||||
// `Item::Info::FetchItemRecord`. We don't share that helper because it's
|
||||
// file-static there; copying the four-line lookup is simpler than
|
||||
// promoting it to a header for one extra caller.
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
// Resolves the bag's metadata: total slot count + BagFamily bitfield.
|
||||
//
|
||||
// - bagID 0 (backpack) → (16, 0). Vanilla 1.12 backpack is fixed at 16
|
||||
@@ -68,7 +54,7 @@ BagInfo ResolveBagInfo(int bagID) {
|
||||
if (itemID == 0)
|
||||
return {0, 0};
|
||||
|
||||
auto *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
auto *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return {0, 0};
|
||||
|
||||
|
||||
@@ -50,9 +50,11 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/StatAccum.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -80,8 +82,7 @@ bool ItemReadable(const uint8_t *item, const uint8_t *record) {
|
||||
if (record != nullptr &&
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_ITEMSTATS_PAGE_TEXT) != 0)
|
||||
return true;
|
||||
const uint8_t *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
const uint8_t *descriptor = Item::ObjectFields(item);
|
||||
return descriptor != nullptr &&
|
||||
*reinterpret_cast<const uint32_t *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_READABLE_TEXT_ID) != 0;
|
||||
@@ -107,12 +108,11 @@ int __fastcall Script_C_Container_GetContainerItemInfo(void *L) {
|
||||
// them off the half-built table avoids any stack interleaving. ---
|
||||
const int itemID = Item::ID::FromCGItem(item);
|
||||
const uint8_t *record = (itemID > 0)
|
||||
? Item::StatAccum::FetchRecord(static_cast<uint32_t>(itemID))
|
||||
? Item::PeekRecord(static_cast<uint32_t>(itemID))
|
||||
: nullptr;
|
||||
|
||||
// Live per-instance descriptor fields.
|
||||
const uint8_t *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
const uint8_t *descriptor = Item::ObjectFields(item);
|
||||
int stackCount = 0;
|
||||
bool isBound = false;
|
||||
if (descriptor != nullptr) {
|
||||
|
||||
+5
-18
@@ -54,6 +54,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "cache/QueryLoad.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "dbc/Names.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -143,17 +144,10 @@ int __fastcall Script_GetRaceInfo(void *L) {
|
||||
return 0; // nil
|
||||
const int raceID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *file =
|
||||
(raceID > 0) ? DBC::StringField(Offsets::VAR_CHRRACES_RECORDS,
|
||||
Offsets::VAR_CHRRACES_COUNT,
|
||||
static_cast<uint32_t>(raceID),
|
||||
Offsets::OFF_CHRRACES_FILENAME)
|
||||
: nullptr;
|
||||
(raceID > 0) ? DBC::RaceToken(static_cast<uint32_t>(raceID)) : nullptr;
|
||||
if (file == nullptr)
|
||||
return 0; // unknown id -> nil
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_CHRRACES_RECORDS,
|
||||
Offsets::VAR_CHRRACES_COUNT,
|
||||
static_cast<uint32_t>(raceID),
|
||||
Offsets::OFF_CHRRACES_NAMES);
|
||||
const char *name = DBC::RaceName(static_cast<uint32_t>(raceID));
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "raceName", name); // NULL coerced to ""
|
||||
@@ -169,17 +163,10 @@ int __fastcall Script_GetClassInfo(void *L) {
|
||||
return 0; // nil
|
||||
const int classID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *file =
|
||||
(classID > 0) ? DBC::StringField(Offsets::VAR_CHRCLASSES_RECORDS,
|
||||
Offsets::VAR_CHRCLASSES_COUNT,
|
||||
static_cast<uint32_t>(classID),
|
||||
Offsets::OFF_CHRCLASSES_FILENAME)
|
||||
: nullptr;
|
||||
(classID > 0) ? DBC::ClassToken(static_cast<uint32_t>(classID)) : nullptr;
|
||||
if (file == nullptr)
|
||||
return 0; // unknown id -> nil
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_CHRCLASSES_RECORDS,
|
||||
Offsets::VAR_CHRCLASSES_COUNT,
|
||||
static_cast<uint32_t>(classID),
|
||||
Offsets::OFF_CHRCLASSES_NAMES);
|
||||
const char *name = DBC::ClassName(static_cast<uint32_t>(classID));
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "className", name); // NULL coerced to ""
|
||||
|
||||
+2
-12
@@ -53,6 +53,7 @@
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/QualityColor.h"
|
||||
#include "item/Record.h"
|
||||
#include "spell/Lookup.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -140,17 +141,6 @@ int PushBagItemCursor(void *L) {
|
||||
return PushItem(L, cgItem);
|
||||
}
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
// Builds a basic `"|cffRRGGBB|Hitem:N:0:0:0|h[Name]|h|r"` link from
|
||||
// the cached ItemStats record for `itemID`. No enchant / random-
|
||||
// suffix decoration — the cursor only stores the bare itemID for
|
||||
@@ -158,7 +148,7 @@ const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
// instance state from. Returns true and writes into `out` (size
|
||||
// `outSize`) on success; returns false if the item isn't cached.
|
||||
bool BuildBasicItemLink(uint32_t itemID, char *out, size_t outSize) {
|
||||
const uint8_t *record = PeekItemRecord(itemID);
|
||||
const uint8_t *record = Item::PeekRecord(itemID);
|
||||
if (record == nullptr) return false;
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// 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 "Names.h"
|
||||
|
||||
#include "Lookup.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/Ascii.h"
|
||||
|
||||
namespace DBC {
|
||||
|
||||
namespace {
|
||||
|
||||
// Reverse lookup shared by ClassIdForToken / RaceIdForToken: the id whose
|
||||
// single-string field at `offset` matches `token` (case-insensitive), or 0.
|
||||
uint32_t IdForStringField(uintptr_t recordsVar, uintptr_t countVar, int offset,
|
||||
const char *token) {
|
||||
if (token == nullptr || *token == '\0')
|
||||
return 0;
|
||||
const int count = *reinterpret_cast<const int *>(countVar);
|
||||
const uint8_t *const *records =
|
||||
*reinterpret_cast<const uint8_t *const *const *>(recordsVar);
|
||||
if (records == nullptr)
|
||||
return 0;
|
||||
for (int i = 1; i <= count; ++i) {
|
||||
const uint8_t *rec = records[i];
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
const char *field =
|
||||
*reinterpret_cast<const char *const *>(rec + offset);
|
||||
if (field != nullptr && Ascii::EqualCI(field, token))
|
||||
return static_cast<uint32_t>(i);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char *ClassName(uint32_t classID) {
|
||||
if (classID == 0)
|
||||
return nullptr;
|
||||
return LocalizedField(Offsets::VAR_CHRCLASSES_RECORDS,
|
||||
Offsets::VAR_CHRCLASSES_COUNT, classID,
|
||||
Offsets::OFF_CHRCLASSES_NAMES);
|
||||
}
|
||||
|
||||
const char *ClassToken(uint32_t classID) {
|
||||
if (classID == 0)
|
||||
return nullptr;
|
||||
return StringField(Offsets::VAR_CHRCLASSES_RECORDS,
|
||||
Offsets::VAR_CHRCLASSES_COUNT, classID,
|
||||
Offsets::OFF_CHRCLASSES_FILENAME);
|
||||
}
|
||||
|
||||
const char *RaceName(uint32_t raceID) {
|
||||
if (raceID == 0)
|
||||
return nullptr;
|
||||
return LocalizedField(Offsets::VAR_CHRRACES_RECORDS,
|
||||
Offsets::VAR_CHRRACES_COUNT, raceID,
|
||||
Offsets::OFF_CHRRACES_NAMES);
|
||||
}
|
||||
|
||||
const char *RaceToken(uint32_t raceID) {
|
||||
if (raceID == 0)
|
||||
return nullptr;
|
||||
return StringField(Offsets::VAR_CHRRACES_RECORDS,
|
||||
Offsets::VAR_CHRRACES_COUNT, raceID,
|
||||
Offsets::OFF_CHRRACES_FILENAME);
|
||||
}
|
||||
|
||||
uint32_t ClassIdForToken(const char *token) {
|
||||
return IdForStringField(Offsets::VAR_CHRCLASSES_RECORDS,
|
||||
Offsets::VAR_CHRCLASSES_COUNT,
|
||||
Offsets::OFF_CHRCLASSES_FILENAME, token);
|
||||
}
|
||||
|
||||
uint32_t RaceIdForToken(const char *token) {
|
||||
return IdForStringField(Offsets::VAR_CHRRACES_RECORDS,
|
||||
Offsets::VAR_CHRRACES_COUNT,
|
||||
Offsets::OFF_CHRRACES_FILENAME, token);
|
||||
}
|
||||
|
||||
const char *AreaName(uint32_t areaID, bool resolveToParent) {
|
||||
if (areaID == 0)
|
||||
return nullptr;
|
||||
// Only the parent-resolving path needs the record; the direct read goes
|
||||
// straight through LocalizedField (which bounds-checks the id itself), so
|
||||
// the common !resolveToParent case is a single lookup.
|
||||
uint32_t use = areaID;
|
||||
if (resolveToParent) {
|
||||
const uint8_t *rec = Record(Offsets::VAR_AREATABLE_RECORDS,
|
||||
Offsets::VAR_AREATABLE_COUNT, areaID);
|
||||
if (rec != nullptr) {
|
||||
const uint32_t parent = *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_AREATABLE_PARENT_ID);
|
||||
if (parent != 0 &&
|
||||
Record(Offsets::VAR_AREATABLE_RECORDS,
|
||||
Offsets::VAR_AREATABLE_COUNT, parent) != nullptr)
|
||||
use = parent;
|
||||
}
|
||||
}
|
||||
return LocalizedField(Offsets::VAR_AREATABLE_RECORDS,
|
||||
Offsets::VAR_AREATABLE_COUNT, use,
|
||||
Offsets::OFF_AREATABLE_NAMES);
|
||||
}
|
||||
|
||||
} // namespace DBC
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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>
|
||||
|
||||
// `DBC::*` display-name resolvers for the character/zone DBCs. Thin wrappers
|
||||
// over `DBC::Record` / `LocalizedField` / `StringField` (see `Lookup.h`) that
|
||||
// bake in the specific table + field for a game concept, so callers don't
|
||||
// repeat the `(recordsVar, countVar, id, offset)` tuple. Shared by the
|
||||
// friends-list and /who surfaces (and any future class/race/zone display).
|
||||
// All return `nullptr` when the id is 0 / out of range / the field is empty.
|
||||
|
||||
namespace DBC {
|
||||
|
||||
// Localized class name for a `ChrClasses.dbc` id (e.g. "Mage").
|
||||
const char *ClassName(uint32_t classID);
|
||||
|
||||
// Locale-independent class token for a `ChrClasses.dbc` id (e.g. "MAGE") —
|
||||
// the key addons use for RAID_CLASS_COLORS and other class tables.
|
||||
const char *ClassToken(uint32_t classID);
|
||||
|
||||
// Localized race name for a `ChrRaces.dbc` id (e.g. "Night Elf").
|
||||
const char *RaceName(uint32_t raceID);
|
||||
|
||||
// Locale-independent race token for a `ChrRaces.dbc` id — the client-filename
|
||||
// column (e.g. "NightElf", "Scourge"), the race analog of ClassToken and what
|
||||
// `UnitRace`'s second return / GetPlayerInfoByGUID's englishRace use.
|
||||
const char *RaceToken(uint32_t raceID);
|
||||
|
||||
// Reverse of ClassToken / RaceToken: the `ChrClasses.dbc` / `ChrRaces.dbc` id
|
||||
// whose client-filename token matches `token` (case-insensitive), or 0 if none
|
||||
// matches. Walks the table (cheap — a handful of records).
|
||||
uint32_t ClassIdForToken(const char *token);
|
||||
uint32_t RaceIdForToken(const char *token);
|
||||
|
||||
// Localized zone name for an `AreaTable.dbc` id. When `resolveToParent` is
|
||||
// true, a sub-area reports its parent zone (so "Goldshire" → "Elwynn
|
||||
// Forest"); when false, the area's own name is returned (what the engine's
|
||||
// GetWhoInfo does — /who zones are already zone-level).
|
||||
const char *AreaName(uint32_t areaID, bool resolveToParent);
|
||||
|
||||
} // namespace DBC
|
||||
+74
-4
@@ -17,6 +17,7 @@ extern "C" {
|
||||
#include <cbor.h>
|
||||
}
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
@@ -76,6 +77,27 @@ int CountTablePairs(void *L, int idx) {
|
||||
return n;
|
||||
}
|
||||
|
||||
// Encode a single Lua value at `idx` into a standalone byte vector,
|
||||
// reusing the same grow-and-retry contract as Script_SerializeCBOR.
|
||||
// Used to obtain a map key's encoded bytes for canonical sorting; the
|
||||
// Lua stack is left untouched (EncodeValue reads but never pops).
|
||||
bool EncodeValueToVector(void *L, int idx, int depth, std::vector<uint8_t> &out) {
|
||||
out.assign(64, 0);
|
||||
for (;;) {
|
||||
CborEncoder enc;
|
||||
cbor_encoder_init(&enc, out.data(), out.size(), 0);
|
||||
if (!EncodeValue(L, idx, &enc, depth))
|
||||
return false;
|
||||
const size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
|
||||
if (extra > 0) {
|
||||
out.resize(out.size() + extra);
|
||||
continue;
|
||||
}
|
||||
out.resize(cbor_encoder_get_buffer_size(&enc, out.data()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool EncodeTable(void *L, int idx, CborEncoder *enc, int depth) {
|
||||
if (depth >= kMaxDepth) return false;
|
||||
if (idx < 0) idx = Game::Lua::GetTop(L) + idx + 1;
|
||||
@@ -102,15 +124,63 @@ bool EncodeTable(void *L, int idx, CborEncoder *enc, int depth) {
|
||||
CborEncoder map;
|
||||
if (!EncOk(cbor_encoder_create_map(enc, &map, static_cast<size_t>(pairs))))
|
||||
return false;
|
||||
|
||||
// Canonical CBOR (RFC 7049 §3.9): emit map entries sorted by their
|
||||
// encoded-key bytes (shorter key first, then bytewise). Lua gives no
|
||||
// stable iteration order for string-keyed tables, so without this the
|
||||
// same config serializes to different byte orders each call. We stash
|
||||
// the keys in a holding array table (`hold`), remember each key's
|
||||
// encoded bytes + slot, sort, then re-fetch key/value in sorted order.
|
||||
struct Entry {
|
||||
std::vector<uint8_t> key;
|
||||
int slot;
|
||||
};
|
||||
std::vector<Entry> entries;
|
||||
entries.reserve(static_cast<size_t>(pairs));
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
const int hold = Game::Lua::GetTop(L);
|
||||
int slot = 0;
|
||||
Game::Lua::PushNil(L);
|
||||
while (Game::Lua::Next(L, idx) != 0) {
|
||||
if (!EncodeValue(L, -2, &map, depth + 1) ||
|
||||
!EncodeValue(L, -1, &map, depth + 1)) {
|
||||
Game::Lua::SetTop(L, -3);
|
||||
// key @ -2, value @ -1
|
||||
std::vector<uint8_t> keyBytes;
|
||||
if (!EncodeValueToVector(L, -2, depth + 1, keyBytes)) {
|
||||
Game::Lua::SetTop(L, hold - 1); // drop hold + key + value
|
||||
return false;
|
||||
}
|
||||
Game::Lua::SetTop(L, -2);
|
||||
++slot;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot));
|
||||
Game::Lua::PushValue(L, -3); // copy key to top
|
||||
Game::Lua::RawSet(L, hold); // hold[slot] = key
|
||||
entries.push_back(Entry{std::move(keyBytes), slot});
|
||||
Game::Lua::SetTop(L, -2); // pop value, keep key for Next
|
||||
}
|
||||
|
||||
std::sort(entries.begin(), entries.end(),
|
||||
[](const Entry &a, const Entry &b) {
|
||||
if (a.key.size() != b.key.size())
|
||||
return a.key.size() < b.key.size();
|
||||
return std::lexicographical_compare(
|
||||
a.key.begin(), a.key.end(), b.key.begin(), b.key.end());
|
||||
});
|
||||
|
||||
for (const Entry &e : entries) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(e.slot));
|
||||
Game::Lua::RawGet(L, hold); // push key = hold[slot]
|
||||
if (!EncodeValue(L, -1, &map, depth + 1)) {
|
||||
Game::Lua::SetTop(L, hold - 1);
|
||||
return false;
|
||||
}
|
||||
Game::Lua::RawGet(L, idx); // key -> value = table[key]
|
||||
if (!EncodeValue(L, -1, &map, depth + 1)) {
|
||||
Game::Lua::SetTop(L, hold - 1);
|
||||
return false;
|
||||
}
|
||||
Game::Lua::SetTop(L, -2); // pop value
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, hold - 1); // drop holding table
|
||||
return EncOk(cbor_encoder_close_container(enc, &map));
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "Set.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Swap.h"
|
||||
|
||||
@@ -72,8 +73,7 @@ const char *ArgString(void *L, int idx) {
|
||||
int ResolveItemID(const uint8_t *cgItem) {
|
||||
if (cgItem == nullptr)
|
||||
return 0;
|
||||
auto *instance = *reinterpret_cast<const uint8_t *const *>(
|
||||
cgItem + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
auto *instance = Item::InstanceBlock(cgItem);
|
||||
if (instance == nullptr)
|
||||
return 0;
|
||||
return static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "item/Location.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -58,13 +59,7 @@ int InvMgrSlotCount(const uint8_t *invMgr) {
|
||||
uint8_t *ResolveByGUID(int type, uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
using ResolveByGUID_t = void *(__fastcall *)(int, const char *, uint32_t,
|
||||
uint32_t, int);
|
||||
auto fn = reinterpret_cast<ResolveByGUID_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
|
||||
return static_cast<uint8_t *>(fn(type, "ItemMgr",
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32),
|
||||
0x172));
|
||||
return static_cast<uint8_t *>(Object::ByGuid(type, guid, "ItemMgr", 0x172));
|
||||
}
|
||||
|
||||
// Walks one bag's contents for a matching GUID. Returns the 1-based
|
||||
@@ -73,7 +68,7 @@ uint8_t *ResolveByGUID(int type, uint64_t guid) {
|
||||
// bank bags (linear 63..68) — the only differences are the source of
|
||||
// the bag's GUID and how the result encodes back into a location.
|
||||
int WalkBagContents(uint64_t bagGuid, uint64_t targetGuid) {
|
||||
auto *bag = ResolveByGUID(Offsets::OBJ_TYPE_CONTAINER, bagGuid);
|
||||
auto *bag = ResolveByGUID(Offsets::TYPEMASK_CONTAINER, bagGuid);
|
||||
if (bag == nullptr)
|
||||
return 0;
|
||||
auto *bagInvMgr =
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
#include "equipmentset/Data.h"
|
||||
#include "equipmentset/Locations.h"
|
||||
#include "equipmentset/Set.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -108,17 +109,6 @@ struct Tally {
|
||||
uint32_t missingItemIDs[SLOT_COUNT];
|
||||
};
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
// Walks a set's slots, classifying each non-empty entry into the
|
||||
// equipped / in-inventory / ignored / missing buckets via
|
||||
// `Locations::FindGUID`. For missing slots, also records the saved
|
||||
@@ -215,7 +205,7 @@ int __fastcall Script_GameTooltipSetEquipmentSet(void *L) {
|
||||
unnamedMissing++;
|
||||
continue;
|
||||
}
|
||||
auto *record = PeekItemRecord(id);
|
||||
auto *record = Item::PeekRecord(id);
|
||||
if (record == nullptr) {
|
||||
unnamedMissing++;
|
||||
continue;
|
||||
|
||||
+84
-31
@@ -96,7 +96,9 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/Ascii.h"
|
||||
#include "cursor/Info.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "spell/AtCursor.h"
|
||||
#include "spell/AtUnit.h"
|
||||
#include "tick/WorldTick.h"
|
||||
@@ -147,15 +149,6 @@ void Compose3Lower(char *dst, size_t n, const char *a, const char *b, const char
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
// ASCII case-insensitive full-string equality.
|
||||
bool EqI(const char *a, const char *b) {
|
||||
for (; *a && *b; ++a, ++b)
|
||||
if (std::tolower(static_cast<unsigned char>(*a)) !=
|
||||
std::tolower(static_cast<unsigned char>(*b)))
|
||||
return false;
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
// True if `lname` (already lowercase) is a click "type" action attribute: an
|
||||
// optional modifier prefix (alt-/ctrl-/shift-, any order/combo) followed by
|
||||
// "type" and an optional button digit — i.e. anything the click resolver
|
||||
@@ -410,6 +403,51 @@ 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);
|
||||
return Object::ByGuid(kUnitOrPlayerMask, guid, "ClassicAPI", 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 +461,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;
|
||||
}
|
||||
}
|
||||
@@ -455,10 +498,10 @@ void BuildModifierPrefix(void *L, char *buf, size_t n) {
|
||||
|
||||
// Button name -> attribute suffix (retail's convention: the button number).
|
||||
const char *ButtonSuffix(const char *btn) {
|
||||
if (EqI(btn, "RightButton")) return "2";
|
||||
if (EqI(btn, "MiddleButton")) return "3";
|
||||
if (EqI(btn, "Button4")) return "4";
|
||||
if (EqI(btn, "Button5")) return "5";
|
||||
if (Ascii::EqualCI(btn, "RightButton")) return "2";
|
||||
if (Ascii::EqualCI(btn, "MiddleButton")) return "3";
|
||||
if (Ascii::EqualCI(btn, "Button4")) return "4";
|
||||
if (Ascii::EqualCI(btn, "Button5")) return "5";
|
||||
return "1"; // LeftButton / unknown
|
||||
}
|
||||
|
||||
@@ -546,10 +589,10 @@ bool CallGlobalNum2(void *L, const char *name, double a, double b) {
|
||||
// Returns true if it owned the click (so the chained handler is skipped).
|
||||
bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
|
||||
const char *verb, const char *unit) {
|
||||
if (EqI(verb, "target")) {
|
||||
if (Ascii::EqualCI(verb, "target")) {
|
||||
if (!unit) return false;
|
||||
// `unit="none"` clears the target (retail's SecureActionButton behavior).
|
||||
if (EqI(unit, "none")) {
|
||||
if (Ascii::EqualCI(unit, "none")) {
|
||||
Game::Lua::CallGlobal(L, "ClearTarget");
|
||||
return true;
|
||||
}
|
||||
@@ -566,17 +609,17 @@ bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
|
||||
Game::Lua::CallGlobalString(L, "TargetUnit", unit);
|
||||
return true;
|
||||
}
|
||||
if (EqI(verb, "assist")) {
|
||||
if (Ascii::EqualCI(verb, "assist")) {
|
||||
if (!unit) return false;
|
||||
Game::Lua::CallGlobalString(L, "AssistUnit", unit);
|
||||
return true;
|
||||
}
|
||||
if (EqI(verb, "focus")) {
|
||||
if (Ascii::EqualCI(verb, "focus")) {
|
||||
if (!unit) return false;
|
||||
Unit::Focus::Set(Unit::Identity::GuidForToken(unit));
|
||||
return true;
|
||||
}
|
||||
if (EqI(verb, "spell")) {
|
||||
if (Ascii::EqualCI(verb, "spell")) {
|
||||
if (!unit) return false;
|
||||
char spell[128];
|
||||
if (!ReadModAttr(L, fi, prefix, "spell", suffix, spell, sizeof spell))
|
||||
@@ -584,7 +627,7 @@ bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
|
||||
Spell::AtUnit::CastByName(spell, unit);
|
||||
return true;
|
||||
}
|
||||
if (EqI(verb, "item")) {
|
||||
if (Ascii::EqualCI(verb, "item")) {
|
||||
char item[128];
|
||||
int bag, slot;
|
||||
if (ReadModAttr(L, fi, prefix, "item", suffix, item, sizeof item)) {
|
||||
@@ -602,23 +645,33 @@ bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (EqI(verb, "macro")) {
|
||||
if (Ascii::EqualCI(verb, "macro")) {
|
||||
char macro[512];
|
||||
if (!ReadModAttr(L, fi, prefix, "macrotext", suffix, macro, sizeof macro) &&
|
||||
!ReadModAttr(L, fi, prefix, "macro", suffix, macro, sizeof macro))
|
||||
return false;
|
||||
// Prefer an addon-provided RunMacro (SuperCleveRoidMacros, pfUI, …) — it
|
||||
// handles named macros and extended macro text; fall back to the stock
|
||||
// ChatEdit_ParseText path when no RunMacro global is present.
|
||||
if (!Game::Lua::CallGlobalString(L, "RunMacro", macro))
|
||||
// `macrotext` is raw macro text (the modern default). Run it as text
|
||||
// through the stock chat parser so each line dispatches via
|
||||
// SlashCmdList — that routes /cast etc. through any addon slash hooks
|
||||
// (e.g. SuperCleveRoidMacros' conditional /cast). A name-based RunMacro
|
||||
// (which is what SuperCleveRoidMacros ships) can't interpret text and
|
||||
// would silently resolve nothing.
|
||||
if (ReadModAttr(L, fi, prefix, "macrotext", suffix, macro, sizeof macro)) {
|
||||
RunMacroTextC(L, macro);
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
// Deprecated `macro` form: a saved-macro name/index. Prefer an
|
||||
// addon-provided RunMacro (SuperCleveRoidMacros, pfUI, …) to run the
|
||||
// named macro's body; fall back to the stock parser when none exists.
|
||||
if (ReadModAttr(L, fi, prefix, "macro", suffix, macro, sizeof macro)) {
|
||||
if (!Game::Lua::CallGlobalString(L, "RunMacro", macro))
|
||||
RunMacroTextC(L, macro);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (EqI(verb, "stop") || EqI(verb, "stopcasting")) {
|
||||
if (Ascii::EqualCI(verb, "stop") || Ascii::EqualCI(verb, "stopcasting")) {
|
||||
Game::Lua::CallGlobal(L, "SpellStopCasting");
|
||||
return true;
|
||||
}
|
||||
if (EqI(verb, "menu") || EqI(verb, "togglemenu")) {
|
||||
if (Ascii::EqualCI(verb, "menu") || Ascii::EqualCI(verb, "togglemenu")) {
|
||||
if (!unit) return false;
|
||||
// The unit dropdown is pure FrameXML work (UnitPopup + ToggleDropDown),
|
||||
// so it lives in the !!!ClassicAPI addon; we just pop it at the cursor.
|
||||
@@ -738,7 +791,7 @@ int __fastcall FrameResolver_h(void *frame, void *edx, const char *name) {
|
||||
const int slot = g_frameResolverOriginal(frame, edx, name);
|
||||
if (slot != 0) // a real base-frame / subtype script — leave it
|
||||
return slot;
|
||||
if (EqI(name, "onattributechanged"))
|
||||
if (Ascii::EqualCI(name, "onattributechanged"))
|
||||
return reinterpret_cast<int>(AttrSlotFor(frame, /*create*/ true));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// 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/>.
|
||||
|
||||
// `GetMouseFoci()` -> table of every frame under the cursor, top-first.
|
||||
//
|
||||
// Modern WoW exposes GetMouseFoci() alongside GetMouseFocus(), with the
|
||||
// invariant `GetMouseFocus() == GetMouseFoci()[1]`. Vanilla stores only the
|
||||
// single topmost focus — Script_GetMouseFocus (0x0048DF40) reads
|
||||
// `*(*VAR_UI_CONTEXT_PTR + OFF_UI_CONTEXT_MOUSE_FOCUS)` and there is no stored
|
||||
// stack — so this is a native reimplementation of the frame-stack walk
|
||||
// Blizzard_DebugTools does in Lua (`EnumerateFrames` + `IsMouseOver`):
|
||||
//
|
||||
// 1. Walk the engine frame list (the same one Script_EnumerateFrames
|
||||
// 0x00705F60 iterates): head at `*(ctx + OFF_UI_CONTEXT_FRAME_LIST_HEAD)`,
|
||||
// each frame's next at `+OFF_FRAME_ENUM_NEXT`, a low-bit-set / null node
|
||||
// ends it.
|
||||
// 2. Keep frames that are visible + mouse-enabled and whose rect contains the
|
||||
// cursor (hit-test mirrors Frame::Modern's IsMouseOver: Region getters +
|
||||
// cursor / effective-scale).
|
||||
// 3. Order top-first by strata rank then frame level.
|
||||
// 4. Force index 1 to be the engine's stored focus so the documented
|
||||
// invariant holds exactly, regardless of how our sort tie-breaks.
|
||||
//
|
||||
// Pure query — no hooks, registered like GetMouseFocus.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "ui/FrameObject.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace Frame::MouseFoci {
|
||||
|
||||
namespace {
|
||||
|
||||
using ScriptFn_t = int(__fastcall *)(void *L);
|
||||
|
||||
int CallScript(uintptr_t fn, void *L) {
|
||||
return reinterpret_cast<ScriptFn_t>(fn)(L);
|
||||
}
|
||||
|
||||
// The UI context is a pointer stored at VAR_UI_CONTEXT_PTR (null pre-world).
|
||||
void *UIContext() {
|
||||
return *reinterpret_cast<void *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_UI_CONTEXT_PTR));
|
||||
}
|
||||
|
||||
// Strata name -> draw rank, low (behind) to high (on top). Unknown -> 0.
|
||||
int StrataRank(const char *s) {
|
||||
if (s == nullptr)
|
||||
return 0;
|
||||
static const struct {
|
||||
const char *name;
|
||||
int rank;
|
||||
} kStrata[] = {
|
||||
{"WORLD", 0}, {"BACKGROUND", 1}, {"LOW", 2},
|
||||
{"MEDIUM", 3}, {"HIGH", 4}, {"DIALOG", 5},
|
||||
{"FULLSCREEN", 6}, {"FULLSCREEN_DIALOG", 7}, {"TOOLTIP", 8},
|
||||
};
|
||||
for (const auto &e : kStrata)
|
||||
if (std::strcmp(s, e.name) == 0)
|
||||
return e.rank;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Region getter that pushes one number, called with `self` at Lua idx 1.
|
||||
// Returns false when it pushed nil/none (unresolved rect).
|
||||
bool SelfNumber(void *L, uintptr_t fn, double *out) {
|
||||
Game::Lua::SetTop(L, 1); // (self)
|
||||
CallScript(fn, L); // (self, value | nil)
|
||||
if (!Game::Lua::IsNumber(L, 2))
|
||||
return false;
|
||||
*out = Game::Lua::ToNumber(L, 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Boolean predicate (IsVisible / IsMouseEnabled), `self` at Lua idx 1.
|
||||
bool SelfBool(void *L, uintptr_t fn) {
|
||||
Game::Lua::SetTop(L, 1);
|
||||
CallScript(fn, L);
|
||||
return Game::Lua::ToBoolean(L, 2) != 0;
|
||||
}
|
||||
|
||||
struct Cursor {
|
||||
double x, y;
|
||||
};
|
||||
|
||||
// GetCursorPosition and the Region rect getters share the same scale factor;
|
||||
// dividing the cursor by the frame's effective scale (the field GetLeft/etc.
|
||||
// themselves divide by) puts both in the frame's logical space. Mirrors
|
||||
// Frame::Modern's IsMouseOver exactly.
|
||||
Cursor ReadCursor(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
CallScript(Offsets::FUN_SCRIPT_GETCURSORPOSITION, L); // (x, y)
|
||||
return Cursor{Game::Lua::ToNumber(L, 1), Game::Lua::ToNumber(L, 2)};
|
||||
}
|
||||
|
||||
// With `self` at Lua idx 1, is the cursor within the frame's rect?
|
||||
bool CursorOverSelf(void *L, void *obj, const Cursor &cur) {
|
||||
const float effScale = *reinterpret_cast<const float *>(
|
||||
reinterpret_cast<const uint8_t *>(obj) + Offsets::OFF_REGION_EFFECTIVE_SCALE);
|
||||
if (effScale == 0.0f)
|
||||
return false; // unpositioned region — no rect
|
||||
double left, right, top, bottom;
|
||||
if (!SelfNumber(L, Offsets::FUN_SCRIPT_REGION_GETLEFT, &left) ||
|
||||
!SelfNumber(L, Offsets::FUN_SCRIPT_REGION_GETRIGHT, &right) ||
|
||||
!SelfNumber(L, Offsets::FUN_SCRIPT_REGION_GETTOP, &top) ||
|
||||
!SelfNumber(L, Offsets::FUN_SCRIPT_REGION_GETBOTTOM, &bottom))
|
||||
return false;
|
||||
const double x = cur.x / effScale;
|
||||
const double y = cur.y / effScale;
|
||||
return x >= left && x <= right && y >= bottom && y <= top;
|
||||
}
|
||||
|
||||
struct Hit {
|
||||
void *obj;
|
||||
int strata;
|
||||
int level;
|
||||
};
|
||||
|
||||
int __fastcall Script_GetMouseFoci(void *L) {
|
||||
void *ctx = UIContext();
|
||||
Game::Lua::SetTop(L, 0);
|
||||
if (ctx == nullptr) {
|
||||
Game::Lua::NewTable(L); // pre-world → empty table
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The engine's authoritative topmost focus — the frame GetMouseFocus
|
||||
// returns. Forced to index 1 below.
|
||||
void *focus = *reinterpret_cast<void *const *>(
|
||||
reinterpret_cast<uint8_t *>(ctx) + Offsets::OFF_UI_CONTEXT_MOUSE_FOCUS);
|
||||
|
||||
const Cursor cur = ReadCursor(L);
|
||||
|
||||
std::vector<Hit> hits;
|
||||
void *node = *reinterpret_cast<void *const *>(
|
||||
reinterpret_cast<uint8_t *>(ctx) + Offsets::OFF_UI_CONTEXT_FRAME_LIST_HEAD);
|
||||
while (node != nullptr && (reinterpret_cast<uintptr_t>(node) & 1) == 0) {
|
||||
// Read the next link before any Lua work (the walk is engine-list
|
||||
// based, independent of the Lua stack, but this keeps it obvious).
|
||||
void *next = *reinterpret_cast<void *const *>(
|
||||
reinterpret_cast<uint8_t *>(node) + Offsets::OFF_FRAME_ENUM_NEXT);
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
UI::FrameObject::Push(L, node); // (self)
|
||||
|
||||
if (SelfBool(L, Offsets::FUN_SCRIPT_FRAME_IS_VISIBLE) &&
|
||||
SelfBool(L, Offsets::FUN_SCRIPT_FRAME_IS_MOUSE_ENABLED) &&
|
||||
CursorOverSelf(L, node, cur)) {
|
||||
Game::Lua::SetTop(L, 1);
|
||||
CallScript(Offsets::FUN_SCRIPT_FRAME_GET_STRATA, L); // (self, strata)
|
||||
const int rank = StrataRank(Game::Lua::ToString(L, 2));
|
||||
const int level = *reinterpret_cast<const int *>(
|
||||
reinterpret_cast<uint8_t *>(node) + Offsets::OFF_FRAME_LEVEL);
|
||||
hits.push_back(Hit{node, rank, level});
|
||||
}
|
||||
|
||||
node = next;
|
||||
}
|
||||
|
||||
// Top-first: higher strata, then higher level within a strata.
|
||||
std::sort(hits.begin(), hits.end(), [](const Hit &a, const Hit &b) {
|
||||
if (a.strata != b.strata)
|
||||
return a.strata > b.strata;
|
||||
return a.level > b.level;
|
||||
});
|
||||
|
||||
// Order the frames, forcing GetMouseFocus() to be [1] so the invariant
|
||||
// holds by construction (focus may be a frame our rect test skips, e.g.
|
||||
// the WorldFrame, or a same-strata/level tie our sort orders differently).
|
||||
std::vector<void *> ordered;
|
||||
ordered.reserve(hits.size() + 1);
|
||||
if (focus != nullptr)
|
||||
ordered.push_back(focus);
|
||||
for (const Hit &h : hits)
|
||||
if (h.obj != focus)
|
||||
ordered.push_back(h.obj);
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L); // result at idx 1
|
||||
for (size_t i = 0; i < ordered.size(); ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
UI::FrameObject::Push(L, ordered[i]);
|
||||
Game::Lua::RawSet(L, 1); // result[i+1] = frame
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Game::Lua::RegisterGlobalFunction("GetMouseFoci", &Script_GetMouseFoci);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Frame::MouseFoci
|
||||
+136
-103
@@ -29,8 +29,9 @@
|
||||
// purely additive; Lua silently drops the extra positional args a zero-param
|
||||
// handler doesn't declare), but push `self` + `arg1..argN` as real arguments
|
||||
// and `pcall` with `nargs = 1 + N`. The reimplementation mirrors the engine's
|
||||
// exact global save/set/restore and its message-handler errfunc; only the
|
||||
// final push+pcall differs.
|
||||
// message-handler errfunc, but saves the clobbered globals on the Lua stack
|
||||
// (not registry refs like the engine) so an out-of-memory longjmp mid-push
|
||||
// unwinds them for free — no leaked refs, no per-call ref churn. See RunModern.
|
||||
//
|
||||
// Convention: `(self, arg1..argN)` for every script, plus `event` inserted as
|
||||
// the first positional for OnEvent — `(self, event, arg1..argN)` — matching
|
||||
@@ -67,17 +68,11 @@ constexpr int kMaxArgs = 19;
|
||||
// push a value stored under an integer ref (handler / frame object / message
|
||||
// handler / luaL_ref'd saved globals).
|
||||
using RawGetI_t = void(__fastcall *)(void *L, int idx, int ref);
|
||||
// luaL_ref(L, t) → int — pop the top, store it under a fresh int key in table
|
||||
// `t`, return the key. luaL_unref frees it.
|
||||
using LuaLRef_t = int(__fastcall *)(void *L, int t);
|
||||
using LuaLUnref_t = void(__fastcall *)(void *L, int t, int ref);
|
||||
// FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER — __thiscall(frame, nameOrNull);
|
||||
// lazily materializes the frame's Lua-registry ref at frame+OFF_COBJECT_LUA_REF.
|
||||
using ScriptRegister_t = void(__thiscall *)(void *frame, void *nameOrNull);
|
||||
|
||||
const auto RawGetI = reinterpret_cast<RawGetI_t>(Offsets::LUA_RAWGETI);
|
||||
const auto LuaLRef = reinterpret_cast<LuaLRef_t>(Offsets::LUA_REF_REF);
|
||||
const auto LuaLUnref = reinterpret_cast<LuaLUnref_t>(Offsets::LUA_REF_UNREF);
|
||||
|
||||
// Returns the frame's Lua object ref (frame+0x08), lazily creating it exactly
|
||||
// as the engine runner does (`if (refcount == 0) ScriptRegister(frame, 0)`).
|
||||
@@ -104,40 +99,93 @@ void ArgName(char *buf, int k) {
|
||||
}
|
||||
}
|
||||
|
||||
// Sets _G[name] = <value currently on the stack top>, capturing the previous
|
||||
// _G[name] into *outOldRef (a luaL_ref for later restore). Stack-neutral:
|
||||
// consumes the value, leaves the stack as it was minus that value.
|
||||
void SetGlobalSaving(void *L, const char *name, int *outOldRef) {
|
||||
using namespace Game::Lua;
|
||||
PushString(L, name); // [.., newVal, name]
|
||||
GetTable(L, GLOBALS_INDEX); // [.., newVal, oldVal]
|
||||
*outOldRef = LuaLRef(L, REGISTRY_INDEX); // [.., newVal]
|
||||
PushString(L, name); // [.., newVal, name]
|
||||
Insert(L, -2); // [.., name, newVal]
|
||||
SetTable(L, GLOBALS_INDEX); // [..]
|
||||
// One parsed positional argument from the runner's printf-style format.
|
||||
// Numbers ('d'/'u'/'f') land in `num`; strings ('s') keep the char* the engine
|
||||
// handed us (stable for the duration of the call).
|
||||
struct Arg {
|
||||
char name[6]; // "arg1".."arg19"
|
||||
char kind; // 'd' | 'u' | 'f' | 's'
|
||||
double num;
|
||||
const char *str;
|
||||
};
|
||||
|
||||
// Decodes `fmt`/`vaPtr` (the runner's %d/%u/%f/%s specs over a packed vararg
|
||||
// buffer) into `out`, returning the count (capped at kMaxArgs). Unknown specs
|
||||
// are skipped and consume no vararg — mirroring the engine runner's switch.
|
||||
// Pure C, touches no Lua state, so it can never throw.
|
||||
int ParseArgs(const char *fmt, const void *vaPtr, Arg *out) {
|
||||
if (fmt == nullptr)
|
||||
return 0;
|
||||
const char *cur = static_cast<const char *>(vaPtr);
|
||||
int n = 0;
|
||||
for (const char *p = fmt; *p != '\0' && n < kMaxArgs; ++p) {
|
||||
if (*p != '%')
|
||||
continue;
|
||||
++p;
|
||||
if (*p == '\0')
|
||||
break;
|
||||
Arg &a = out[n];
|
||||
switch (*p) {
|
||||
case 'd':
|
||||
a.num = static_cast<double>(*reinterpret_cast<const int *>(cur));
|
||||
cur += 4;
|
||||
break;
|
||||
case 'u':
|
||||
a.num = static_cast<double>(
|
||||
*reinterpret_cast<const unsigned int *>(cur));
|
||||
cur += 4;
|
||||
break;
|
||||
case 'f':
|
||||
a.num = *reinterpret_cast<const double *>(cur);
|
||||
cur += 8;
|
||||
break;
|
||||
case 's':
|
||||
a.str = *reinterpret_cast<const char *const *>(cur);
|
||||
cur += 4;
|
||||
break;
|
||||
default:
|
||||
continue; // unknown spec — consume nothing
|
||||
}
|
||||
a.kind = *p;
|
||||
ArgName(a.name, n + 1);
|
||||
++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Restores _G[name] to the value at oldRef, then frees the ref.
|
||||
void RestoreGlobal(void *L, const char *name, int oldRef) {
|
||||
using namespace Game::Lua;
|
||||
RawGetI(L, REGISTRY_INDEX, oldRef); // [.., oldVal]
|
||||
PushString(L, name); // [.., oldVal, name]
|
||||
Insert(L, -2); // [.., name, oldVal]
|
||||
SetTable(L, GLOBALS_INDEX); // [..]
|
||||
LuaLUnref(L, REGISTRY_INDEX, oldRef);
|
||||
// Pushes a parsed argument's value onto the Lua stack.
|
||||
void PushArg(void *L, const Arg &a) {
|
||||
if (a.kind == 's')
|
||||
Game::Lua::PushString(L, a.str);
|
||||
else
|
||||
Game::Lua::PushNumber(L, a.num);
|
||||
}
|
||||
|
||||
// Reimplements the runner tail: set globals (with save/restore), then invoke
|
||||
// the handler with modern positional args `(self, arg1..argN)` under the
|
||||
// engine's own message-handler errfunc. `fmt`/`vaPtr` are null for the no-arg
|
||||
// runner (N == 0).
|
||||
void RunModern(int handlerRef, void *frame, const char *fmt, const void *vaPtr) {
|
||||
// Reimplements the runner tail: set the `this`/`arg1..argN` globals, invoke the
|
||||
// handler with modern positional args `(self, [event,] arg1..argN)` under the
|
||||
// engine's own message-handler errfunc, then restore the globals. `fmt`/`vaPtr`
|
||||
// are null for the no-arg runner (N == 0).
|
||||
//
|
||||
// Unlike the engine (and our first cut), the old global values are saved on the
|
||||
// Lua STACK, not in the registry via luaL_ref. A registry save leaks its ref if
|
||||
// any push before the protected call longjmps — and out-of-memory, the one
|
||||
// failure that actually fires here, does exactly that. Stack slots need no
|
||||
// cleanup: an unwinding error resets `L->top` past them for free, so nothing
|
||||
// leaks and there's no per-call ref churn on the OnUpdate hot path. The cost is
|
||||
// a deeper stack (up to ~2*N slots live at once), so we CheckStack up front.
|
||||
//
|
||||
// Returns false WITHOUT side effects when it declines (no Lua state, or the
|
||||
// stack can't grow); the caller then runs the original. Every false path is
|
||||
// before the first push, so falling back never double-runs the handler.
|
||||
bool RunModern(int handlerRef, void *frame, const char *fmt, const void *vaPtr) {
|
||||
using namespace Game::Lua;
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
return false;
|
||||
// Headroom for the saved-old block plus the transient set/call pushes.
|
||||
if (CheckStack(L, 2 * kMaxArgs + 16) == 0)
|
||||
return false;
|
||||
|
||||
// --- _G.this = frame -----------------------------------------------------
|
||||
const bool haveThis = frame != nullptr;
|
||||
// OnEvent dispatch? The event dispatchers invoke the frame's OnEvent slot
|
||||
// (frame+0x0C); matching the handler ref against that slot identifies it
|
||||
@@ -148,65 +196,46 @@ void RunModern(int handlerRef, void *frame, const char *fmt, const void *vaPtr)
|
||||
*reinterpret_cast<const int *>(reinterpret_cast<uint8_t *>(frame) +
|
||||
Offsets::OFF_FRAME_ONEVENT_SLOT) ==
|
||||
handlerRef;
|
||||
int selfRef = 0;
|
||||
int oldThisRef = 0;
|
||||
if (haveThis) {
|
||||
selfRef = EnsureLuaRef(frame);
|
||||
RawGetI(L, REGISTRY_INDEX, selfRef); // push frame object
|
||||
SetGlobalSaving(L, "this", &oldThisRef);
|
||||
}
|
||||
const int selfRef = haveThis ? EnsureLuaRef(frame) : 0;
|
||||
|
||||
// --- _G.arg1..argN from the format --------------------------------------
|
||||
int oldArgRefs[kMaxArgs];
|
||||
char argNames[kMaxArgs][6];
|
||||
int n = 0;
|
||||
if (fmt != nullptr) {
|
||||
const char *cur = static_cast<const char *>(vaPtr);
|
||||
for (const char *p = fmt; *p != '\0' && n < kMaxArgs; ++p) {
|
||||
if (*p != '%')
|
||||
continue;
|
||||
++p;
|
||||
if (*p == '\0')
|
||||
break;
|
||||
bool consumed = true;
|
||||
switch (*p) {
|
||||
case 'd':
|
||||
PushNumber(L, static_cast<double>(*reinterpret_cast<const int *>(cur)));
|
||||
cur += 4;
|
||||
break;
|
||||
case 'u':
|
||||
PushNumber(L, static_cast<double>(
|
||||
*reinterpret_cast<const unsigned int *>(cur)));
|
||||
cur += 4;
|
||||
break;
|
||||
case 'f':
|
||||
PushNumber(L, *reinterpret_cast<const double *>(cur));
|
||||
cur += 8;
|
||||
break;
|
||||
case 's':
|
||||
PushString(L, *reinterpret_cast<const char *const *>(cur));
|
||||
cur += 4;
|
||||
break;
|
||||
default:
|
||||
consumed = false; // unknown spec — skip, consume nothing
|
||||
break;
|
||||
}
|
||||
if (!consumed)
|
||||
continue;
|
||||
ArgName(argNames[n], n + 1);
|
||||
SetGlobalSaving(L, argNames[n], &oldArgRefs[n]);
|
||||
++n;
|
||||
}
|
||||
}
|
||||
Arg args[kMaxArgs];
|
||||
const int n = ParseArgs(fmt, vaPtr, args);
|
||||
|
||||
// --- call handler(self, arg1..argN) -------------------------------------
|
||||
// --- save the globals we're about to clobber, onto the stack ------------
|
||||
// Absolute indices, live until the closing SetTop(base):
|
||||
// base+1 = old _G.this (if haveThis)
|
||||
// savedArgBase+1 .. +n = old _G.arg1..argN
|
||||
const int base = GetTop(L);
|
||||
if (haveThis) {
|
||||
PushString(L, "this");
|
||||
GetTable(L, GLOBALS_INDEX); // [.. oldThis]
|
||||
}
|
||||
const int savedArgBase = base + (haveThis ? 1 : 0);
|
||||
for (int k = 0; k < n; ++k) {
|
||||
PushString(L, args[k].name);
|
||||
GetTable(L, GLOBALS_INDEX); // [.. oldArgk]
|
||||
}
|
||||
|
||||
// --- set the new globals (each op is stack-neutral) ---------------------
|
||||
if (haveThis) {
|
||||
PushString(L, "this");
|
||||
RawGetI(L, REGISTRY_INDEX, selfRef); // frame object
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
}
|
||||
for (int k = 0; k < n; ++k) {
|
||||
PushString(L, args[k].name);
|
||||
PushArg(L, args[k]);
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
}
|
||||
|
||||
// --- call handler(self, [event,] arg1..argN) ---------------------------
|
||||
const int callBase = GetTop(L); // == base + saved-block size
|
||||
const int errRef = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
|
||||
int errIdx = 0;
|
||||
if (errRef > 0) {
|
||||
RawGetI(L, REGISTRY_INDEX, errRef); // message handler
|
||||
errIdx = base + 1; // absolute index of the errfunc
|
||||
errIdx = callBase + 1; // absolute index of the errfunc
|
||||
}
|
||||
RawGetI(L, REGISTRY_INDEX, handlerRef); // handler
|
||||
if (haveThis)
|
||||
@@ -217,18 +246,24 @@ void RunModern(int handlerRef, void *frame, const char *fmt, const void *vaPtr)
|
||||
PushString(L, "event");
|
||||
GetTable(L, GLOBALS_INDEX);
|
||||
}
|
||||
for (int k = 0; k < n; ++k) { // positional args, re-read from the globals
|
||||
PushString(L, argNames[k]);
|
||||
GetTable(L, GLOBALS_INDEX);
|
||||
}
|
||||
for (int k = 0; k < n; ++k)
|
||||
PushArg(L, args[k]);
|
||||
PCall(L, 1 + (isOnEvent ? 1 : 0) + n, 0, errIdx);
|
||||
SetTop(L, base); // drop the message handler + any leftover error object
|
||||
SetTop(L, callBase); // drop the message handler + any leftover error object
|
||||
|
||||
// --- restore globals (reverse order) ------------------------------------
|
||||
for (int k = n - 1; k >= 0; --k)
|
||||
RestoreGlobal(L, argNames[k], oldArgRefs[k]);
|
||||
if (haveThis)
|
||||
RestoreGlobal(L, "this", oldThisRef);
|
||||
// --- restore globals from the saved block, then drop it -----------------
|
||||
for (int k = 0; k < n; ++k) {
|
||||
PushString(L, args[k].name);
|
||||
PushValue(L, savedArgBase + 1 + k);
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
}
|
||||
if (haveThis) {
|
||||
PushString(L, "this");
|
||||
PushValue(L, base + 1);
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
}
|
||||
SetTop(L, base);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- FUN_FRAME_RUN_SCRIPT_ARGS co-hook (scripts with values) ----------------
|
||||
@@ -238,11 +273,10 @@ RunArgs_t g_origRunArgs = nullptr;
|
||||
|
||||
void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt,
|
||||
const void *vaPtr) {
|
||||
if (!g_enabled || handlerRef == 0 || fmt == nullptr) {
|
||||
g_origRunArgs(handlerRef, frame, fmt, vaPtr);
|
||||
if (g_enabled && handlerRef != 0 && fmt != nullptr &&
|
||||
RunModern(handlerRef, frame, fmt, vaPtr))
|
||||
return;
|
||||
}
|
||||
RunModern(handlerRef, frame, fmt, vaPtr);
|
||||
g_origRunArgs(handlerRef, frame, fmt, vaPtr);
|
||||
}
|
||||
|
||||
// --- FUN_FRAME_INVOKE_SCRIPT co-hook (no-arg scripts) -----------------------
|
||||
@@ -252,11 +286,10 @@ using Invoke_t = void(__fastcall *)(int handlerRef, void *frame);
|
||||
Invoke_t g_origInvoke = nullptr;
|
||||
|
||||
void __fastcall Invoke_h(int handlerRef, void *frame) {
|
||||
if (!g_enabled || handlerRef == 0 || frame == nullptr) {
|
||||
g_origInvoke(handlerRef, frame);
|
||||
if (g_enabled && handlerRef != 0 && frame != nullptr &&
|
||||
RunModern(handlerRef, frame, /*fmt*/ nullptr, /*vaPtr*/ nullptr))
|
||||
return;
|
||||
}
|
||||
RunModern(handlerRef, frame, /*fmt*/ nullptr, /*vaPtr*/ nullptr);
|
||||
g_origInvoke(handlerRef, frame);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _runArgsHook{
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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 "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Shared access to the client's friend list. The friend entries live inline
|
||||
// in the social singleton (`VAR_SOCIAL_SYSTEM`, shared with the ignore list
|
||||
// and /who): up to 50 entries from offset 0, stride 0x20. The field offsets
|
||||
// are verified from GetNumFriends (FUN_005AD000), GetFriendInfo
|
||||
// (FUN_005AD060), and the count walker FUN_005AE490.
|
||||
//
|
||||
// The ignore list lives in the same singleton: a flat array of u64 GUIDs at
|
||||
// `+0x650`, stride 8, up to 25 entries, terminated by the first zero GUID.
|
||||
// Vanilla ignores are GUID-keyed, not name-keyed — GetIgnoreName (0x005AD460)
|
||||
// reads the GUID via FUN_005AE570 (`social + 0x650 + index*8`) and resolves it
|
||||
// to a name through the NameCache, and GetNumIgnores (0x005AD400 → the count
|
||||
// walker FUN_005AE550) stops at the first zero GUID and caps at 25.
|
||||
|
||||
namespace FriendList {
|
||||
|
||||
constexpr int FRIEND_ENTRY_STRIDE = 0x20;
|
||||
constexpr int MAX_FRIENDS = 50;
|
||||
|
||||
constexpr int OFF_FRIEND_CONNECTED = 0x00; // u8 — 0 = offline
|
||||
constexpr int OFF_FRIEND_STATUS = 0x01; // u8 — bit 0x02 AFK, 0x04 DND
|
||||
constexpr int OFF_FRIEND_NAME = 0x04; // char*
|
||||
constexpr int OFF_FRIEND_GUID = 0x08; // u64
|
||||
constexpr int OFF_FRIEND_LEVEL = 0x10; // i32
|
||||
constexpr int OFF_FRIEND_CLASS_INDEX = 0x14; // i32 → ChrClasses.dbc
|
||||
constexpr int OFF_FRIEND_AREA_INDEX = 0x18; // i32 → AreaTable.dbc
|
||||
|
||||
constexpr uint8_t FRIEND_STATUS_AFK = 0x02;
|
||||
constexpr uint8_t FRIEND_STATUS_DND = 0x04;
|
||||
|
||||
constexpr int OFF_IGNORE_LIST = 0x650; // u64 GUID array in the social singleton
|
||||
constexpr int IGNORE_ENTRY_STRIDE = 0x8; // one packed u64 GUID per entry
|
||||
constexpr int MAX_IGNORES = 25; // 0x19 — GetNumIgnores' cap
|
||||
|
||||
// The social singleton, or null before login.
|
||||
inline const uint8_t *Social() {
|
||||
return *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SOCIAL_SYSTEM));
|
||||
}
|
||||
|
||||
// Number of populated friend slots (what GetNumFriends returns).
|
||||
inline int Count() {
|
||||
const uint8_t *social = Social();
|
||||
if (social == nullptr)
|
||||
return 0;
|
||||
using CountFn = uint32_t(__fastcall *)(const void *social);
|
||||
auto fn = reinterpret_cast<CountFn>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FRIEND_LIST_COUNT));
|
||||
return static_cast<int>(fn(social));
|
||||
}
|
||||
|
||||
// Friend entry at 0-based `index`, or null if there is no list or the index
|
||||
// is out of the fixed slot range. Does not bounds-check against the live
|
||||
// count — callers that iterate should use `Count()`.
|
||||
inline const uint8_t *EntryByIndex(int index) {
|
||||
const uint8_t *social = Social();
|
||||
if (social == nullptr || index < 0 || index >= MAX_FRIENDS)
|
||||
return nullptr;
|
||||
return social + index * FRIEND_ENTRY_STRIDE;
|
||||
}
|
||||
|
||||
// The inline name of the friend with GUID `guid`, or null if that GUID is
|
||||
// not on the friends list. The list keeps name + GUID for online AND offline
|
||||
// friends (SMSG_FRIEND_LIST), so this resolves a GUID to a name even when the
|
||||
// unit is not synced in the object manager and not in the name cache.
|
||||
inline const char *NameForGuid(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
const int count = Count();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint8_t *entry = EntryByIndex(i);
|
||||
if (entry == nullptr)
|
||||
break;
|
||||
if (*reinterpret_cast<const uint64_t *>(entry + OFF_FRIEND_GUID) == guid) {
|
||||
const char *name =
|
||||
*reinterpret_cast<const char *const *>(entry + OFF_FRIEND_NAME);
|
||||
return (name != nullptr && *name != '\0') ? name : nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The ignore-list GUID at 0-based `index`, or 0 if there is no list or the
|
||||
// index is past the fixed cap. Mirrors FUN_005AE570.
|
||||
inline uint64_t IgnoreGuidByIndex(int index) {
|
||||
const uint8_t *social = Social();
|
||||
if (social == nullptr || index < 0 || index >= MAX_IGNORES)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
social + OFF_IGNORE_LIST + index * IGNORE_ENTRY_STRIDE);
|
||||
}
|
||||
|
||||
// True if `guid` is on the ignore list. Walks the GUID array, stopping at the
|
||||
// first zero entry (the list is contiguous — same terminate-on-zero contract
|
||||
// GetNumIgnores relies on).
|
||||
inline bool IsGuidIgnored(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return false;
|
||||
for (int i = 0; i < MAX_IGNORES; ++i) {
|
||||
const uint64_t entry = IgnoreGuidByIndex(i);
|
||||
if (entry == 0)
|
||||
break;
|
||||
if (entry == guid)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace FriendList
|
||||
@@ -0,0 +1,166 @@
|
||||
// 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_FriendList.GetFriendInfo(name)` / `GetFriendInfoByIndex(index)` — the
|
||||
// modern FriendInfo table, plus `C_FriendList.GetNumFriends()`.
|
||||
//
|
||||
// Reads the friend entries directly (see `FriendList.h`). The class and zone
|
||||
// names come from the same DBC lookups the engine's own GetFriendInfo uses:
|
||||
// ChrClasses.dbc by the entry's class index, and AreaTable.dbc by the area
|
||||
// index — resolved to the parent zone so a sub-area reports its zone. Both
|
||||
// are locale-applied through `DBC::LocalizedField`.
|
||||
//
|
||||
// vanilla 1.12 stores no friend notes, so `notes` is omitted (nil). `mobile`
|
||||
// and `referAFriend` are always false, and `rafLinkType` is 0 — vanilla has
|
||||
// none of those systems; the fields exist so modern consumers do not nil-out.
|
||||
|
||||
#include "FriendList.h"
|
||||
#include "Notes.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "dbc/Names.h"
|
||||
#include "guid/Guid.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace FriendList::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
// Push the modern FriendInfo table for `entry` on top of the Lua stack.
|
||||
void PushFriendInfo(void *L, const uint8_t *entry) {
|
||||
const bool connected = *(entry + OFF_FRIEND_CONNECTED) != 0;
|
||||
const char *name =
|
||||
*reinterpret_cast<const char *const *>(entry + OFF_FRIEND_NAME);
|
||||
const int level = *reinterpret_cast<const int *>(entry + OFF_FRIEND_LEVEL);
|
||||
const uint64_t guid =
|
||||
*reinterpret_cast<const uint64_t *>(entry + OFF_FRIEND_GUID);
|
||||
const uint8_t status = *(entry + OFF_FRIEND_STATUS);
|
||||
const uint32_t classIndex =
|
||||
*reinterpret_cast<const uint32_t *>(entry + OFF_FRIEND_CLASS_INDEX);
|
||||
const uint32_t areaIndex =
|
||||
*reinterpret_cast<const uint32_t *>(entry + OFF_FRIEND_AREA_INDEX);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "name", name);
|
||||
Game::Lua::SetFieldBool(L, "connected", connected);
|
||||
Game::Lua::SetFieldNumber(L, "level", static_cast<double>(level));
|
||||
// Optional strings: leave the field nil (unset) when unresolved rather
|
||||
// than "" — SetFieldString coerces null to "", which is truthy in Lua and
|
||||
// would fool an `if info.notes then` presence check. Matches retail, where
|
||||
// these are nil when absent.
|
||||
if (const char *className = DBC::ClassName(classIndex))
|
||||
Game::Lua::SetFieldString(L, "className", className);
|
||||
// classFilename: the locale-independent class token ("MAGE", "WARRIOR"),
|
||||
// the key for RAID_CLASS_COLORS and other class tables. nil when unknown,
|
||||
// so `className` and `classFilename` appear together.
|
||||
if (const char *classToken = DBC::ClassToken(classIndex))
|
||||
Game::Lua::SetFieldString(L, "classFilename", classToken);
|
||||
// Resolve a sub-area to its parent zone for friends (e.g. "Goldshire" ->
|
||||
// "Elwynn Forest"), matching what the friends UI shows.
|
||||
if (const char *area = DBC::AreaName(areaIndex, /*resolveToParent=*/true))
|
||||
Game::Lua::SetFieldString(L, "area", area);
|
||||
if (guid != 0) {
|
||||
char buf[Guid::STRING_SIZE];
|
||||
Game::Lua::SetFieldString(L, "guid",
|
||||
Guid::FormatAsString(guid, buf, sizeof buf));
|
||||
}
|
||||
if (const char *note = Notes::Get(guid)) // client-side note (see Notes.h)
|
||||
Game::Lua::SetFieldString(L, "notes", note);
|
||||
Game::Lua::SetFieldBool(L, "afk", (status & FRIEND_STATUS_AFK) != 0);
|
||||
Game::Lua::SetFieldBool(L, "dnd", (status & FRIEND_STATUS_DND) != 0);
|
||||
Game::Lua::SetFieldBool(L, "mobile", false);
|
||||
Game::Lua::SetFieldBool(L, "referAFriend", false);
|
||||
Game::Lua::SetFieldNumber(L, "rafLinkType", 0);
|
||||
}
|
||||
|
||||
int __fastcall Script_GetNumFriends(void *L) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(FriendList::Count()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_FriendList.GetNumOnlineFriends()` — how many friends are online. Vanilla
|
||||
// stores no online count, so we count entries whose `connected` byte is set
|
||||
// (the same flag GetFriendInfo reports as `connected`).
|
||||
int __fastcall Script_GetNumOnlineFriends(void *L) {
|
||||
int online = 0;
|
||||
const int count = FriendList::Count();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint8_t *entry = FriendList::EntryByIndex(i);
|
||||
if (entry == nullptr)
|
||||
break;
|
||||
if (*(entry + OFF_FRIEND_CONNECTED) != 0)
|
||||
++online;
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(online));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetFriendInfoByIndex(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const int index = static_cast<int>(Game::Lua::ToNumber(L, 1)); // 1-based
|
||||
if (index < 1 || index > FriendList::Count()) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const uint8_t *entry = FriendList::EntryByIndex(index - 1);
|
||||
if (entry == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
PushFriendInfo(L, entry);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetFriendInfo(void *L) {
|
||||
const char *name = Game::Lua::IsString(L, 1) ? Game::Lua::ToString(L, 1) : nullptr;
|
||||
if (name == nullptr || *name == '\0') {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const int count = FriendList::Count();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint8_t *entry = FriendList::EntryByIndex(i);
|
||||
if (entry == nullptr)
|
||||
break;
|
||||
const char *fname =
|
||||
*reinterpret_cast<const char *const *>(entry + OFF_FRIEND_NAME);
|
||||
if (fname != nullptr && _stricmp(fname, name) == 0) {
|
||||
PushFriendInfo(L, entry);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_FriendList", "GetNumFriends",
|
||||
&Script_GetNumFriends);
|
||||
Game::Lua::RegisterTableFunction("C_FriendList", "GetNumOnlineFriends",
|
||||
&Script_GetNumOnlineFriends);
|
||||
Game::Lua::RegisterTableFunction("C_FriendList", "GetFriendInfoByIndex",
|
||||
&Script_GetFriendInfoByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_FriendList", "GetFriendInfo",
|
||||
&Script_GetFriendInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace FriendList::Info
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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_FriendList.IsFriend(guid)` — is the given player on the friends list?
|
||||
//
|
||||
// Matches the input GUID against each friend entry's GUID (+0x08) — the same
|
||||
// GUID `SMSG_FRIEND_LIST` populates for online AND offline friends — and also
|
||||
// falls back to a name compare (resolving the GUID through the NameCache) so
|
||||
// the answer holds even if a build stores the GUID differently. As a
|
||||
// ClassicAPI convenience the arg may also be a bare character name, since
|
||||
// vanilla's friends list is fundamentally name-keyed (AddFriend(name)). See
|
||||
// `FriendList.h` for the entry layout.
|
||||
|
||||
#include "FriendList.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace FriendList::IsFriend {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsFriendByGuidOrName(uint64_t guid, const char *name) {
|
||||
const int count = FriendList::Count();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint8_t *entry = FriendList::EntryByIndex(i);
|
||||
if (entry == nullptr)
|
||||
break;
|
||||
if (guid != 0 &&
|
||||
*reinterpret_cast<const uint64_t *>(entry + FriendList::OFF_FRIEND_GUID) == guid)
|
||||
return true;
|
||||
if (name != nullptr) {
|
||||
const char *fname = *reinterpret_cast<const char *const *>(
|
||||
entry + FriendList::OFF_FRIEND_NAME);
|
||||
if (fname != nullptr && _stricmp(fname, name) == 0)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int __fastcall Script_IsFriend(void *L) {
|
||||
const char *arg = Game::Lua::IsString(L, 1) ? Game::Lua::ToString(L, 1) : nullptr;
|
||||
if (arg == nullptr || *arg == '\0') {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint64_t guid = 0;
|
||||
const char *name = nullptr;
|
||||
if (Guid::Parse(arg, &guid) && guid != 0)
|
||||
name = Unit::Identity::NameForGuid(guid); // GUID input → name for the fallback compare
|
||||
else
|
||||
name = arg; // not a GUID → treat the string as a character name
|
||||
|
||||
Game::Lua::PushBool(L, IsFriendByGuidOrName(guid, name));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_FriendList", "IsFriend", &Script_IsFriend);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace FriendList::IsFriend
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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_FriendList.IsIgnored(token)` / `C_FriendList.IsIgnoredByGuid(guid)` — is
|
||||
// the given player on the ignore list?
|
||||
//
|
||||
// The ignore list is a flat array of u64 GUIDs in the social singleton (see
|
||||
// `FriendList.h`) — vanilla ignores are GUID-keyed, not name-keyed. So
|
||||
// `IsIgnoredByGuid` is a direct GUID walk, and `IsIgnored` accepts either a
|
||||
// GUID string or a character name: for a name it resolves each ignored GUID to
|
||||
// its cached name through the NameCache and compares (the same GUID→name path
|
||||
// GetIgnoreName uses), so it matches any ignored player the client has synced.
|
||||
// An ignored player never seen this session has no cached name and so cannot
|
||||
// match by name — identical to GetIgnoreName returning "Unknown" for them.
|
||||
|
||||
#include "FriendList.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace FriendList::IsIgnored {
|
||||
|
||||
namespace {
|
||||
|
||||
// True if any ignored GUID matches `guid`, or (when `name` is given) resolves
|
||||
// to `name`. Single walk of the GUID array.
|
||||
bool IgnoredByGuidOrName(uint64_t guid, const char *name) {
|
||||
for (int i = 0; i < FriendList::MAX_IGNORES; ++i) {
|
||||
const uint64_t entry = FriendList::IgnoreGuidByIndex(i);
|
||||
if (entry == 0)
|
||||
break; // contiguous list — first zero is the end
|
||||
if (guid != 0 && entry == guid)
|
||||
return true;
|
||||
if (name != nullptr) {
|
||||
const char *ename = Unit::Identity::NameForGuid(entry);
|
||||
if (ename != nullptr && _stricmp(ename, name) == 0)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// `IsIgnored(token)` — token is a character name or a GUID string.
|
||||
int __fastcall Script_IsIgnored(void *L) {
|
||||
const char *arg = Game::Lua::IsString(L, 1) ? Game::Lua::ToString(L, 1) : nullptr;
|
||||
if (arg == nullptr || *arg == '\0') {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint64_t guid = 0;
|
||||
const char *name = nullptr;
|
||||
if (Guid::Parse(arg, &guid) && guid != 0)
|
||||
name = Unit::Identity::NameForGuid(guid); // GUID input → name for the fallback compare
|
||||
else
|
||||
name = arg; // not a GUID → treat the string as a character name
|
||||
|
||||
Game::Lua::PushBool(L, IgnoredByGuidOrName(guid, name));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `IsIgnoredByGuid(guid)` — GUID string only.
|
||||
int __fastcall Script_IsIgnoredByGuid(void *L) {
|
||||
const char *arg = Game::Lua::IsString(L, 1) ? Game::Lua::ToString(L, 1) : nullptr;
|
||||
uint64_t guid = 0;
|
||||
if (arg == nullptr || !Guid::Parse(arg, &guid) || guid == 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBool(L, FriendList::IsGuidIgnored(guid));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_FriendList", "IsIgnored", &Script_IsIgnored);
|
||||
Game::Lua::RegisterTableFunction("C_FriendList", "IsIgnoredByGuid",
|
||||
&Script_IsIgnoredByGuid);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace FriendList::IsIgnored
|
||||
@@ -0,0 +1,206 @@
|
||||
// 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/>.
|
||||
|
||||
// See `Notes.h`. Mirrors the equipment manager's storage approach
|
||||
// (equipmentset/Storage.cpp): a small text file under WTF, atomic
|
||||
// tmp+rename write, path re-resolved on every access so a
|
||||
// logout→character-select→login (no DLL reload) picks up the new realm.
|
||||
|
||||
#include "Notes.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace FriendList::Notes {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Entry {
|
||||
std::string name; // last-seen name, for a readable file (not the key)
|
||||
std::string note;
|
||||
};
|
||||
|
||||
std::unordered_map<uint64_t, Entry> g_notes;
|
||||
std::string g_path;
|
||||
bool g_loaded = false;
|
||||
|
||||
const char *ReadAccountName() {
|
||||
return *reinterpret_cast<const char *const *>(Offsets::VAR_ACCOUNT_NAME_PTR);
|
||||
}
|
||||
|
||||
const char *ReadRealmName() {
|
||||
auto *info = *reinterpret_cast<uint8_t **>(Offsets::VAR_REALM_INFO_PTR);
|
||||
if (info == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const char *const *>(info + Offsets::OFF_REALM_INFO_NAME);
|
||||
}
|
||||
|
||||
const char *ReadCharacterName() {
|
||||
auto *p = reinterpret_cast<const char *>(Offsets::VAR_CHARACTER_NAME);
|
||||
return (p[0] == '\0') ? nullptr : p;
|
||||
}
|
||||
|
||||
// WTF\Account\<acct>\<realm>\<char>\ClassicAPI_FriendNotes.txt, or empty until
|
||||
// the account + realm + character session globals are populated. Per-character
|
||||
// because the friends list is per-character in vanilla.
|
||||
std::string ResolveFilePath() {
|
||||
const char *account = ReadAccountName();
|
||||
if (account == nullptr || account[0] == '\0')
|
||||
return {};
|
||||
const char *realm = ReadRealmName();
|
||||
if (realm == nullptr || realm[0] == '\0')
|
||||
return {};
|
||||
const char *character = ReadCharacterName();
|
||||
if (character == nullptr)
|
||||
return {};
|
||||
std::string out = "WTF\\Account\\";
|
||||
out += account;
|
||||
out += '\\';
|
||||
out += realm;
|
||||
out += '\\';
|
||||
out += character;
|
||||
out += "\\ClassicAPI_FriendNotes.txt";
|
||||
return out;
|
||||
}
|
||||
|
||||
// Notes are a single line each; the file is tab-delimited, so a note must
|
||||
// carry no tab or newline. Replace any with a space and trim trailing space.
|
||||
void Sanitize(std::string *s) {
|
||||
for (char &c : *s)
|
||||
if (c == '\r' || c == '\n' || c == '\t')
|
||||
c = ' ';
|
||||
while (!s->empty() && s->back() == ' ')
|
||||
s->pop_back();
|
||||
}
|
||||
|
||||
void Load() {
|
||||
g_notes.clear();
|
||||
std::ifstream file(g_path);
|
||||
if (!file.is_open())
|
||||
return; // first run for this character — empty store is fine
|
||||
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == '\n'))
|
||||
line.pop_back();
|
||||
if (line.empty() || line[0] == '#')
|
||||
continue;
|
||||
// Format: `0xGUID<tab>name<tab>note`.
|
||||
const auto t1 = line.find('\t');
|
||||
if (t1 == std::string::npos)
|
||||
continue;
|
||||
const auto t2 = line.find('\t', t1 + 1);
|
||||
const uint64_t guid = std::strtoull(line.c_str(), nullptr, 0);
|
||||
if (guid == 0 || t2 == std::string::npos)
|
||||
continue;
|
||||
std::string name = line.substr(t1 + 1, t2 - (t1 + 1));
|
||||
std::string note = line.substr(t2 + 1);
|
||||
if (note.empty())
|
||||
continue;
|
||||
if (static_cast<int>(note.size()) > MAX_NOTE_LEN)
|
||||
note.resize(MAX_NOTE_LEN);
|
||||
g_notes[guid] = Entry{std::move(name), std::move(note)};
|
||||
}
|
||||
}
|
||||
|
||||
void Save() {
|
||||
if (g_path.empty())
|
||||
return;
|
||||
const std::string tmp = g_path + ".tmp";
|
||||
{
|
||||
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
|
||||
if (!out.is_open())
|
||||
return;
|
||||
out << "# ClassicAPI Friend Notes v1\n";
|
||||
out << "# <0xGUID>\\t<name>\\t<note> (per-character, GUID-keyed)\n";
|
||||
for (const auto &kv : g_notes) {
|
||||
char buf[24];
|
||||
std::snprintf(buf, sizeof(buf), "0x%016llX",
|
||||
static_cast<unsigned long long>(kv.first));
|
||||
out << buf << '\t' << kv.second.name << '\t' << kv.second.note << '\n';
|
||||
}
|
||||
}
|
||||
std::remove(g_path.c_str());
|
||||
std::rename(tmp.c_str(), g_path.c_str());
|
||||
}
|
||||
|
||||
// Re-resolve the path each call; reload when it changes. The DLL survives a
|
||||
// character/realm switch, so a permanent latch would keep the old character's
|
||||
// file — matching EquipmentSet::Data::EnsureLoaded.
|
||||
void EnsureLoaded() {
|
||||
std::string path = ResolveFilePath();
|
||||
if (path.empty()) {
|
||||
// Session globals not ready (login / char-select / loading screen).
|
||||
// Drop any cached state so a stale path from the PREVIOUS character
|
||||
// can never be read from or written to — that is the "didn't wipe
|
||||
// between characters" trap. Nothing queries notes out of world, so
|
||||
// there is nothing to lose; the next in-world call reloads cleanly.
|
||||
g_notes.clear();
|
||||
g_path.clear();
|
||||
g_loaded = false;
|
||||
return;
|
||||
}
|
||||
if (g_loaded && path == g_path)
|
||||
return;
|
||||
// Path changed (different character/account) — Load() clears the map
|
||||
// before reading, so the new character never inherits the old one's notes.
|
||||
g_path = std::move(path);
|
||||
Load();
|
||||
g_loaded = true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char *Get(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
EnsureLoaded();
|
||||
auto it = g_notes.find(guid);
|
||||
if (it == g_notes.end() || it->second.note.empty())
|
||||
return nullptr;
|
||||
return it->second.note.c_str();
|
||||
}
|
||||
|
||||
bool Set(uint64_t guid, const char *name, const char *note) {
|
||||
if (guid == 0)
|
||||
return false;
|
||||
EnsureLoaded();
|
||||
if (g_path.empty())
|
||||
return false; // no realm/account yet — nowhere to persist
|
||||
|
||||
std::string text = (note != nullptr) ? note : "";
|
||||
Sanitize(&text);
|
||||
if (static_cast<int>(text.size()) > MAX_NOTE_LEN)
|
||||
text.resize(MAX_NOTE_LEN);
|
||||
|
||||
if (text.empty()) {
|
||||
if (g_notes.erase(guid) == 0)
|
||||
return false; // nothing to clear
|
||||
} else {
|
||||
Entry e;
|
||||
e.name = (name != nullptr) ? name : "";
|
||||
Sanitize(&e.name);
|
||||
e.note = std::move(text);
|
||||
g_notes[guid] = std::move(e);
|
||||
}
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace FriendList::Notes
|
||||
@@ -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>
|
||||
|
||||
// Client-side friend notes. Vanilla 1.12 has no server-side note field
|
||||
// (TBC added it), so we persist notes ourselves — the same pattern the
|
||||
// equipment manager uses for its sets. Backing file:
|
||||
// WTF\Account\<acct>\<realm>\<char>\ClassicAPI_FriendNotes.txt
|
||||
// Per-character, because the friends list itself is per-character in
|
||||
// vanilla (each character keeps its own friends). GUID-keyed within that
|
||||
// file — a friend's GUID is stable and unique on the realm.
|
||||
|
||||
namespace FriendList::Notes {
|
||||
|
||||
// Longest stored note. Longer input is truncated on Set.
|
||||
constexpr int MAX_NOTE_LEN = 128;
|
||||
|
||||
// The note for a friend GUID, or nullptr if none. Loads the per-realm store
|
||||
// on first use. The pointer is valid until the next Set or realm switch.
|
||||
const char *Get(uint64_t guid);
|
||||
|
||||
// Set the note for a friend GUID, or clear it when `note` is null/empty.
|
||||
// `name` is stored alongside so the file stays human-readable. Persists to
|
||||
// disk immediately. Returns true if the store changed.
|
||||
bool Set(uint64_t guid, const char *name, const char *note);
|
||||
|
||||
} // namespace FriendList::Notes
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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_FriendList.SetFriendNotes(name, notes)` / `SetFriendNotesByIndex(index,
|
||||
// notes)` — write a client-side note for a friend (see `Notes.h`). Passing an
|
||||
// empty / nil note clears it. Matches retail: the target must already be on
|
||||
// the friends list, and there is no return value. Fires FRIENDLIST_UPDATE on
|
||||
// change so the friends UI and note-aware addons refresh.
|
||||
|
||||
#include "FriendList.h"
|
||||
#include "Notes.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace FriendList::SetNotes {
|
||||
|
||||
namespace {
|
||||
|
||||
// The friend entry whose name matches (case-insensitive), or null.
|
||||
const uint8_t *FindByName(const char *name) {
|
||||
if (name == nullptr || *name == '\0')
|
||||
return nullptr;
|
||||
const int count = FriendList::Count();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint8_t *entry = FriendList::EntryByIndex(i);
|
||||
if (entry == nullptr)
|
||||
break;
|
||||
const char *fname = *reinterpret_cast<const char *const *>(
|
||||
entry + FriendList::OFF_FRIEND_NAME);
|
||||
if (fname != nullptr && _stricmp(fname, name) == 0)
|
||||
return entry;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ApplyNote(const uint8_t *entry, const char *note) {
|
||||
if (entry == nullptr)
|
||||
return;
|
||||
const uint64_t guid = *reinterpret_cast<const uint64_t *>(
|
||||
entry + FriendList::OFF_FRIEND_GUID);
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
entry + FriendList::OFF_FRIEND_NAME);
|
||||
if (Notes::Set(guid, name, note))
|
||||
Event::Custom::Fire(Event::Custom::LookupByName("FRIENDLIST_UPDATE"), "");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetFriendNotes(void *L) {
|
||||
const char *name = Game::Lua::IsString(L, 1) ? Game::Lua::ToString(L, 1) : nullptr;
|
||||
const char *note = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
|
||||
ApplyNote(FindByName(name), note);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetFriendNotesByIndex(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0;
|
||||
const int index = static_cast<int>(Game::Lua::ToNumber(L, 1)); // 1-based
|
||||
const char *note = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
|
||||
if (index >= 1 && index <= FriendList::Count())
|
||||
ApplyNote(FriendList::EntryByIndex(index - 1), note);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_FriendList", "SetFriendNotes",
|
||||
&Script_SetFriendNotes);
|
||||
Game::Lua::RegisterTableFunction("C_FriendList", "SetFriendNotesByIndex",
|
||||
&Script_SetFriendNotesByIndex);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace FriendList::SetNotes
|
||||
@@ -121,7 +121,7 @@ using SendQuery_t = void(__thiscall *)(void *self, const char *queryStr);
|
||||
|
||||
bool SendQueryInternal(const char *fullQuery) {
|
||||
auto *system = *reinterpret_cast<void *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_WHO_SYSTEM));
|
||||
static_cast<uintptr_t>(Offsets::VAR_SOCIAL_SYSTEM));
|
||||
if (system == nullptr)
|
||||
return false;
|
||||
auto fn = reinterpret_cast<SendQuery_t>(Offsets::FUN_WHO_SYSTEM_SEND_QUERY);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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_FriendList.GetWhoInfo(index)` — the modern table form of vanilla's
|
||||
// `GetWhoInfo(index)`. The vanilla global returns positional values and, most
|
||||
// notably, no class *token* — only the localized class name. This returns a
|
||||
// table that includes `filename` (the "MAGE"/"WARRIOR" token), so a /who list
|
||||
// can be class-colored via RAID_CLASS_COLORS without reverse-mapping the
|
||||
// localized name.
|
||||
//
|
||||
// Reads the same /who result buffer the engine's Script_GetWhoInfo
|
||||
// (0x005AD6E0) reads (see the WHO_RESULT_* offsets in Offsets.h): an array of
|
||||
// stride-0xA0 entries with inline name/guild strings and race/class/zone DBC
|
||||
// indices. `C_FriendList.GetNumWhoResults()` is the companion count.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Names.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace FriendList::WhoInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
int WhoResultCount() {
|
||||
return *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_WHO_RESULT_COUNT));
|
||||
}
|
||||
|
||||
// `C_FriendList.GetNumWhoResults()` → `numWhos, totalCount`. The first is the
|
||||
// number of entries you can index with GetWhoInfo; the second is the server's
|
||||
// total-match count (it can exceed the displayed list). Mirrors the vanilla
|
||||
// global's two returns.
|
||||
int __fastcall Script_GetNumWhoResults(void *L) {
|
||||
const int displayed = WhoResultCount();
|
||||
const int total = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_WHO_RESULT_COUNT) + 4);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(displayed));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(total));
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `C_FriendList.GetWhoInfo(index)` → WhoInfo table, or nil for a bad index.
|
||||
int __fastcall Script_GetWhoInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const int index = static_cast<int>(Game::Lua::ToNumber(L, 1)); // 1-based
|
||||
if (index < 1 || index > WhoResultCount()) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint8_t *entry = reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_WHO_RESULTS) +
|
||||
static_cast<size_t>(index - 1) * Offsets::WHO_RESULT_STRIDE);
|
||||
|
||||
const char *name = reinterpret_cast<const char *>(entry + Offsets::OFF_WHO_NAME);
|
||||
const char *guild = reinterpret_cast<const char *>(entry + Offsets::OFF_WHO_GUILD);
|
||||
const int level = *reinterpret_cast<const int *>(entry + Offsets::OFF_WHO_LEVEL);
|
||||
const uint32_t raceID =
|
||||
*reinterpret_cast<const uint32_t *>(entry + Offsets::OFF_WHO_RACE);
|
||||
const uint32_t classID =
|
||||
*reinterpret_cast<const uint32_t *>(entry + Offsets::OFF_WHO_CLASS);
|
||||
const uint32_t zoneID =
|
||||
*reinterpret_cast<const uint32_t *>(entry + Offsets::OFF_WHO_ZONE);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "fullName", name); // inline; "" if empty
|
||||
Game::Lua::SetFieldString(L, "fullGuildName", guild); // "" when guildless
|
||||
Game::Lua::SetFieldNumber(L, "level", static_cast<double>(level));
|
||||
// Optional strings stay nil (unset) when the DBC id doesn't resolve, so an
|
||||
// `if info.filename then` check works — SetFieldString would coerce null
|
||||
// to "" (truthy).
|
||||
if (const char *raceStr = DBC::RaceName(raceID))
|
||||
Game::Lua::SetFieldString(L, "raceStr", raceStr);
|
||||
if (const char *classStr = DBC::ClassName(classID))
|
||||
Game::Lua::SetFieldString(L, "classStr", classStr);
|
||||
if (const char *filename = DBC::ClassToken(classID))
|
||||
Game::Lua::SetFieldString(L, "filename", filename);
|
||||
// /who zones are already zone-level, so use the area's own name (no parent
|
||||
// walk) — matches the engine's own GetWhoInfo.
|
||||
if (const char *area = DBC::AreaName(zoneID, /*resolveToParent=*/false))
|
||||
Game::Lua::SetFieldString(L, "area", area);
|
||||
// No `gender`: vanilla's /who result carries no sex field (the entry ends
|
||||
// at the zone index), so modern's gender return has no source here.
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_FriendList", "GetWhoInfo",
|
||||
&Script_GetWhoInfo);
|
||||
Game::Lua::RegisterTableFunction("C_FriendList", "GetNumWhoResults",
|
||||
&Script_GetNumWhoResults);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace FriendList::WhoInfo
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "object/Resolve.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -28,9 +29,6 @@ namespace GameObject::Tooltip {
|
||||
// on every subsequent `SetX` call — same gating pattern Has/GetUnitGUID,
|
||||
// Has/GetItem, and Has/GetSpell use.
|
||||
|
||||
using ObjectResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
int priority);
|
||||
using GameObjectGetName_t = const char *(__fastcall *)(void *gameObject);
|
||||
|
||||
// `GameTooltip:GetGameObject()` → (name, id, guid) for whichever gameobject
|
||||
@@ -72,10 +70,9 @@ static int __fastcall Script_GameTooltipGetGameObject(void *L) {
|
||||
if (guidLo == 0 && guidHi == 0)
|
||||
return 0;
|
||||
|
||||
auto resolve = reinterpret_cast<ObjectResolveByGUID_t>(
|
||||
Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
|
||||
void *obj = resolve(Offsets::OBJ_TYPE_GAMEOBJECT, "GameTooltip:GetGameObject",
|
||||
guidLo, guidHi, 0x172);
|
||||
void *obj = Object::ByGuid(Offsets::TYPEMASK_GAMEOBJECT,
|
||||
(static_cast<uint64_t>(guidHi) << 32) | guidLo,
|
||||
"GameTooltip:GetGameObject", 0x172);
|
||||
if (obj == nullptr)
|
||||
return 0;
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -65,10 +67,6 @@ namespace Item::AverageLevel {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
// 1-based slot range used by both walks.
|
||||
constexpr int kFirstSlot = 1;
|
||||
constexpr int kLastSlot = 19;
|
||||
@@ -119,13 +117,6 @@ constexpr uint32_t kSlotMaskByInvType[29] = {
|
||||
constexpr int kInvTypeMax = static_cast<int>(sizeof(kSlotMaskByInvType) /
|
||||
sizeof(kSlotMaskByInvType[0])) - 1;
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
// Returns (itemLevel, inventoryType) for the CGItem, or (0, 0) if
|
||||
// the itemID is unresolvable or the cache record hasn't loaded.
|
||||
struct ItemInfo {
|
||||
@@ -139,7 +130,7 @@ ItemInfo ReadItemInfo(const uint8_t *cgItem) {
|
||||
const int itemID = Item::ID::FromCGItem(cgItem);
|
||||
if (itemID <= 0)
|
||||
return {0, 0};
|
||||
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
// Equipped or bagged item with no cache record yet — queue a
|
||||
// warmup so the next call gets populated data. Cheap on cache
|
||||
@@ -154,22 +145,11 @@ ItemInfo ReadItemInfo(const uint8_t *cgItem) {
|
||||
// Bank-walk helpers — bypass the bank-window gate on `GetItemBySlot`
|
||||
// by reading the player's invMgr GUID array directly. Same shape as
|
||||
// `C_Item.GetItemCount`'s bank path.
|
||||
using ResolveObjectByGuid_t = void *(__fastcall *)(int type,
|
||||
const char *debugName,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int priority);
|
||||
|
||||
const uint8_t *ResolveByGuid(int type, uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveObjectByGuid_t>(
|
||||
Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
|
||||
return static_cast<const uint8_t *>(
|
||||
fn(type, "AverageLevel",
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32),
|
||||
0x172));
|
||||
Object::ByGuid(type, guid, "AverageLevel", 0x172));
|
||||
}
|
||||
|
||||
// Collects a single CGItem into `candidates` if it could equip in
|
||||
@@ -209,7 +189,7 @@ void WalkGuidArrayRange(const uint8_t *invMgr, int firstSlot, int lastSlot,
|
||||
if (guidArray == nullptr)
|
||||
return;
|
||||
for (int slot = firstSlot; slot <= lastSlot; ++slot) {
|
||||
const uint8_t *item = ResolveByGuid(Offsets::OBJ_TYPE_ITEM, guidArray[slot]);
|
||||
const uint8_t *item = ResolveByGuid(Offsets::TYPEMASK_ITEM, guidArray[slot]);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
CollectBagOrBankItem(item, candidates);
|
||||
@@ -229,7 +209,7 @@ void WalkBankBags(std::vector<Candidate> &candidates) {
|
||||
return;
|
||||
for (int slot = Offsets::INVMGR_BANK_BAG_FIRST_SLOT;
|
||||
slot <= Offsets::INVMGR_BANK_BAG_LAST_SLOT; ++slot) {
|
||||
const uint8_t *bag = ResolveByGuid(Offsets::OBJ_TYPE_CONTAINER,
|
||||
const uint8_t *bag = ResolveByGuid(Offsets::TYPEMASK_CONTAINER,
|
||||
playerGuidArray[slot]);
|
||||
if (bag == nullptr)
|
||||
continue;
|
||||
|
||||
+2
-2
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -20,8 +21,7 @@
|
||||
namespace Item::Bound {
|
||||
|
||||
static bool ItemIsSoulbound(const uint8_t *item) {
|
||||
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
auto *descriptor = Item::ObjectFields(item);
|
||||
if (descriptor == nullptr)
|
||||
return false;
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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 "CGItem.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Item {
|
||||
|
||||
const uint8_t *InstanceBlock(const uint8_t *cgItem) {
|
||||
if (cgItem == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const uint8_t *const *>(
|
||||
cgItem + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
}
|
||||
|
||||
const uint8_t *ObjectFields(const uint8_t *cgItem) {
|
||||
if (cgItem == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const uint8_t *const *>(
|
||||
cgItem + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
}
|
||||
|
||||
} // namespace Item
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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>
|
||||
|
||||
// Accessors for a `CGItem`'s two descriptor-like pointers (see the "Item
|
||||
// lookups" notes in CLAUDE.md). Every item module used to spell out the same
|
||||
// `*reinterpret_cast<const uint8_t *const *>(cgItem + OFF_...)` deref; these
|
||||
// name it once.
|
||||
|
||||
namespace Item {
|
||||
|
||||
// The item's **instance block** (`CGItem + 0x08` dereferenced): a `uint64`
|
||||
// item GUID at `+0x00` and the `uint32` itemID at `+0x0C`. This is the block
|
||||
// the canonical inventory→cache path reads the itemID from. Returns nullptr
|
||||
// for a null `cgItem`; the stored pointer itself may still be null (callers
|
||||
// null-check the result before reading fields).
|
||||
const uint8_t *InstanceBlock(const uint8_t *cgItem);
|
||||
|
||||
// The item's **m_objectFields descriptor** (`CGItem + 0x114` dereferenced):
|
||||
// the UpdateField block holding stack count (`+0x20`), flags (`+0x3C`),
|
||||
// spell charges (`+0x28`), durability (`+0xA0`), enchantments, etc. Distinct
|
||||
// from the instance block. Returns nullptr for a null `cgItem`.
|
||||
const uint8_t *ObjectFields(const uint8_t *cgItem);
|
||||
|
||||
} // namespace Item
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "CGItem.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
@@ -23,8 +24,7 @@ namespace Item::Charges {
|
||||
int PushChargesForItem(void *L, const uint8_t *item) {
|
||||
if (item == nullptr)
|
||||
return 0;
|
||||
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
auto *descriptor = Item::ObjectFields(item);
|
||||
if (descriptor == nullptr)
|
||||
return 0;
|
||||
const int32_t stack = static_cast<int32_t>(*reinterpret_cast<const uint32_t *>(
|
||||
|
||||
+2
-12
@@ -34,6 +34,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -41,17 +42,6 @@ namespace Item::Consumable {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
constexpr uint32_t kItemClassConsumable = 0;
|
||||
constexpr uint32_t kInvTypeAmmo = 24; // INVTYPE_AMMO
|
||||
constexpr uint32_t kInvTypeThrown = 25; // INVTYPE_THROWN
|
||||
@@ -62,7 +52,7 @@ int __fastcall Script_C_Item_IsConsumableItem(void *L) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
|
||||
+7
-18
@@ -16,8 +16,10 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -32,8 +34,7 @@ namespace {
|
||||
int GetStackCount(const uint8_t *cgItem) {
|
||||
if (cgItem == nullptr)
|
||||
return 0;
|
||||
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
cgItem + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
auto *descriptor = Item::ObjectFields(cgItem);
|
||||
if (descriptor == nullptr)
|
||||
return 0;
|
||||
return static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
@@ -51,8 +52,7 @@ int GetStackCount(const uint8_t *cgItem) {
|
||||
int GetUsesPerItem(const uint8_t *cgItem) {
|
||||
if (cgItem == nullptr)
|
||||
return 1;
|
||||
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
cgItem + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
auto *descriptor = Item::ObjectFields(cgItem);
|
||||
if (descriptor == nullptr)
|
||||
return 1;
|
||||
const int32_t raw = *reinterpret_cast<const int32_t *>(
|
||||
@@ -97,22 +97,11 @@ int CountInBag(void *L, int bagID, int targetItemID, bool includeUses) {
|
||||
// object resolver `FUN_OBJECT_RESOLVE_BY_GUID` (the same function
|
||||
// `GetItemBySlot` would call internally if the gate let us through).
|
||||
|
||||
using ResolveObjectByGuid_t = void *(__fastcall *)(int type,
|
||||
const char *debugName,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int priority);
|
||||
|
||||
const uint8_t *ResolveByGuid(int type, uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveObjectByGuid_t>(
|
||||
Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
|
||||
return static_cast<const uint8_t *>(
|
||||
fn(type, "ItemMgr",
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32),
|
||||
0x172));
|
||||
Object::ByGuid(type, guid, "ItemMgr", 0x172));
|
||||
}
|
||||
|
||||
// Walks linear slots `[firstSlot, lastSlot]` of `invMgr`'s GUID array
|
||||
@@ -130,7 +119,7 @@ int CountInGuidArray(const uint8_t *invMgr, int firstSlot, int lastSlot,
|
||||
return 0;
|
||||
int total = 0;
|
||||
for (int slot = firstSlot; slot <= lastSlot; slot++) {
|
||||
const uint8_t *item = ResolveByGuid(Offsets::OBJ_TYPE_ITEM, guidArray[slot]);
|
||||
const uint8_t *item = ResolveByGuid(Offsets::TYPEMASK_ITEM, guidArray[slot]);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
if (Item::ID::FromCGItem(item) != targetItemID)
|
||||
@@ -159,7 +148,7 @@ int CountInBankBags(int targetItemID, bool includeUses) {
|
||||
int total = 0;
|
||||
for (int slot = Offsets::INVMGR_BANK_BAG_FIRST_SLOT;
|
||||
slot <= Offsets::INVMGR_BANK_BAG_LAST_SLOT; slot++) {
|
||||
const uint8_t *bag = ResolveByGuid(Offsets::OBJ_TYPE_CONTAINER,
|
||||
const uint8_t *bag = ResolveByGuid(Offsets::TYPEMASK_CONTAINER,
|
||||
playerGuidArray[slot]);
|
||||
if (bag == nullptr)
|
||||
continue;
|
||||
|
||||
+2
-2
@@ -16,6 +16,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cursor/Info.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -44,8 +45,7 @@ bool ItemLocked(const uint8_t *item) {
|
||||
// Reads a CGItem's 64-bit instance GUID into lo/hi. False if the instance
|
||||
// block pointer is absent.
|
||||
bool ReadItemGuid(const uint8_t *item, uint32_t *lo, uint32_t *hi) {
|
||||
auto *instance = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
auto *instance = Item::InstanceBlock(item);
|
||||
if (instance == nullptr)
|
||||
return false;
|
||||
*lo = *reinterpret_cast<const uint32_t *>(instance + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
|
||||
+2
-2
@@ -15,6 +15,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/Location.h"
|
||||
#include "tick/WorldTick.h"
|
||||
@@ -119,8 +120,7 @@ static int ResolveLocationToItemID(void *L, int idx) {
|
||||
const uint8_t *item = Item::Location::Resolve(L, idx);
|
||||
if (item == nullptr)
|
||||
return 0;
|
||||
auto *instance = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
auto *instance = Item::InstanceBlock(item);
|
||||
if (instance == nullptr)
|
||||
return 0;
|
||||
return static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -34,8 +35,7 @@ namespace Item::Durability {
|
||||
int PushDurabilityForItem(void *L, const uint8_t *item) {
|
||||
if (item == nullptr)
|
||||
return 0;
|
||||
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
auto *descriptor = Item::ObjectFields(item);
|
||||
if (descriptor == nullptr)
|
||||
return 0;
|
||||
const uint32_t cur = *reinterpret_cast<const uint32_t *>(
|
||||
|
||||
+3
-16
@@ -17,6 +17,7 @@
|
||||
#include "item/Cursor.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/Swap.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -25,20 +26,6 @@ namespace Item::Equipment {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
// Same item-cache peek pattern as `Item::Bag::PeekItemRecord` — we
|
||||
// don't share because each caller is one-line-different and the
|
||||
// helper isn't worth a header.
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
// `OffhandHasWeapon()` — true iff the player has a one-handed
|
||||
// weapon (or off-hand-only weapon) equipped in the off-hand slot.
|
||||
// Returns false for empty off-hand, shields, and held items
|
||||
@@ -64,7 +51,7 @@ int __fastcall Script_OffhandHasWeapon(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
auto *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
@@ -102,7 +89,7 @@ int __fastcall Script_C_Item_IsEquippableItem(void *L) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
auto *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
auto *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
|
||||
+2
-2
@@ -14,6 +14,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -27,8 +28,7 @@ namespace {
|
||||
uint64_t ReadItemGuid(const uint8_t *cgItem) {
|
||||
if (cgItem == nullptr)
|
||||
return 0;
|
||||
auto *instance = *reinterpret_cast<const uint8_t *const *>(
|
||||
cgItem + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
auto *instance = Item::InstanceBlock(cgItem);
|
||||
if (instance == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
|
||||
+2
-12
@@ -27,6 +27,7 @@
|
||||
#include "item/Arg.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/Spell.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -36,17 +37,6 @@ namespace Item::GetData {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, bool requestIfMissing);
|
||||
|
||||
const uint8_t *FetchItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, false);
|
||||
}
|
||||
|
||||
int CurrentLocaleIndex() {
|
||||
return *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
}
|
||||
@@ -377,7 +367,7 @@ int PushDataForItemID(void *L, int itemID) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const uint8_t *record = FetchItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
|
||||
+2
-2
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
@@ -23,8 +24,7 @@ namespace Item::ID {
|
||||
int FromCGItem(const uint8_t *item) {
|
||||
if (item == nullptr)
|
||||
return 0;
|
||||
auto *instance = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
auto *instance = Item::InstanceBlock(item);
|
||||
if (instance == nullptr)
|
||||
return 0;
|
||||
return static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
|
||||
+5
-15
@@ -20,6 +20,7 @@
|
||||
#include "item/ID.h"
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -27,21 +28,10 @@
|
||||
|
||||
namespace Item::Info {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, bool requestIfMissing);
|
||||
|
||||
static const char *PushedOrEmpty(const char *s) { return (s != nullptr && s[0] != 0) ? s : ""; }
|
||||
|
||||
static int CurrentLocaleIndex() { return *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX); }
|
||||
|
||||
static const uint8_t *FetchItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, false);
|
||||
}
|
||||
|
||||
static const char *LookupItemClassName(uint32_t classID) {
|
||||
return PushedOrEmpty(DBC::LocalizedField(
|
||||
Offsets::VAR_ITEMCLASS_RECORDS, Offsets::VAR_ITEMCLASS_COUNT,
|
||||
@@ -114,7 +104,7 @@ static int __fastcall Script_GetItemInfoInstant(void *L) {
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(itemID));
|
||||
|
||||
const uint8_t *record = FetchItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
for (int i = 0; i < 6; ++i)
|
||||
Game::Lua::PushNil(L);
|
||||
@@ -150,7 +140,7 @@ static int __fastcall Script_GetItemInfoInstant(void *L) {
|
||||
static int PushIconForItemID(void *L, int itemID) {
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const uint8_t *record = FetchItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
const uint32_t displayInfoID = *reinterpret_cast<const uint32_t *>(
|
||||
@@ -229,7 +219,7 @@ static int __fastcall Script_C_Item_GetItemFamily(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const uint8_t *record = FetchItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
@@ -264,7 +254,7 @@ static int __fastcall Script_C_Item_GetItemInfo(void *L) {
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *record = FetchItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0; // nil this call; GET_ITEM_INFO_RECEIVED fires when ready
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "unit/Flags.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -41,8 +42,7 @@ int ItemIDForLocalPlayer(void *playerPtr, int slot1Based) {
|
||||
auto *item = static_cast<uint8_t *>(getBySlot(invMgr, slot1Based - 1));
|
||||
if (item == nullptr)
|
||||
return 0;
|
||||
auto *instance = *reinterpret_cast<uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
auto *instance = Item::InstanceBlock(item);
|
||||
if (instance == nullptr)
|
||||
return 0;
|
||||
return static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
@@ -106,14 +106,17 @@ static int __fastcall Script_GetInventoryItemID(void *L) {
|
||||
int itemID = 0;
|
||||
if (unitPtr == playerPtr) {
|
||||
itemID = ItemIDForLocalPlayer(playerPtr, slot);
|
||||
} else if (Unit::Flags::IsPlayerControlled(
|
||||
} else if (Unit::Flags::IsPlayerObject(
|
||||
static_cast<const uint8_t *>(unitPtr))) {
|
||||
itemID = ItemIDForOtherUnit(unitPtr, slot);
|
||||
} else {
|
||||
// NPC / creature — no visible-items array. Returning nil here
|
||||
// also matches `GetInventoryItemLink`'s practical behavior on
|
||||
// NPCs (it crashes the client without this gate; addons that
|
||||
// don't pre-filter the unit don't typically reach this path).
|
||||
// Non-player object — no CGPlayer visible-items array at +0xE68.
|
||||
// Gate on the object being a player, not merely player-controlled:
|
||||
// a pet/totem/MC'd creature sets UNIT_FLAG_PLAYER_CONTROLLED but
|
||||
// has no sub-struct there, so IsPlayerControlled would let it
|
||||
// through and `ItemIDForOtherUnit` would deref garbage and crash
|
||||
// (same root cause as pfUI issue #34). Returning nil also matches
|
||||
// `GetInventoryItemLink`'s practical behavior on non-players.
|
||||
return 0;
|
||||
}
|
||||
if (itemID == 0)
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
#include "item/ID.h"
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
@@ -54,10 +55,6 @@ namespace Item::InventoryItemsForSlot {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
// invType → slot bitmask. Bit N (0-based) means "this item type fits
|
||||
// 1-based slot N+1". Values 0..28 cover every InventoryType
|
||||
// vanilla's Item.dbc emits. Indices outside this range read as 0
|
||||
@@ -146,18 +143,11 @@ bool PlayerCanEquip(const uint8_t *record) {
|
||||
return PlayerHasProficiency(classCode, subclass);
|
||||
}
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
const uint8_t *ItemRecordForCGItem(const uint8_t *cgItem) {
|
||||
const int itemID = Item::ID::FromCGItem(cgItem);
|
||||
if (itemID <= 0)
|
||||
return nullptr;
|
||||
return PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
return Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
}
|
||||
|
||||
uint32_t ReadInventoryType(const uint8_t *record) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -24,21 +25,10 @@ namespace Item::InventoryType {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
int PushInventoryTypeForItemID(void *L, int itemID) {
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
|
||||
+2
-12
@@ -17,6 +17,7 @@
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -24,21 +25,10 @@ namespace Item::Level {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
int PushItemLevelForItemID(void *L, int itemID) {
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
|
||||
+2
-15
@@ -17,6 +17,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/QualityColor.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -34,20 +35,6 @@ using BuildInstanceName_t = void(__thiscall *)(const void *cgItem, char *out,
|
||||
using BuildNameFromID_t = void(__fastcall *)(char *out, unsigned outSize,
|
||||
uint32_t itemID, int suffixID);
|
||||
|
||||
// Cached ItemStats record lookup. Same `_GetRecord` pattern the rest of
|
||||
// the codebase uses; passes a noop NULL callback so no SMSG fires for
|
||||
// uncached items — caller treats "not cached" as a clean failure.
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int dedupFlag);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char *FromCGItem(const uint8_t *cgItem) {
|
||||
@@ -80,7 +67,7 @@ bool NameFromIDSuffix(uint32_t itemID, int suffixID, char *out, size_t outSize)
|
||||
bool BasicFromIDSuffix(uint32_t itemID, int suffixID, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
const uint8_t *record = PeekItemRecord(itemID);
|
||||
const uint8_t *record = Item::PeekRecord(itemID);
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
char name[128];
|
||||
|
||||
+8
-25
@@ -16,10 +16,13 @@
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "../object/Resolve.h"
|
||||
#include "../unit/Identity.h"
|
||||
#include "Arg.h"
|
||||
#include "CGItem.h"
|
||||
#include "ID.h"
|
||||
#include "Link.h"
|
||||
#include "Record.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
@@ -30,9 +33,6 @@ namespace {
|
||||
using GetItemBySlot_t = void *(__thiscall *)(void *thisInvMgr, int slot);
|
||||
using PackBagSlot_t = int(__fastcall *)(void *L, void **outInvMgr, int *outLinearSlot,
|
||||
int *outUnused);
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
// Reads `loc.fieldName` and returns it as an int. Returns false via the
|
||||
// boolean result if the field is missing or non-numeric. Always leaves the
|
||||
@@ -56,16 +56,8 @@ const uint8_t *ResolveBagSlot(void *L, int bagID, int slotIndex) {
|
||||
|
||||
// --- GUID-walk helpers ---------------------------------------------------
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
uint64_t ReadCGItemGUID(const uint8_t *item) {
|
||||
auto *instance = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
auto *instance = Item::InstanceBlock(item);
|
||||
if (instance == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
@@ -105,12 +97,8 @@ void *EquippedBagInventory(int bagID) {
|
||||
const uint64_t bagGuid = getBagGuid(static_cast<uint32_t>(bagID - 1));
|
||||
if (bagGuid == 0)
|
||||
return nullptr; // no bag equipped in that slot
|
||||
using ResolveByGUID_t = void *(__fastcall *)(int, const char *, uint32_t,
|
||||
uint32_t, int);
|
||||
auto resolve = reinterpret_cast<ResolveByGUID_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
|
||||
auto *container = static_cast<const uint8_t *>(
|
||||
resolve(Offsets::OBJ_TYPE_CONTAINER, "ItemMgr", static_cast<uint32_t>(bagGuid),
|
||||
static_cast<uint32_t>(bagGuid >> 32), 0x172));
|
||||
Object::ByGuid(Offsets::TYPEMASK_CONTAINER, bagGuid, "ItemMgr", 0x172));
|
||||
return ContainerInventory(container);
|
||||
}
|
||||
|
||||
@@ -128,7 +116,7 @@ int GetBagSlotCount(int bagID) {
|
||||
const int bagItemID = Item::ID::FromCGItem(bagItem);
|
||||
if (bagItemID == 0)
|
||||
return 0;
|
||||
auto *record = PeekItemRecord(static_cast<uint32_t>(bagItemID));
|
||||
auto *record = Item::PeekRecord(static_cast<uint32_t>(bagItemID));
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
return static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
@@ -170,13 +158,8 @@ bool ParseGUIDString(const char *s, uint64_t *out) {
|
||||
const uint8_t *ResolveByGUID(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
using ResolveByGUID_t = void *(__fastcall *)(int, const char *, uint32_t,
|
||||
uint32_t, int);
|
||||
auto fn = reinterpret_cast<ResolveByGUID_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
|
||||
return static_cast<const uint8_t *>(fn(Offsets::OBJ_TYPE_ITEM, "ItemMgr",
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32),
|
||||
0x172));
|
||||
return static_cast<const uint8_t *>(
|
||||
Object::ByGuid(Offsets::TYPEMASK_ITEM, guid, "ItemMgr", 0x172));
|
||||
}
|
||||
|
||||
bool FindByItemID(void *L, int itemID, ByGUIDResult *out) {
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ const uint8_t *ResolveEquipmentSlot(int slot1Based);
|
||||
int GetBagSlotCount(int bagID);
|
||||
|
||||
// Given a `CGContainer*` (an equipped bag or bank bag, resolved by GUID via
|
||||
// `FUN_OBJECT_RESOLVE_BY_GUID` with `OBJ_TYPE_CONTAINER`), returns its own
|
||||
// `FUN_OBJECT_RESOLVE_BY_GUID` with `TYPEMASK_CONTAINER`), returns its own
|
||||
// inventory-manager object via the container's vtable method at
|
||||
// `+OFF_CONTAINER_GET_INVENTORY`. The returned object has the same layout as
|
||||
// the player inventory manager: slot count at `+0x00`, flat GUID array at
|
||||
|
||||
+2
-2
@@ -32,6 +32,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -54,8 +55,7 @@ using UnlockAll_t = void(__cdecl *)();
|
||||
bool ReadItemGuid(const uint8_t *item, uint32_t *lo, uint32_t *hi) {
|
||||
if (item == nullptr)
|
||||
return false;
|
||||
auto *inst = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
auto *inst = Item::InstanceBlock(item);
|
||||
if (inst == nullptr)
|
||||
return false;
|
||||
*lo = *reinterpret_cast<const uint32_t *>(inst + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -24,21 +25,10 @@ namespace Item::MaxStackSize {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
int PushMaxStackSizeForItemID(void *L, int itemID) {
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
|
||||
+2
-12
@@ -18,6 +18,7 @@
|
||||
#include "item/ID.h"
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -25,17 +26,6 @@ namespace Item::Name {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
// Reads `m_name[0]` directly off a cached record. Items only ever
|
||||
// populate the first name slot — the array shape (`char *m_name[4]`
|
||||
// per VanillaHelpers's `ItemStats_C`) is a client-internal layout, not
|
||||
@@ -44,7 +34,7 @@ const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
int PushNameForItemID(void *L, int itemID) {
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
|
||||
@@ -75,7 +75,9 @@
|
||||
#include "Offsets.h"
|
||||
#include "bag/UpdateDelayed.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
@@ -196,8 +198,7 @@ bool RemoveFrom(uint64_t *arr, int *count, uint64_t guid) {
|
||||
uint64_t ItemGUID(const uint8_t *item) {
|
||||
if (item == nullptr)
|
||||
return 0;
|
||||
auto *instance = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
auto *instance = Item::InstanceBlock(item);
|
||||
if (instance == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
@@ -265,16 +266,11 @@ void AppendGuidArray(const uint8_t *invMgr, int first, int last, uint64_t *out,
|
||||
AddUnique(out, count, arr[slot]);
|
||||
}
|
||||
|
||||
using ResolveByGuid_t = void *(__fastcall *)(int type, const char *debugName,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
int priority);
|
||||
const uint8_t *ResolveByGuid(int type, uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveByGuid_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
|
||||
return static_cast<const uint8_t *>(
|
||||
fn(type, "ItemMgr", static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0x172));
|
||||
Object::ByGuid(type, guid, "ItemMgr", 0x172));
|
||||
}
|
||||
|
||||
// Appends every item the player owns that ISN'T in bags 0..4 — worn
|
||||
@@ -308,7 +304,7 @@ void AppendNonBagOwned(uint64_t *out, int *count) {
|
||||
return;
|
||||
for (int slot = Offsets::INVMGR_BANK_BAG_FIRST_SLOT;
|
||||
slot <= Offsets::INVMGR_BANK_BAG_LAST_SLOT; ++slot) {
|
||||
const uint8_t *bag = ResolveByGuid(Offsets::OBJ_TYPE_CONTAINER, playerArr[slot]);
|
||||
const uint8_t *bag = ResolveByGuid(Offsets::TYPEMASK_CONTAINER, playerArr[slot]);
|
||||
if (bag == nullptr)
|
||||
continue;
|
||||
auto *bagInvMgr =
|
||||
|
||||
+4
-18
@@ -27,10 +27,12 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Openable.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -38,29 +40,13 @@ namespace Item::Openable {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
|
||||
// Reads `ITEM_FIELD_FLAGS` from the instance descriptor at +0x114.
|
||||
// Defaults to 0 if `cgItem` is null or the descriptor pointer hasn't
|
||||
// been populated yet.
|
||||
uint32_t InstanceFlags(const uint8_t *cgItem) {
|
||||
if (cgItem == nullptr)
|
||||
return 0;
|
||||
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
cgItem + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
auto *descriptor = Item::ObjectFields(cgItem);
|
||||
if (descriptor == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint32_t *>(
|
||||
@@ -73,7 +59,7 @@ int PushIsItemOpenable(void *L, const uint8_t *cgItem) {
|
||||
const int itemID = Item::ID::FromCGItem(cgItem);
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
|
||||
+2
-12
@@ -17,6 +17,7 @@
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -24,21 +25,10 @@ namespace Item::Quality {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
int PushQualityForItemID(void *L, int itemID) {
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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 "Record.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Item {
|
||||
|
||||
const uint8_t *PeekRecord(uint32_t itemID) {
|
||||
using GetRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid,
|
||||
void *callback, void *userData,
|
||||
int dedup);
|
||||
auto fn = reinterpret_cast<GetRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
// Null callback = passive peek (no network query). dedup is irrelevant
|
||||
// with a null callback (nothing is appended to the pending list).
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, false);
|
||||
}
|
||||
|
||||
} // namespace Item
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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>
|
||||
|
||||
// Passive read of the client-side item cache (the ItemSparse `ItemStats_C`
|
||||
// records). Every read-only item accessor used to re-declare the same
|
||||
// `DBCache_ItemStats_C_GetRecord` typedef and call it with a null callback;
|
||||
// this names that one passive peek.
|
||||
|
||||
namespace Item {
|
||||
|
||||
// The cached `ItemStats_C` record for `itemID`, or nullptr if it isn't cached.
|
||||
// PASSIVE — a null callback means "look up only": it never warms the cache or
|
||||
// fires SMSG_ITEM_QUERY_SINGLE, so it is safe to call many times per frame
|
||||
// from tooltips / bag scans. (Eagerly triggering a query from a passive getter
|
||||
// is the cache-race trap documented in CLAUDE.md — the request/response path
|
||||
// lives in item/Data.cpp, not here.) Fields are read via the `OFF_ITEMSTATS_*`
|
||||
// offsets.
|
||||
const uint8_t *PeekRecord(uint32_t itemID);
|
||||
|
||||
} // namespace Item
|
||||
+2
-12
@@ -17,6 +17,7 @@
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -24,21 +25,10 @@ namespace Item::SellPrice {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
int PushSellPriceForItemID(void *L, int itemID) {
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
|
||||
+2
-12
@@ -27,6 +27,7 @@
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -34,21 +35,10 @@ namespace Item::Set {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
int PushSetIDForItemID(void *L, int itemID) {
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
|
||||
+3
-13
@@ -29,6 +29,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/Record.h"
|
||||
#include "spell/Lookup.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -37,17 +38,6 @@ namespace Item::Spell {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, bool requestIfMissing);
|
||||
|
||||
const uint8_t *FetchItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, false);
|
||||
}
|
||||
|
||||
const char *SpellNameForID(int spellID) {
|
||||
const uint8_t *record = ::Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
@@ -69,7 +59,7 @@ int __fastcall Script_GetItemSpell(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const uint8_t *record = FetchItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
@@ -113,7 +103,7 @@ int FindOnUseSpellIDInRecord(const uint8_t *record) {
|
||||
int OnUseSpellIDForItemID(uint32_t itemID) {
|
||||
if (itemID == 0)
|
||||
return 0;
|
||||
const uint8_t *record = FetchItemRecord(itemID);
|
||||
const uint8_t *record = Item::PeekRecord(itemID);
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
return FindOnUseSpellIDInRecord(record);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -36,8 +37,7 @@ static int __fastcall Script_GetStackCount(void *L) {
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 1;
|
||||
}
|
||||
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
auto *descriptor = Item::ObjectFields(item);
|
||||
if (descriptor == nullptr) {
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 1;
|
||||
|
||||
+1
-11
@@ -47,6 +47,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -55,10 +56,6 @@ namespace Item::StatAccum {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, bool requestIfMissing);
|
||||
|
||||
// Base-record m_statType uses the ItemModType enum: 0=mana, 1=health,
|
||||
// 3=agility, 4=strength, 5=intellect, 6=spirit, 7=stamina (2 is
|
||||
// deprecated). Returns nullptr for types with no vanilla key.
|
||||
@@ -288,13 +285,6 @@ long Value(const Accum *acc, const char *key) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint8_t *FetchRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, false);
|
||||
}
|
||||
|
||||
double ComputeDPS(const uint8_t *record) {
|
||||
if (record == nullptr)
|
||||
return 0.0;
|
||||
|
||||
@@ -54,11 +54,6 @@ void AddByKey(Accum *acc, const char *key, long delta);
|
||||
// Read the accumulated value for `key` (0 for an unknown key). Linear scan.
|
||||
long Value(const Accum *acc, const char *key);
|
||||
|
||||
// Passive cache peek for an item's `ItemStats_C` record. Does NOT warm
|
||||
// the cache (returns nullptr for an uncached item) — see the item-cache
|
||||
// race note in CLAUDE.md.
|
||||
const uint8_t *FetchRecord(uint32_t itemID);
|
||||
|
||||
// Fold an item record's base stats + on-equip-spell stats into `acc`,
|
||||
// scaled by `sign` (+1 to add, -1 to subtract for a delta). No-op for a
|
||||
// null record.
|
||||
|
||||
+4
-3
@@ -24,6 +24,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/StatAccum.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -70,7 +71,7 @@ int __fastcall Script_GetItemStats(void *L) {
|
||||
return 0;
|
||||
}
|
||||
const int suffixID = StatAccum::ParseRandomSuffixFromLink(L, 1);
|
||||
const uint8_t *record = StatAccum::FetchRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
@@ -99,8 +100,8 @@ int __fastcall Script_GetItemStatDelta(void *L) {
|
||||
}
|
||||
const int suffix1 = StatAccum::ParseRandomSuffixFromLink(L, 1);
|
||||
const int suffix2 = StatAccum::ParseRandomSuffixFromLink(L, 2);
|
||||
const uint8_t *r1 = StatAccum::FetchRecord(static_cast<uint32_t>(id1));
|
||||
const uint8_t *r2 = StatAccum::FetchRecord(static_cast<uint32_t>(id2));
|
||||
const uint8_t *r1 = Item::PeekRecord(static_cast<uint32_t>(id1));
|
||||
const uint8_t *r2 = Item::PeekRecord(static_cast<uint32_t>(id2));
|
||||
if (r1 == nullptr || r2 == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
|
||||
+3
-4
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
@@ -40,8 +41,7 @@ bool ReadGuid(const void *cgObject, uint32_t *lo, uint32_t *hi) {
|
||||
if (cgObject == nullptr)
|
||||
return false;
|
||||
auto *base = static_cast<const uint8_t *>(cgObject);
|
||||
auto *inst = *reinterpret_cast<const uint8_t *const *>(
|
||||
base + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
auto *inst = Item::InstanceBlock(base);
|
||||
if (inst == nullptr)
|
||||
return false;
|
||||
*lo = *reinterpret_cast<const uint32_t *>(inst + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
@@ -257,8 +257,7 @@ bool MoveCount(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot, int co
|
||||
// server treats as a merge for same-item destinations (up to
|
||||
// maxStack). Route there for count == srcStack so callers don't
|
||||
// have to special-case "move everything".
|
||||
auto *srcDescriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
srcItem + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
auto *srcDescriptor = Item::ObjectFields(srcItem);
|
||||
const int srcStack = srcDescriptor == nullptr ? 0 :
|
||||
static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
srcDescriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT));
|
||||
|
||||
+6
-18
@@ -18,7 +18,9 @@
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/QualityColor.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/TooltipItem.h"
|
||||
#include "object/Resolve.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -138,21 +140,6 @@ static int __fastcall Script_GameTooltipSetInventoryItemByID(void *L) {
|
||||
}
|
||||
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
static const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
using ResolveObjectByGuid_t = void *(__fastcall *)(int type, const char *debugName,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
int priority);
|
||||
|
||||
// Resolves a stored item GUID into a CGItem via the engine's own
|
||||
// resolver. Same path Item::Count uses for direct bank reads — no
|
||||
// gating, no inventory walk, works for any object the engine has
|
||||
@@ -161,8 +148,9 @@ using ResolveObjectByGuid_t = void *(__fastcall *)(int type, const char *debugNa
|
||||
static void *ResolveItemByGuid(uint32_t guidLo, uint32_t guidHi) {
|
||||
if (guidLo == 0 && guidHi == 0)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveObjectByGuid_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
|
||||
return fn(Offsets::OBJ_TYPE_ITEM, "GameTooltip:GetItem", guidLo, guidHi, 0x172);
|
||||
return Object::ByGuid(Offsets::TYPEMASK_ITEM,
|
||||
(static_cast<uint64_t>(guidHi) << 32) | guidLo,
|
||||
"GameTooltip:GetItem", 0x172);
|
||||
}
|
||||
|
||||
// `GameTooltip:GetItem()` → (name, link, itemID) for whichever item
|
||||
@@ -248,7 +236,7 @@ static int __fastcall Script_GameTooltipGetItem(void *L) {
|
||||
// OFF_TOOLTIP_COMPARE_SUFFIX — include it (in the link's suffix field)
|
||||
// when the compare-descriptor flag is set, so the link round-trips the
|
||||
// suffix for stat comparison. Matches 3.3.5's auction GetItem.
|
||||
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
|
||||
+2
-15
@@ -42,6 +42,7 @@
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -49,25 +50,11 @@ namespace Item::Uniqueness {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid,
|
||||
void *callback, void *userData,
|
||||
int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD));
|
||||
auto *cache = reinterpret_cast<void *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ITEMDB_CACHE));
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
// Returns the item's `m_maxCount` or -1 if the record isn't cached.
|
||||
// `-1` is a "warm-and-bail" sentinel; caller pushes nothing on -1
|
||||
// so the Lua side sees the modern "MayReturnNothing" behaviour.
|
||||
int ReadMaxCount(uint32_t itemID) {
|
||||
const uint8_t *record = PeekItemRecord(itemID);
|
||||
const uint8_t *record = Item::PeekRecord(itemID);
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(itemID);
|
||||
return -1;
|
||||
|
||||
+2
-13
@@ -40,6 +40,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/Spell.h"
|
||||
#include "spell/Lookup.h"
|
||||
|
||||
@@ -49,21 +50,9 @@ namespace Item::Usable {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using SpellIsUsable_t = int(__fastcall *)(const uint8_t *spellRecord, int *outNoMana);
|
||||
|
||||
// Same item-cache peek pattern as the other item modules (no shared
|
||||
// helper — each call site is one line different). Sync, no load fired.
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
const uint8_t *PlayerDescriptor() {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(resolve("player"));
|
||||
@@ -91,7 +80,7 @@ Usability ComputeUsability(int itemID) {
|
||||
if (itemID <= 0)
|
||||
return r;
|
||||
|
||||
auto *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
auto *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return r; // not cached → sync "false", no load fired
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -52,8 +53,7 @@ EnchantInfo ReadTempEnchant(int paperdollSlot) {
|
||||
const uint8_t *item = Item::Location::ResolveEquipmentSlot(paperdollSlot);
|
||||
if (item == nullptr)
|
||||
return r;
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
auto *desc = Item::ObjectFields(item);
|
||||
if (desc == nullptr)
|
||||
return r;
|
||||
auto *slot = reinterpret_cast<const uint32_t *>(desc + OFF_TEMP_ENCHANT_SLOT);
|
||||
|
||||
+2
-20
@@ -47,6 +47,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Names.h"
|
||||
#include "event/Custom.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
@@ -107,25 +108,6 @@ uint32_t ClassIdFromRecord(const uint8_t *rec) {
|
||||
: 0;
|
||||
}
|
||||
|
||||
// ChrClasses.dbc record ID -> class token ("WARRIOR", "MAGE", …). The DBC's
|
||||
// filename field is the token; RAID_CLASS_COLORS et al. key on it. nullptr if
|
||||
// the ID is out of range or the record is missing.
|
||||
const char *ClassTokenForId(uint32_t classId) {
|
||||
if (classId == 0)
|
||||
return nullptr;
|
||||
const int count = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CHRCLASSES_COUNT));
|
||||
const uint8_t *const *records = *reinterpret_cast<const uint8_t *const *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CHRCLASSES_RECORDS));
|
||||
if (records == nullptr || static_cast<int>(classId) > count)
|
||||
return nullptr;
|
||||
const uint8_t *rec = records[classId];
|
||||
if (rec == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const char *const *>(
|
||||
rec + Offsets::OFF_CHRCLASSES_FILENAME);
|
||||
}
|
||||
|
||||
constexpr int kNameLen = 48; // engine NameCache inline name[48]
|
||||
|
||||
void CopyName(char *dst, const char *src) {
|
||||
@@ -506,7 +488,7 @@ int __fastcall Script_GetPlayerInfo(void *L) {
|
||||
Game::Lua::PushString(L, r.name); // 1 name
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
const char *classToken = ClassTokenForId(r.classId); // 2 class token
|
||||
const char *classToken = DBC::ClassToken(r.classId); // 2 class token
|
||||
if (classToken != nullptr)
|
||||
Game::Lua::PushString(L, classToken);
|
||||
else
|
||||
|
||||
+2
-18
@@ -14,6 +14,7 @@
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "../object/Resolve.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -62,19 +63,6 @@ using GetPosition_t = const C3Vector *(__thiscall *)(const void *self,
|
||||
C3Vector *outBuf);
|
||||
constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
|
||||
|
||||
// `ClntObjMgrObjectPtr(typeMask, debugMessage, guid_lo, guid_hi, debugCode)`
|
||||
// — resolves a 64-bit GUID to the loaded `CGObject_C *` it currently
|
||||
// refers to, filtered by `typeMask`. Returns NULL when no live object
|
||||
// matches the GUID *and* the typeMask. Splitting the GUID into lo/hi
|
||||
// DWORDs on the stack is equivalent to the engine's
|
||||
// `__fastcall(uint32_t, const char*, uint64_t, int)` signature once the
|
||||
// `uint64_t` argument is placed in the same stack slots.
|
||||
using ClntObjMgrObjectPtr_t = void *(__fastcall *)(uint32_t typeMask,
|
||||
const char *debugMsg,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int debugCode);
|
||||
|
||||
// Reads `(unit.m_objectFields[+0x224] & UNIT_DYNFLAG_LOOTABLE) != 0`
|
||||
// — the server-driven bit it sets when the local player has loot
|
||||
// rights on this unit. Works uniformly for live and dead CGUnit
|
||||
@@ -157,11 +145,7 @@ struct ScanCtx {
|
||||
};
|
||||
|
||||
int __fastcall ScanCallback(ScanCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
auto ObjectPtr = reinterpret_cast<ClntObjMgrObjectPtr_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *obj = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
void *obj = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
|
||||
if (obj == nullptr)
|
||||
return 1; // GUID isn't a unit; continue
|
||||
|
||||
|
||||
+3
-15
@@ -40,6 +40,7 @@
|
||||
#include "../event/Custom.h"
|
||||
#include "../event/SignalHook.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "../object/Resolve.h"
|
||||
#include "../tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -104,11 +105,6 @@ using ClntObjMgrEnumVisibleObjectsCallback_t = int(__fastcall *)(void *ctx,
|
||||
uint64_t guid);
|
||||
using ClntObjMgrEnumVisibleObjects_t =
|
||||
int(__fastcall *)(ClntObjMgrEnumVisibleObjectsCallback_t cb, void *ctx);
|
||||
using ClntObjMgrObjectPtr_t = void *(__fastcall *)(uint32_t typeMask,
|
||||
const char *debugMsg,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int debugCode);
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using LootUnit_t = void(__thiscall *)(void *player, void *target,
|
||||
char useDistanceCheck);
|
||||
@@ -290,11 +286,7 @@ void TryStartNext() {
|
||||
}
|
||||
|
||||
void StartLoot(uint64_t guid) {
|
||||
auto ObjectPtr = reinterpret_cast<ClntObjMgrObjectPtr_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *target = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
void *target = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
|
||||
if (target == nullptr) {
|
||||
TryStartNext();
|
||||
return;
|
||||
@@ -423,11 +415,7 @@ struct EnumCtx {
|
||||
};
|
||||
|
||||
int __fastcall EnumCallback(EnumCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
auto ObjectPtr = reinterpret_cast<ClntObjMgrObjectPtr_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *obj = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
void *obj = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
|
||||
if (obj == nullptr)
|
||||
return 1;
|
||||
if (!IsLootableUnit(obj))
|
||||
|
||||
+2
-10
@@ -14,6 +14,7 @@
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "../object/Resolve.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -21,11 +22,6 @@ namespace Loot::Unit {
|
||||
|
||||
namespace {
|
||||
|
||||
using ClntObjMgrObjectPtr_t = void *(__fastcall *)(uint32_t typeMask,
|
||||
const char *debugMsg,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int debugCode);
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// `FUN_CMSG_LOOT_UNIT` is `__thiscall(player, target, useDistanceCheck)`.
|
||||
@@ -71,11 +67,7 @@ int __fastcall Script_LootUnit(void *L) {
|
||||
if (*reinterpret_cast<void *volatile *>(Offsets::VAR_LOCAL_PLAYER_PTR) == nullptr)
|
||||
return 0;
|
||||
|
||||
auto ObjectPtr = reinterpret_cast<ClntObjMgrObjectPtr_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *target = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
void *target = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
|
||||
if (target == nullptr)
|
||||
return 0;
|
||||
|
||||
|
||||
+2
-10
@@ -23,6 +23,7 @@
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "../object/Resolve.h"
|
||||
#include "../tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -38,11 +39,6 @@ enum class State {
|
||||
WaitingOpen,
|
||||
};
|
||||
|
||||
using ClntObjMgrObjectPtr_t = void *(__fastcall *)(uint32_t typeMask,
|
||||
const char *debugMsg,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int debugCode);
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using LootUnit_t = void(__thiscall *)(void *player, void *target,
|
||||
char useDistanceCheck);
|
||||
@@ -176,11 +172,7 @@ int __fastcall Script_LootUnitItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto ObjectPtr = reinterpret_cast<ClntObjMgrObjectPtr_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *target = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
void *target = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
|
||||
if (target == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user