mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-13 22:50:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84da9c23df | ||
|
|
6e58cb81d1 | ||
|
|
8a065481b4 | ||
|
|
2a7cbf75ca | ||
|
|
ff5ce5cdcb | ||
|
|
47b6a66b96 | ||
|
|
e78c36bb70 | ||
|
|
41fb17502d | ||
|
|
a989499cae | ||
|
|
722c257acb | ||
|
|
a6c7098106 | ||
|
|
7caee620e8 | ||
|
|
954533a170 | ||
|
|
dbe5ab40ee | ||
|
|
f3b328247c | ||
|
|
470361fbce | ||
|
|
2d266a9b76 | ||
|
|
8e3e207f83 | ||
|
|
1fd1295679 | ||
|
|
e4a75e68ca | ||
|
|
28bf41c647 | ||
|
|
ac97f9b960 | ||
|
|
7f6c6e2dd4 | ||
|
|
9d09266fc6 | ||
|
|
d972433ab0 | ||
|
|
b5513c6b22 | ||
|
|
8a434fea0e | ||
|
|
dbbba88aad | ||
|
|
b8bd0bce4e | ||
|
|
c3a80053e9 | ||
|
|
5a3badb5bd |
+77
-15
@@ -4,48 +4,110 @@ 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:
|
||||
tag_name: ${{ steps.ver.outputs.tag }}
|
||||
files: build/Release/ClassicAPI.dll
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,7 +2,31 @@ 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.";
|
||||
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 +39,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,6 +70,8 @@ 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%%";
|
||||
SILVER_AMOUNT_SYMBOL = "S";
|
||||
SLASH_CLEARFOCUS1 = "/fokusaufheben";
|
||||
|
||||
@@ -1,11 +1,39 @@
|
||||
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.";
|
||||
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,6 +67,8 @@ 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%%";
|
||||
SILVER_AMOUNT_SYMBOL = "s";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
|
||||
@@ -2,7 +2,31 @@ 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.";
|
||||
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 +39,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,6 +70,8 @@ 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%%";
|
||||
SILVER_AMOUNT_SYMBOL = "p";
|
||||
SLASH_CLEARFOCUS1 = "/quitarenfoque";
|
||||
|
||||
@@ -2,6 +2,29 @@ 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.";
|
||||
FOCUSTARGET = "Définir la cible de focalisation";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4objet:objets;";
|
||||
ITEMS_EQUIPPED = "%d |4objet:objets; équipés";
|
||||
@@ -12,9 +35,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,6 +70,8 @@ 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%%";
|
||||
SILVER_AMOUNT_SYMBOL = "a";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
|
||||
@@ -2,6 +2,29 @@ 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\".";
|
||||
FOCUSTARGET = "Запомнить цель";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
|
||||
ITEMS_EQUIPPED = "%d |4предмет экипирован:предмета экипировано:предметов экипировано;";
|
||||
@@ -12,9 +35,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,6 +70,8 @@ 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%%";
|
||||
SILVER_AMOUNT_SYMBOL = "с";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
|
||||
@@ -2,6 +2,29 @@ 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中的一件或多件物品,无法装备。";
|
||||
FOCUSTARGET = "设定焦点目标";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
ITEMS_EQUIPPED = "已装备%d件物品";
|
||||
@@ -12,9 +35,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,6 +70,8 @@ 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%%";
|
||||
SILVER_AMOUNT_SYMBOL = "银";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
|
||||
@@ -2,6 +2,29 @@ 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設定中的一個或多個物品未尋獲,因此無法裝備。";
|
||||
FOCUSTARGET = "設定鎖定目標";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
ITEMS_EQUIPPED = "裝備%d件物品";
|
||||
@@ -12,9 +35,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,6 +70,8 @@ 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%%";
|
||||
SILVER_AMOUNT_SYMBOL = "銀";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
|
||||
@@ -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
|
||||
|
||||
+37
-27
@@ -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})
|
||||
|
||||
@@ -35,7 +35,7 @@ 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` |
|
||||
| [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.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
|
||||
@@ -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` |
|
||||
|
||||
+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")
|
||||
|
||||
|
||||
+95
@@ -159,6 +159,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)
|
||||
@@ -335,6 +336,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)
|
||||
@@ -3718,6 +3720,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
|
||||
@@ -8117,6 +8133,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
|
||||
@@ -13645,6 +13715,31 @@ if d then
|
||||
end
|
||||
```
|
||||
|
||||
**Filter tokens.** `filter` uses the modern AuraFilters format: tokens
|
||||
separated by `|` or spaces, each token optionally negated with a
|
||||
leading `!`. This build honors:
|
||||
|
||||
- `HELPFUL` / `HARMFUL` — buffs / debuffs. With neither token the
|
||||
query returns both (the indexed getter defaults to helpful).
|
||||
- `PLAYER` / `!PLAYER` — only auras the local player cast, or only
|
||||
auras the player did not cast. Caster data comes from casts this
|
||||
session, so an aura present before you saw it cast has no caster and
|
||||
counts as not-player.
|
||||
- `DISPELLABLE` / `!DISPELLABLE` — only auras that can be dispelled,
|
||||
purged, or stolen (dispel type Magic, Curse, Disease, or Poison), or
|
||||
only auras that cannot. This is "can it be removed at all", not "can
|
||||
you remove it".
|
||||
- `CROWD_CONTROL` / `!CROWD_CONTROL` — only auras that are a crowd-
|
||||
control effect (stun, fear, silence, root, charm, confuse, disarm, and
|
||||
movement slows), or only auras that are not. Shares the loss-of-control
|
||||
classifier with `C_LossOfControl`, plus snares (which are crowd
|
||||
control but not loss of control).
|
||||
|
||||
All other AuraFilters tokens (`RAID`, `CANCELABLE`,
|
||||
`INCLUDE_NAME_PLATE_ONLY`, `MAW`, and the rest) are accepted and
|
||||
ignored — vanilla has no data for them. Token matching is whole-token,
|
||||
so `RAID_PLAYER_DISPELLABLE` is not read as `PLAYER`.
|
||||
|
||||
### `C_UnitAuras.GetBuffDataByIndex(unit, index)` / `GetDebuffDataByIndex(unit, index)`
|
||||
|
||||
Convenience wrappers locking the filter to `HELPFUL` or `HARMFUL`
|
||||
|
||||
+119
-4
@@ -202,6 +202,19 @@ enum Offsets {
|
||||
// GO field at +0x370.
|
||||
FUN_GAMETOOLTIP_BUILD_UNIT = 0x00529FE0,
|
||||
FUN_GAMETOOLTIP_BUILD_GAMEOBJECT = 0x0052AA20,
|
||||
// Pointer to the GameTooltip CFrameScriptObject the engine's mouseover
|
||||
// setter builds into (`DAT_00b4b3c4`; the value at this address is the
|
||||
// tooltip object). `Frame::Attributes` reads it to build the unit tooltip
|
||||
// for offline/out-of-range party & raid members, which FUN_00492890 skips.
|
||||
VAR_GAMETOOLTIP_OBJECT_PTR = 0x00B4B3C4,
|
||||
// The tooltip's OnTooltipSetDefaultAnchor handler slot (the `{handler,
|
||||
// context}` pair the script resolver maps that name to). The engine
|
||||
// mouseover setter fires this before building to (re)establish the
|
||||
// tooltip owner/anchor via FrameXML's GameTooltip_SetDefaultAnchor —
|
||||
// without it a rebuild after the tooltip was hidden has no owner and
|
||||
// stays invisible. `Frame::Attributes` fires it (via the self-contained
|
||||
// FUN_FRAME_INVOKE_SCRIPT) before the offline roster build.
|
||||
OFF_TOOLTIP_SET_DEFAULT_ANCHOR_HANDLER = 0x444,
|
||||
FUN_GAMETOOLTIP_ADD_LINE = 0x00530270, // __thiscall(self, left, right, lColorBGRA*, rColorBGRA*, wrap)
|
||||
OFF_GAMETOOLTIP_NUM_LINES = 0x31C, // int — live line count (AddLine index; +0x320 is the cap)
|
||||
// The displayed item's identity is read via the existing
|
||||
@@ -1498,6 +1511,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 +1531,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
|
||||
@@ -3407,6 +3438,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
|
||||
@@ -5680,14 +5759,46 @@ enum Offsets {
|
||||
// VAR_SPELLMOD_*). Verified from FUN_006e6b30: SpellFamilyName at
|
||||
// +0x280 (gates whether the player's class mods apply), 64-bit
|
||||
// SpellFamilyFlags at +0x284 (selects which familyFlagBit rows to
|
||||
// sum). AttributesEx2 (+0x24) bit 0x20000000 disables spell mods.
|
||||
// sum). AttributesEx3 (+0x24) bit 0x20000000 (IGNORE_CASTER_MODIFIERS)
|
||||
// disables spell mods — the client reads [record + 0x24] in FUN_006e6b30.
|
||||
OFF_SPELL_RECORD_FAMILY_NAME = 0x280, // u32
|
||||
OFF_SPELL_RECORD_FAMILY_FLAGS = 0x284, // u64
|
||||
OFF_SPELL_RECORD_ICON_ID = 0x1D4, // u32 SpellIconID (→ SpellIcon.dbc)
|
||||
OFF_SPELL_RECORD_ATTRIBUTES = 0x18, // u32 (base Attributes)
|
||||
OFF_SPELL_RECORD_ATTRIBUTES = 0x18, // u32 (column 6, base Attributes)
|
||||
SPELL_ATTR_PASSIVE = 0x40, // bit 6 — always-on aura
|
||||
OFF_SPELL_RECORD_ATTRIBUTES_EX2 = 0x24, // u32
|
||||
SPELL_ATTR_EX2_NO_SPELL_MODS = 0x20000000,
|
||||
OFF_SPELL_RECORD_ATTRIBUTES_EX = 0x1C, // u32 (column 7)
|
||||
OFF_SPELL_RECORD_ATTRIBUTES_EX2 = 0x20, // u32 (column 8)
|
||||
OFF_SPELL_RECORD_ATTRIBUTES_EX3 = 0x24, // u32 (column 9)
|
||||
// The client's spell-mod core (FUN_006e6b30) skips all mods when this bit
|
||||
// is set: it reads [record + 0x24] & 0x20000000 — AttributesEx3 bit 29,
|
||||
// SPELL_ATTR_EX3_IGNORE_CASTER_MODIFIERS (verified via disassembly; the
|
||||
// server enum agrees). NOT AttributesEx2's bit 29 (that's CANT_CRIT).
|
||||
SPELL_ATTR_EX3_IGNORE_CASTER_MODIFIERS = 0x20000000,
|
||||
// AttributesEx channel bits (CHANNELED_1 0x4 | CHANNELED_2 0x40) and the
|
||||
// AttributesEx2 autorepeat flag (bit 5, Auto Shot / Shoot) — each shared
|
||||
// by more than one module, so kept here rather than redefined locally.
|
||||
SPELL_ATTR_EX_CHANNELED = 0x4 | 0x40,
|
||||
SPELL_ATTR_EX2_AUTOREPEAT_FLAG = 0x20,
|
||||
OFF_SPELL_RECORD_INTERRUPT_FLAGS = 0x54, // u32 (column 21)
|
||||
|
||||
// Remaining Spell.dbc record fields — the single source of truth for the
|
||||
// record layout (byte = column * 4). Modules must use these rather than
|
||||
// redefining local copies.
|
||||
OFF_SPELL_RECORD_CASTING_TIME_INDEX = 0x48, // u32 → SpellCastTimes.dbc
|
||||
OFF_SPELL_RECORD_MAX_LEVEL = 0x6C, // u32 (scaling cap level)
|
||||
OFF_SPELL_RECORD_SPELL_LEVEL = 0x74, // u32 (this rank's effective level)
|
||||
OFF_SPELL_RECORD_POWER_TYPE = 0x7C, // i32 (0=mana,1=rage,2=focus,3=energy,4=happiness)
|
||||
OFF_SPELL_RECORD_MANA_COST = 0x80, // u32 base cost
|
||||
OFF_SPELL_RECORD_RANGE_INDEX = 0x90, // u32 → SpellRange.dbc
|
||||
OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A = 0x148,// i32[3] implicit target A
|
||||
OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_B = 0x154,// i32[3] implicit target B
|
||||
OFF_SPELL_RECORD_MANA_COST_PERCENT = 0x270, // i32 % of base resource (0 = flat)
|
||||
OFF_SPELL_RECORD_RANK = 0x204, // char*[9] localized rank text
|
||||
OFF_SPELL_RECORD_TOTEM = 0xA0, // i32[2] required totem/tool item IDs
|
||||
|
||||
// SpellIcon.dbc record's path string field (char* at +0x04). Distinct
|
||||
// struct from Spell.dbc — reached via SpellIcon.dbc[Spell.SpellIconID].
|
||||
OFF_SPELLICON_PATH = 0x04,
|
||||
|
||||
// Spell.dbc Mechanic field — the spell-level SpellMechanic ID (→
|
||||
// SpellMechanic.dbc), 0 = none. Field 5 of the record
|
||||
@@ -5709,6 +5820,10 @@ enum Offsets {
|
||||
SPELL_AURA_MOD_CHARM = 6,
|
||||
SPELL_AURA_MOD_FEAR = 7,
|
||||
SPELL_AURA_MOD_STUN = 12,
|
||||
// Movement slow / snare. Not a loss-of-control (C_LossOfControl omits it),
|
||||
// but the C_UnitAuras CROWD_CONTROL filter counts it — see
|
||||
// Spell::CrowdControl::IsCrowdControl.
|
||||
SPELL_AURA_MOD_DECREASE_SPEED = 33,
|
||||
SPELL_AURA_MOD_PACIFY = 25,
|
||||
SPELL_AURA_MOD_ROOT = 26,
|
||||
SPELL_AURA_MOD_SILENCE = 27,
|
||||
|
||||
+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);
|
||||
|
||||
+108
-61
@@ -17,12 +17,20 @@
|
||||
// nameplate flags, etc.) are populated with vanilla-truthful
|
||||
// defaults so consumers reading those keys get sensible values.
|
||||
//
|
||||
// Filter parsing here mirrors what most modern addons actually pass:
|
||||
// "HELPFUL" / "HARMFUL" are honored, every other modern filter
|
||||
// (`PLAYER` / `RAID` / `CANCELABLE` / `INCLUDE_NAME_PLATE_ONLY`) is
|
||||
// accepted but no-ops — they'd require either source-GUID tracking
|
||||
// (`PLAYER`), class-dispel-matrix infra (`RAID`), or systems
|
||||
// vanilla doesn't have at all.
|
||||
// Filter parsing (`ParseFilters`) tokenizes the modern AuraFilters string:
|
||||
// tokens separated by `|` and/or whitespace, each optionally negated with a
|
||||
// leading `!`. Whole-token matching, so `RAID_PLAYER_DISPELLABLE` is not
|
||||
// mistaken for `PLAYER` and `!PLAYER` is a negation rather than a match.
|
||||
// Honored: `HELPFUL` / `HARMFUL` (descriptor slot ranges), `PLAYER` /
|
||||
// `!PLAYER` (caster == / != the local player, from the Aura::Source cache),
|
||||
// `DISPELLABLE` / `!DISPELLABLE` (dispel type is / isn't one a
|
||||
// dispel/purge/steal can remove — Spell.dbc Dispel ∈ Magic/Curse/Disease/
|
||||
// Poison, matching the server's DISPEL_ALL_MASK), and `CROWD_CONTROL` /
|
||||
// `!CROWD_CONTROL` (is / isn't a hard control effect, via the shared
|
||||
// `Spell::CrowdControl` classifier that also backs C_LossOfControl). Every
|
||||
// other modern token (`RAID`, `CANCELABLE`, `INCLUDE_NAME_PLATE_ONLY`, `MAW`,
|
||||
// …) is accepted and ignored — they need a class-dispel matrix or systems
|
||||
// vanilla has no data for.
|
||||
|
||||
#include "Data.h"
|
||||
|
||||
@@ -60,24 +68,72 @@ uint64_t GuidForOutOfRange(const char *token) {
|
||||
return Unit::Identity::GuidForToken(token);
|
||||
}
|
||||
|
||||
// Parses the filter string. Returns Helpful by default; Harmful if
|
||||
// the string contains the substring "HARMFUL". Case-sensitive — the
|
||||
// modern API documents the filter tokens as upper-case constants
|
||||
// (`"HELPFUL"` etc.) and addons that pass them through case-changing
|
||||
// transforms are already broken on modern too.
|
||||
Data::Filter ParseFilter(const char *filter) {
|
||||
if (filter == nullptr)
|
||||
return Data::Filter::Helpful;
|
||||
if (strstr(filter, "HARMFUL") != nullptr)
|
||||
return Data::Filter::Harmful;
|
||||
return Data::Filter::Helpful;
|
||||
// Parsed AuraFilters string. `helpful`/`harmful` record which range tokens
|
||||
// were present (neither → both, matching modern's default); `caster` carries
|
||||
// the PLAYER / !PLAYER restriction.
|
||||
struct ParsedFilter {
|
||||
bool helpful = false;
|
||||
bool harmful = false;
|
||||
Data::CasterMode caster = Data::CasterMode::Any;
|
||||
Data::DispelMode dispel = Data::DispelMode::Any;
|
||||
Data::CcMode cc = Data::CcMode::Any;
|
||||
|
||||
Data::Match ToMatch() const { return Data::Match{caster, dispel, cc}; }
|
||||
};
|
||||
|
||||
// Reduces the parsed range tokens to the single `Data::Filter` the indexed /
|
||||
// by-id / by-name getters take: Harmful when HARMFUL was given, else Helpful
|
||||
// (the modern default). HELPFUL wins ties are irrelevant here — an indexed
|
||||
// getter reads one range.
|
||||
Data::Filter RangeFilter(const ParsedFilter &pf) {
|
||||
return pf.harmful && !pf.helpful ? Data::Filter::Harmful
|
||||
: Data::Filter::Helpful;
|
||||
}
|
||||
|
||||
// The `PLAYER` filter token restricts results to auras the local player
|
||||
// cast. No other modern token contains the substring "PLAYER", so a plain
|
||||
// substring test is safe.
|
||||
bool HasPlayerFilter(const char *filter) {
|
||||
return filter != nullptr && strstr(filter, "PLAYER") != nullptr;
|
||||
// Tokenizes the filter string. Case-sensitive (modern documents the tokens as
|
||||
// upper-case constants). `HELPFUL`/`HARMFUL` are positive range selectors;
|
||||
// negating them is meaningless for our slot-based ranges, so a leading `!` is
|
||||
// ignored on those. `PLAYER` honors negation (`!PLAYER` → NotPlayer). Any
|
||||
// unrecognized token is accepted and skipped.
|
||||
ParsedFilter ParseFilters(const char *filter) {
|
||||
ParsedFilter out;
|
||||
if (filter == nullptr)
|
||||
return out;
|
||||
for (const char *p = filter; *p != '\0';) {
|
||||
while (*p == '|' || *p == ' ' || *p == '\t')
|
||||
++p;
|
||||
if (*p == '\0')
|
||||
break;
|
||||
bool negate = false;
|
||||
if (*p == '!') {
|
||||
negate = true;
|
||||
++p;
|
||||
}
|
||||
char tok[64];
|
||||
size_t n = 0;
|
||||
while (*p != '\0' && *p != '|' && *p != ' ' && *p != '\t') {
|
||||
if (n + 1 < sizeof(tok))
|
||||
tok[n++] = *p;
|
||||
++p;
|
||||
}
|
||||
tok[n] = '\0';
|
||||
|
||||
if (strcmp(tok, "HELPFUL") == 0)
|
||||
out.helpful = true;
|
||||
else if (strcmp(tok, "HARMFUL") == 0)
|
||||
out.harmful = true;
|
||||
else if (strcmp(tok, "PLAYER") == 0)
|
||||
out.caster = negate ? Data::CasterMode::NotPlayer
|
||||
: Data::CasterMode::PlayerOnly;
|
||||
else if (strcmp(tok, "DISPELLABLE") == 0)
|
||||
out.dispel = negate ? Data::DispelMode::NotDispellable
|
||||
: Data::DispelMode::DispellableOnly;
|
||||
else if (strcmp(tok, "CROWD_CONTROL") == 0)
|
||||
out.cc = negate ? Data::CcMode::NotCrowdControl
|
||||
: Data::CcMode::CrowdControlOnly;
|
||||
// else: accepted and ignored (retail-only / unimplemented token).
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const char *ArgUnit(void *L, int idx) {
|
||||
@@ -102,10 +158,10 @@ const char *ArgOptString(void *L, int idx) {
|
||||
// or nil if no such aura. Used by `GetAuraDataByIndex` and the
|
||||
// filter-locked aliases.
|
||||
int PushAuraByIndex(void *L, const char *unitToken, int index,
|
||||
Data::Filter filter, bool playerOnly = false) {
|
||||
Data::Filter filter, Data::Match match = {}) {
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
if (unit != nullptr) {
|
||||
const int slot = Data::FindNthSlot(unit, index, filter, playerOnly);
|
||||
const int slot = Data::FindNthSlot(unit, index, filter, match);
|
||||
if (slot >= 0) {
|
||||
Data::Push(L, unit, slot);
|
||||
return 1;
|
||||
@@ -113,14 +169,14 @@ int PushAuraByIndex(void *L, const char *unitToken, int index,
|
||||
// Descriptor exhausted — an aura the engine dropped from the slot array
|
||||
// (rogue stealth, nearby party range fluctuation) may still be live in
|
||||
// the Aura::Source cache. Surface it after the descriptor entries.
|
||||
if (Data::PushNthCacheFallback(L, unit, index, filter, playerOnly))
|
||||
if (Data::PushNthCacheFallback(L, unit, index, filter, match))
|
||||
return 1;
|
||||
} else {
|
||||
// No live CGUnit — an out-of-range / cross-map groupmate. The server
|
||||
// still sends their aura spell IDs via SMSG_PARTY_MEMBER_STATS; read
|
||||
// them the same way the built-in UnitBuff/UnitDebuff do.
|
||||
if (Data::PushNthGroupAura(L, GuidForOutOfRange(unitToken), index,
|
||||
filter, playerOnly))
|
||||
filter, match))
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushNil(L);
|
||||
@@ -135,8 +191,8 @@ int __fastcall Script_GetAuraDataByIndex(void *L) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
return PushAuraByIndex(L, unit, index, ParseFilter(filterStr),
|
||||
HasPlayerFilter(filterStr));
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
return PushAuraByIndex(L, unit, index, RangeFilter(pf), pf.ToMatch());
|
||||
}
|
||||
|
||||
int __fastcall Script_GetBuffDataByIndex(void *L) {
|
||||
@@ -168,23 +224,19 @@ int __fastcall Script_GetUnitAuraBySpellID(void *L) {
|
||||
return 1;
|
||||
}
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
Data::Filter f;
|
||||
const Data::Filter *fp = nullptr;
|
||||
if (filterStr != nullptr) {
|
||||
f = ParseFilter(filterStr);
|
||||
fp = &f;
|
||||
}
|
||||
const bool playerOnly = HasPlayerFilter(filterStr);
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
Data::Filter f = RangeFilter(pf);
|
||||
const Data::Filter *fp = (pf.helpful || pf.harmful) ? &f : nullptr;
|
||||
if (unit != nullptr) {
|
||||
const int slot = Data::FindSlotBySpellID(
|
||||
unit, static_cast<uint32_t>(spellID), fp, playerOnly);
|
||||
unit, static_cast<uint32_t>(spellID), fp, pf.ToMatch());
|
||||
if (slot >= 0) {
|
||||
Data::Push(L, unit, slot);
|
||||
return 1;
|
||||
}
|
||||
} else if (Data::PushGroupAuraBySpellID(L, GuidForOutOfRange(unitToken),
|
||||
static_cast<uint32_t>(spellID), fp,
|
||||
playerOnly)) {
|
||||
pf.ToMatch())) {
|
||||
// Out-of-range groupmate — read from the group-member aura array.
|
||||
return 1;
|
||||
}
|
||||
@@ -201,21 +253,17 @@ int __fastcall Script_GetAuraDataBySpellName(void *L) {
|
||||
return 1;
|
||||
}
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
Data::Filter f;
|
||||
const Data::Filter *fp = nullptr;
|
||||
if (filterStr != nullptr) {
|
||||
f = ParseFilter(filterStr);
|
||||
fp = &f;
|
||||
}
|
||||
const bool playerOnly = HasPlayerFilter(filterStr);
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
Data::Filter f = RangeFilter(pf);
|
||||
const Data::Filter *fp = (pf.helpful || pf.harmful) ? &f : nullptr;
|
||||
if (unit != nullptr) {
|
||||
const int slot = Data::FindSlotBySpellName(unit, spellName, fp, playerOnly);
|
||||
const int slot = Data::FindSlotBySpellName(unit, spellName, fp, pf.ToMatch());
|
||||
if (slot >= 0) {
|
||||
Data::Push(L, unit, slot);
|
||||
return 1;
|
||||
}
|
||||
} else if (Data::PushGroupAuraBySpellName(L, GuidForOutOfRange(unitToken),
|
||||
spellName, fp, playerOnly)) {
|
||||
spellName, fp, pf.ToMatch())) {
|
||||
// Out-of-range groupmate — read from the group-member aura array.
|
||||
return 1;
|
||||
}
|
||||
@@ -243,7 +291,7 @@ int __fastcall Script_GetPlayerAuraBySpellID(void *L) {
|
||||
// sequential keys starting from `nextKey`. Updates `nextKey` so a
|
||||
// follow-up call can append to the same outer table.
|
||||
void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
|
||||
Data::Filter filter, int &nextKey, bool playerOnly) {
|
||||
Data::Filter filter, int &nextKey, Data::Match match) {
|
||||
const int start = (filter == Data::Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
@@ -253,14 +301,15 @@ void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!Data::IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (playerOnly && !Data::IsPlayerCast(unit, slot))
|
||||
if (!Data::MatchesAura(match, Data::IsPlayerCast(unit, slot),
|
||||
Data::ReadSpellID(unit, slot)))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
Data::Push(L, unit, slot);
|
||||
Game::Lua::SetTable(L, outerIdx);
|
||||
}
|
||||
// Append auras the descriptor dropped but Aura::Source still has live.
|
||||
Data::AppendCacheFallbacks(L, unit, filter, playerOnly, outerIdx, nextKey);
|
||||
Data::AppendCacheFallbacks(L, unit, filter, match, outerIdx, nextKey);
|
||||
}
|
||||
|
||||
int __fastcall Script_GetUnitAuras(void *L) {
|
||||
@@ -274,26 +323,24 @@ int __fastcall Script_GetUnitAuras(void *L) {
|
||||
// Range tokens are independent of PLAYER: an explicit HELPFUL/HARMFUL
|
||||
// selects that range, neither selects both. So `"PLAYER"` alone returns
|
||||
// both ranges restricted to player-cast auras (matching retail).
|
||||
const bool playerOnly = HasPlayerFilter(filterStr);
|
||||
const bool hasHelpful = filterStr != nullptr && strstr(filterStr, "HELPFUL") != nullptr;
|
||||
const bool hasHarmful = filterStr != nullptr && strstr(filterStr, "HARMFUL") != nullptr;
|
||||
const bool both = !hasHelpful && !hasHarmful;
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
const bool both = !pf.helpful && !pf.harmful;
|
||||
|
||||
int nextKey = 1;
|
||||
if (unit != nullptr) {
|
||||
if (hasHelpful || both)
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Helpful, nextKey, playerOnly);
|
||||
if (hasHarmful || both)
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Harmful, nextKey, playerOnly);
|
||||
if (pf.helpful || both)
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Helpful, nextKey, pf.ToMatch());
|
||||
if (pf.harmful || both)
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Harmful, nextKey, pf.ToMatch());
|
||||
} else {
|
||||
// No live CGUnit — out-of-range / cross-map groupmate. Enumerate the
|
||||
// group-member aura array (spell IDs the server still transmits), the
|
||||
// same source the built-in UnitBuff/UnitDebuff read out of range.
|
||||
const uint64_t guid = GuidForOutOfRange(unitToken);
|
||||
if (hasHelpful || both)
|
||||
Data::AppendGroupAuras(L, guid, Data::Filter::Helpful, playerOnly, 1, nextKey);
|
||||
if (hasHarmful || both)
|
||||
Data::AppendGroupAuras(L, guid, Data::Filter::Harmful, playerOnly, 1, nextKey);
|
||||
if (pf.helpful || both)
|
||||
Data::AppendGroupAuras(L, guid, Data::Filter::Helpful, pf.ToMatch(), 1, nextKey);
|
||||
if (pf.harmful || both)
|
||||
Data::AppendGroupAuras(L, guid, Data::Filter::Harmful, pf.ToMatch(), 1, nextKey);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
+146
-66
@@ -19,6 +19,8 @@
|
||||
#include "dbc/Lookup.h"
|
||||
#include "group/MemberStats.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "spell/CrowdControl.h"
|
||||
#include "spell/IsSelfBuff.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -28,13 +30,6 @@ namespace Aura::Data {
|
||||
|
||||
namespace {
|
||||
|
||||
// Spell.dbc record field offsets used locally. `spell/Info.cpp` has
|
||||
// the same set of locals for `Script_GetSpellInfo`; promoting them
|
||||
// to `Offsets.h` is a future refactor — for now we mirror its
|
||||
// convention so both files stay in sync.
|
||||
constexpr int OFF_SPELL_ICON_ID = 0x1D4;
|
||||
constexpr int OFF_SPELLICON_PATH = 0x04;
|
||||
|
||||
const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
if (unit == nullptr)
|
||||
return nullptr;
|
||||
@@ -72,13 +67,13 @@ const char *SpellIconPath(const uint8_t *spellRecord) {
|
||||
if (spellRecord == nullptr)
|
||||
return nullptr;
|
||||
const int iconID = *reinterpret_cast<const int *>(
|
||||
spellRecord + OFF_SPELL_ICON_ID);
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
if (iconID <= 0)
|
||||
return nullptr;
|
||||
return DBC::StringField(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT,
|
||||
static_cast<uint32_t>(iconID),
|
||||
OFF_SPELLICON_PATH);
|
||||
Offsets::OFF_SPELLICON_PATH);
|
||||
}
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *);
|
||||
@@ -209,6 +204,18 @@ double SpellBaseDurationSeconds(uint32_t spellID, int unitLevel) {
|
||||
// Reads the player's level from UNIT_FIELD_LEVEL. Returns 0 when
|
||||
// unresolved (pre-login, no descriptor) — caller should treat that
|
||||
// as "skip level scaling".
|
||||
// True if the engine-tick expiration `expMs` has already reached/passed now.
|
||||
// Same signed-wrap-safe compare `PlayerBuffExpirationSeconds` uses (absorbs
|
||||
// the ~24.86-day GetTickCount wrap). An already-elapsed cached expiration is
|
||||
// not meaningful — the caster is kept regardless, but the time is reported as
|
||||
// unknown rather than as a negative remaining.
|
||||
bool ExpirationElapsed(uint32_t expMs) {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
const uint32_t nowMs = reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
return static_cast<int32_t>(nowMs - expMs) >= 0;
|
||||
}
|
||||
|
||||
int PlayerLevel(const uint8_t *player) {
|
||||
auto *desc = Descriptor(player);
|
||||
if (desc == nullptr)
|
||||
@@ -217,6 +224,25 @@ int PlayerLevel(const uint8_t *player) {
|
||||
desc + Offsets::OFF_UNIT_FIELD_LEVEL);
|
||||
}
|
||||
|
||||
// The caster GUID attributed to `(guid, spellID)`: the observed caster from
|
||||
// the Aura::Source cache, else — when we never saw the cast — the unit itself
|
||||
// if the spell is a self-only-target buff (self-buffs can't be cross-cast).
|
||||
// 0 when genuinely unknown. Single source of truth for both display
|
||||
// (sourceGUID/sourceUnit) and the PLAYER caster filter, so a self-buff shows a
|
||||
// source AND matches HELPFUL|PLAYER instead of the two paths disagreeing.
|
||||
uint64_t EffectiveCaster(uint64_t guid, uint32_t spellID) {
|
||||
if (guid == 0 || spellID == 0)
|
||||
return 0;
|
||||
uint64_t c = 0;
|
||||
uint32_t expMs = 0;
|
||||
uint32_t durMs = 0;
|
||||
if (Aura::Source::Get(guid, spellID, &c, &expMs, &durMs) && c != 0)
|
||||
return c; // observed caster
|
||||
if (Spell::IsSelfBuff::IsSelfBuff(spellID))
|
||||
return guid; // self-only-target aura → cast by the unit itself
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t ReadSpellID(const uint8_t *unit, int slot) {
|
||||
@@ -250,16 +276,67 @@ bool IsPlayerCast(const uint8_t *unit, int slot) {
|
||||
const uint32_t spellID = ReadSpellID(unit, slot);
|
||||
if (spellID == 0)
|
||||
return false;
|
||||
uint64_t casterGuid = 0;
|
||||
uint32_t expMs = 0;
|
||||
uint32_t durMs = 0;
|
||||
if (!Aura::Source::Get(UnitGuid(unit), spellID, &casterGuid, &expMs, &durMs))
|
||||
const uint64_t player = Unit::Identity::PlayerGuid();
|
||||
return player != 0 && EffectiveCaster(UnitGuid(unit), spellID) == player;
|
||||
}
|
||||
|
||||
// Applies the PLAYER / !PLAYER caster restriction. `isPlayerCast` is the
|
||||
// per-aura "was this cast by the local player" answer (false on a cache miss).
|
||||
bool CasterMatches(CasterMode caster, bool isPlayerCast) {
|
||||
switch (caster) {
|
||||
case CasterMode::PlayerOnly: return isPlayerCast;
|
||||
case CasterMode::NotPlayer: return !isPlayerCast;
|
||||
default: return true; // Any
|
||||
}
|
||||
}
|
||||
|
||||
bool IsDispellable(uint32_t spellID) {
|
||||
const uint8_t *rec = SpellRecord(spellID);
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
return casterGuid != 0 && casterGuid == Unit::Identity::PlayerGuid();
|
||||
const uint32_t dispelType = *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_DISPEL_TYPE);
|
||||
// Magic(1) / Curse(2) / Disease(3) / Poison(4) — the server's
|
||||
// DISPEL_ALL_MASK. None(0) / Stealth(5) / Invisibility(6) / Enrage(9)
|
||||
// etc. are not dispellable by any mechanism.
|
||||
return dispelType >= 1 && dispelType <= 4;
|
||||
}
|
||||
|
||||
bool DispelMatches(DispelMode dispel, uint32_t spellID) {
|
||||
switch (dispel) {
|
||||
case DispelMode::DispellableOnly: return IsDispellable(spellID);
|
||||
case DispelMode::NotDispellable: return !IsDispellable(spellID);
|
||||
default: return true; // Any
|
||||
}
|
||||
}
|
||||
|
||||
bool CcMatches(CcMode cc, uint32_t spellID) {
|
||||
switch (cc) {
|
||||
case CcMode::CrowdControlOnly:
|
||||
return Spell::CrowdControl::IsCrowdControl(spellID);
|
||||
case CcMode::NotCrowdControl:
|
||||
return !Spell::CrowdControl::IsCrowdControl(spellID);
|
||||
default:
|
||||
return true; // Any
|
||||
}
|
||||
}
|
||||
|
||||
bool MatchesAura(const Match &match, bool isPlayerCast, uint32_t spellID) {
|
||||
return CasterMatches(match.caster, isPlayerCast) &&
|
||||
DispelMatches(match.dispel, spellID) &&
|
||||
CcMatches(match.cc, spellID);
|
||||
}
|
||||
|
||||
// Group-array analog of IsPlayerCast: the group aura carries no caster, so we
|
||||
// consult the Aura::Source cache by (guid, spellID). A miss counts as "not the
|
||||
// player" (same as IsPlayerCast).
|
||||
bool GroupIsPlayerCast(uint64_t guid, uint32_t spellID) {
|
||||
const uint64_t player = Unit::Identity::PlayerGuid();
|
||||
return player != 0 && EffectiveCaster(guid, spellID) == player;
|
||||
}
|
||||
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
bool playerOnly) {
|
||||
Match match) {
|
||||
if (unit == nullptr || oneBasedIndex < 1)
|
||||
return -1;
|
||||
const int start = (filter == Filter::Harmful)
|
||||
@@ -272,7 +349,7 @@ int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
continue;
|
||||
if (++matches == oneBasedIndex)
|
||||
return slot;
|
||||
@@ -281,7 +358,7 @@ int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
}
|
||||
|
||||
int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
const Filter *filter, bool playerOnly) {
|
||||
const Filter *filter, Match match) {
|
||||
if (unit == nullptr || spellID == 0)
|
||||
return -1;
|
||||
const int start = (filter != nullptr && *filter == Filter::Harmful)
|
||||
@@ -295,7 +372,7 @@ int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
continue;
|
||||
if (ReadSpellID(unit, slot) != spellID)
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
continue;
|
||||
return slot;
|
||||
}
|
||||
@@ -303,7 +380,7 @@ int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
}
|
||||
|
||||
int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const Filter *filter, bool playerOnly) {
|
||||
const Filter *filter, Match match) {
|
||||
if (unit == nullptr || spellName == nullptr || *spellName == '\0')
|
||||
return -1;
|
||||
const int start = (filter != nullptr && *filter == Filter::Harmful)
|
||||
@@ -318,7 +395,7 @@ int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const char *name = LocalizedSpellName(SpellRecord(ReadSpellID(unit, slot)));
|
||||
if (name == nullptr || std::strcmp(name, spellName) != 0)
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
continue;
|
||||
return slot;
|
||||
}
|
||||
@@ -469,12 +546,21 @@ static void PushEnriched(void *L, uint64_t guid, uint32_t spellID,
|
||||
uint32_t expMs = 0;
|
||||
uint32_t durMs = 0;
|
||||
if (Aura::Source::Get(guid, spellID, &c, &expMs, &durMs)) {
|
||||
if (expirationTime == 0.0 && expMs != 0)
|
||||
// A cached expiration that already elapsed while the aura is still
|
||||
// present (non-player casters get an underestimated base duration)
|
||||
// is not meaningful — report unknown (0) rather than a negative
|
||||
// remaining time.
|
||||
if (expirationTime == 0.0 && expMs != 0 && !ExpirationElapsed(expMs))
|
||||
expirationTime = static_cast<double>(expMs) * 0.001;
|
||||
if (durMs != 0)
|
||||
duration = static_cast<double>(durMs) * 0.001;
|
||||
casterGuid = c;
|
||||
}
|
||||
// Caster (sourceGUID/sourceUnit): observed caster, else the self-buff
|
||||
// inference — same resolution the PLAYER filter uses, so a self-buff
|
||||
// both shows a source and matches HELPFUL|PLAYER. Timing stays unknown
|
||||
// when we never saw the cast; only the caster is inferred, and it's
|
||||
// exact.
|
||||
casterGuid = EffectiveCaster(guid, spellID);
|
||||
}
|
||||
BuildTable(L, spellID, applications, isHelpful, duration, expirationTime,
|
||||
casterGuid);
|
||||
@@ -548,8 +634,8 @@ bool DescriptorHasVisibleAura(const uint8_t *unit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Counts populated descriptor slots matching `filter` (and `playerOnly`).
|
||||
int CountSlots(const uint8_t *unit, Filter filter, bool playerOnly) {
|
||||
// Counts populated descriptor slots matching `filter` (and `caster`).
|
||||
int CountSlots(const uint8_t *unit, Filter filter, Match match) {
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
const int start = (filter == Filter::Harmful)
|
||||
@@ -561,7 +647,7 @@ int CountSlots(const uint8_t *unit, Filter filter, bool playerOnly) {
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
continue;
|
||||
++n;
|
||||
}
|
||||
@@ -571,21 +657,21 @@ int CountSlots(const uint8_t *unit, Filter filter, bool playerOnly) {
|
||||
constexpr int kFallbackMax = Offsets::UNIT_AURA_TOTAL;
|
||||
|
||||
// Whether cache entry `c` should be surfaced as a fallback for `unit` under
|
||||
// `filter`/`playerOnly`: the spell must be a user-visible aura, it must not
|
||||
// `filter`/`caster`: the spell must be a user-visible aura, it must not
|
||||
// already be in a populated descriptor slot (no double-listing the same live
|
||||
// aura) and, when player-filtered, must have been cast by us. Superseded /
|
||||
// dispelled auras are kept out of the cache by `Aura::Source`'s removal
|
||||
// eviction, not filtered here.
|
||||
bool FallbackEligible(const uint8_t *unit, const Aura::Source::CachedAura &c,
|
||||
Filter filter, bool playerOnly) {
|
||||
Filter filter, Match match) {
|
||||
if (!IsVisible(SpellRecord(c.spellId)))
|
||||
return false; // hidden/internal aura (e.g. SPELL_ATTR_HIDDEN_CLIENTSIDE):
|
||||
// the descriptor path drops these via IsSlotPopulated, so
|
||||
// the cache path must apply the same engine predicate or
|
||||
// they leak through as tooltip-less phantom buffs.
|
||||
if (FindSlotBySpellID(unit, c.spellId, &filter, false) >= 0)
|
||||
if (FindSlotBySpellID(unit, c.spellId, &filter, Match{}) >= 0)
|
||||
return false; // still in the descriptor — returned via the slot path
|
||||
if (playerOnly && c.casterGuid != Unit::Identity::PlayerGuid())
|
||||
if (!MatchesAura(match, c.casterGuid == Unit::Identity::PlayerGuid(), c.spellId))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
@@ -593,7 +679,7 @@ bool FallbackEligible(const uint8_t *unit, const Aura::Source::CachedAura &c,
|
||||
} // namespace
|
||||
|
||||
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
Filter filter, bool playerOnly) {
|
||||
Filter filter, Match match) {
|
||||
if (unit == nullptr)
|
||||
return false;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
@@ -602,7 +688,7 @@ bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
// The descriptor held `descCount` matches; the caller already found fewer
|
||||
// than `oneBasedIndex` there, so the cache supplies index
|
||||
// `oneBasedIndex - descCount` (1-based) of the entries it didn't.
|
||||
const int target = oneBasedIndex - CountSlots(unit, filter, playerOnly);
|
||||
const int target = oneBasedIndex - CountSlots(unit, filter, match);
|
||||
if (target < 1)
|
||||
return false;
|
||||
const uint64_t guid = UnitGuid(unit);
|
||||
@@ -613,7 +699,7 @@ bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
int seen = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!FallbackEligible(unit, buf[i], filter, playerOnly))
|
||||
if (!FallbackEligible(unit, buf[i], filter, match))
|
||||
continue;
|
||||
if (++seen == target) {
|
||||
PushFromCache(L, unit, buf[i], filter == Filter::Helpful);
|
||||
@@ -624,7 +710,7 @@ bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
}
|
||||
|
||||
void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
bool playerOnly, int outerIdx, int &nextKey) {
|
||||
Match match, int outerIdx, int &nextKey) {
|
||||
if (unit == nullptr)
|
||||
return;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
@@ -637,7 +723,7 @@ void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
const int n = Aura::Source::Enumerate(
|
||||
guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!FallbackEligible(unit, buf[i], filter, playerOnly))
|
||||
if (!FallbackEligible(unit, buf[i], filter, match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
PushFromCache(L, unit, buf[i], filter == Filter::Helpful);
|
||||
@@ -668,22 +754,16 @@ uint32_t GroupSpellID(const uint16_t *arr, int slot) {
|
||||
// the engine's per-slot test in Script_UnitBuff/UnitDebuff — a non-zero spell
|
||||
// ID with a real Spell.dbc record that passes the visibility predicate (the
|
||||
// server only transmits *applied* auras, so there is no flag nibble to check).
|
||||
// `playerOnly` additionally requires the `Aura::Source` cache to attribute the
|
||||
// `caster` additionally requires the `Aura::Source` cache to attribute the
|
||||
// cast to the local player (a miss excludes it — we can't confirm the caster
|
||||
// from the group array alone).
|
||||
bool GroupSlotEligible(uint64_t guid, const uint16_t *arr, int slot,
|
||||
bool playerOnly) {
|
||||
Match match) {
|
||||
const uint32_t id = GroupSpellID(arr, slot);
|
||||
if (id == 0 || !IsVisible(SpellRecord(id)))
|
||||
return false;
|
||||
if (playerOnly) {
|
||||
uint64_t c = 0;
|
||||
uint32_t e = 0;
|
||||
uint32_t d = 0;
|
||||
if (!Aura::Source::Get(guid, id, &c, &e, &d) ||
|
||||
c != Unit::Identity::PlayerGuid())
|
||||
return false;
|
||||
}
|
||||
if (!MatchesAura(match, GroupIsPlayerCast(guid, id), id))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -715,13 +795,13 @@ bool GroupArrayHasVisibleAura(const uint16_t *arr) {
|
||||
// aura, not already present in the array, and — when player-filtered — cast by
|
||||
// us. See GroupArrayHasVisibleAura for when this path runs.
|
||||
bool GroupFallbackEligible(const uint16_t *arr, const Aura::Source::CachedAura &c,
|
||||
bool playerOnly) {
|
||||
Match match) {
|
||||
if (!IsVisible(SpellRecord(c.spellId)))
|
||||
return false;
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot)
|
||||
if (GroupSpellID(arr, slot) == c.spellId)
|
||||
return false;
|
||||
if (playerOnly && c.casterGuid != Unit::Identity::PlayerGuid())
|
||||
if (!MatchesAura(match, c.casterGuid == Unit::Identity::PlayerGuid(), c.spellId))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
@@ -729,13 +809,13 @@ bool GroupFallbackEligible(const uint16_t *arr, const Aura::Source::CachedAura &
|
||||
// Nth (1-based) cache-fallback aura of `filter`'s kind for an out-of-range
|
||||
// member whose group array is empty. Pushes the AuraData table on a hit.
|
||||
bool PushNthGroupCacheFallback(void *L, uint64_t guid, const uint16_t *arr,
|
||||
int oneBasedIndex, Filter filter, bool playerOnly) {
|
||||
int oneBasedIndex, Filter filter, Match match) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n =
|
||||
Aura::Source::Enumerate(guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
int seen = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!GroupFallbackEligible(arr, buf[i], playerOnly))
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
continue;
|
||||
if (++seen == oneBasedIndex) {
|
||||
PushEnriched(L, guid, buf[i].spellId, filter == Filter::Helpful, 1,
|
||||
@@ -748,14 +828,14 @@ bool PushNthGroupCacheFallback(void *L, uint64_t guid, const uint16_t *arr,
|
||||
|
||||
// Append every cache-fallback aura of `filter`'s kind into the array table.
|
||||
void AppendGroupCacheFallbacks(void *L, uint64_t guid, const uint16_t *arr,
|
||||
Filter filter, bool playerOnly, int outerIdx,
|
||||
Filter filter, Match match, int outerIdx,
|
||||
int &nextKey) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n =
|
||||
Aura::Source::Enumerate(guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
const int level = GroupMemberLevel(guid);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!GroupFallbackEligible(arr, buf[i], playerOnly))
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
PushEnriched(L, guid, buf[i].spellId, filter == Filter::Helpful, 1, level,
|
||||
@@ -768,7 +848,7 @@ void AppendGroupCacheFallbacks(void *L, uint64_t guid, const uint16_t *arr,
|
||||
// (when non-null), of the given kind. Backs the by-ID / by-name group queries.
|
||||
bool PushGroupCacheFallbackMatch(void *L, uint64_t guid, const uint16_t *arr,
|
||||
bool harmful, uint32_t spellID,
|
||||
const char *spellName, bool playerOnly) {
|
||||
const char *spellName, Match match) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(guid, harmful, buf, kFallbackMax);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
@@ -779,7 +859,7 @@ bool PushGroupCacheFallbackMatch(void *L, uint64_t guid, const uint16_t *arr,
|
||||
if (nm == nullptr || std::strcmp(nm, spellName) != 0)
|
||||
continue;
|
||||
}
|
||||
if (!GroupFallbackEligible(arr, buf[i], playerOnly))
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
continue;
|
||||
PushEnriched(L, guid, buf[i].spellId, !harmful, 1, GroupMemberLevel(guid),
|
||||
false);
|
||||
@@ -791,7 +871,7 @@ bool PushGroupCacheFallbackMatch(void *L, uint64_t guid, const uint16_t *arr,
|
||||
} // namespace
|
||||
|
||||
bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
bool playerOnly) {
|
||||
Match match) {
|
||||
if (guid == 0 || oneBasedIndex < 1)
|
||||
return false;
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
@@ -802,7 +882,7 @@ bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
GroupRange(filter, start, end);
|
||||
int matches = 0;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!GroupSlotEligible(guid, arr, slot, playerOnly))
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
continue;
|
||||
if (++matches == oneBasedIndex) {
|
||||
PushEnriched(L, guid, GroupSpellID(arr, slot),
|
||||
@@ -814,12 +894,12 @@ bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
if (!GroupArrayHasVisibleAura(arr))
|
||||
return PushNthGroupCacheFallback(L, guid, arr, oneBasedIndex, filter,
|
||||
playerOnly);
|
||||
match);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
const Filter *filter, bool playerOnly) {
|
||||
const Filter *filter, Match match) {
|
||||
if (guid == 0 || spellID == 0)
|
||||
return false;
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
@@ -835,7 +915,7 @@ bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (GroupSpellID(arr, slot) != spellID)
|
||||
continue;
|
||||
if (!GroupSlotEligible(guid, arr, slot, playerOnly))
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
continue;
|
||||
PushEnriched(L, guid, spellID, slot < Offsets::UNIT_AURA_BUFF_COUNT, 1,
|
||||
GroupMemberLevel(guid), false);
|
||||
@@ -845,18 +925,18 @@ bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, spellID, nullptr,
|
||||
playerOnly))
|
||||
match))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, spellID, nullptr,
|
||||
playerOnly))
|
||||
match))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
const Filter *filter, bool playerOnly) {
|
||||
const Filter *filter, Match match) {
|
||||
if (guid == 0 || spellName == nullptr || *spellName == '\0')
|
||||
return false;
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
@@ -876,7 +956,7 @@ bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
const char *name = LocalizedSpellName(SpellRecord(id));
|
||||
if (name == nullptr || std::strcmp(name, spellName) != 0)
|
||||
continue;
|
||||
if (!GroupSlotEligible(guid, arr, slot, playerOnly))
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
continue;
|
||||
PushEnriched(L, guid, id, slot < Offsets::UNIT_AURA_BUFF_COUNT, 1,
|
||||
GroupMemberLevel(guid), false);
|
||||
@@ -886,17 +966,17 @@ bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, 0, spellName,
|
||||
playerOnly))
|
||||
match))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, 0, spellName,
|
||||
playerOnly))
|
||||
match))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AppendGroupAuras(void *L, uint64_t guid, Filter filter, bool playerOnly,
|
||||
void AppendGroupAuras(void *L, uint64_t guid, Filter filter, Match match,
|
||||
int outerIdx, int &nextKey) {
|
||||
if (guid == 0)
|
||||
return;
|
||||
@@ -908,7 +988,7 @@ void AppendGroupAuras(void *L, uint64_t guid, Filter filter, bool playerOnly,
|
||||
GroupRange(filter, start, end);
|
||||
const int level = GroupMemberLevel(guid);
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!GroupSlotEligible(guid, arr, slot, playerOnly))
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
PushEnriched(L, guid, GroupSpellID(arr, slot),
|
||||
@@ -917,7 +997,7 @@ void AppendGroupAuras(void *L, uint64_t guid, Filter filter, bool playerOnly,
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
if (!GroupArrayHasVisibleAura(arr))
|
||||
AppendGroupCacheFallbacks(L, guid, arr, filter, playerOnly, outerIdx,
|
||||
AppendGroupCacheFallbacks(L, guid, arr, filter, match, outerIdx,
|
||||
nextKey);
|
||||
}
|
||||
|
||||
|
||||
+54
-9
@@ -33,6 +33,37 @@ namespace Aura::Data {
|
||||
// to helpful when no filter is specified.
|
||||
enum class Filter { Helpful, Harmful };
|
||||
|
||||
// Caster restriction from the `PLAYER` / `!PLAYER` aura filter tokens.
|
||||
// `Any` = no restriction; `PlayerOnly` = only auras the local player cast
|
||||
// (`PLAYER`); `NotPlayer` = only auras NOT cast by the local player
|
||||
// (`!PLAYER`). Caster attribution comes from the `Aura::Source` cache; a
|
||||
// cache miss counts as "not the player" (so `PlayerOnly` excludes it and
|
||||
// `NotPlayer` includes it — matching `IsPlayerCast`'s miss semantics).
|
||||
enum class CasterMode { Any, PlayerOnly, NotPlayer };
|
||||
|
||||
// Restriction from the `DISPELLABLE` / `!DISPELLABLE` aura filter tokens.
|
||||
// `Any` = no restriction; `DispellableOnly` = only auras that can be
|
||||
// dispelled / purged / stolen by SOME mechanism (dispel type Magic, Curse,
|
||||
// Disease, or Poison — the server's DISPEL_ALL_MASK); `NotDispellable` = the
|
||||
// complement. "Regardless of whether the local player can dispel it" —
|
||||
// matching the modern AuraFilters semantics.
|
||||
enum class DispelMode { Any, DispellableOnly, NotDispellable };
|
||||
|
||||
// Restriction from the `CROWD_CONTROL` / `!CROWD_CONTROL` aura filter tokens.
|
||||
// `Any` = no restriction; `CrowdControlOnly` = only auras that are a crowd-
|
||||
// control effect (stun / fear / silence / root / etc., per
|
||||
// `Spell::CrowdControl`); `NotCrowdControl` = the complement.
|
||||
enum class CcMode { Any, CrowdControlOnly, NotCrowdControl };
|
||||
|
||||
// A parsed aura-filter's per-aura predicates (beyond the helpful/harmful
|
||||
// range, which is the separate `Filter`). Extensible: new orthogonal filter
|
||||
// dimensions add a field here rather than another function parameter.
|
||||
struct Match {
|
||||
CasterMode caster = CasterMode::Any;
|
||||
DispelMode dispel = DispelMode::Any;
|
||||
CcMode cc = CcMode::Any;
|
||||
};
|
||||
|
||||
// Reads the spell ID at the given absolute slot (0..47). Returns 0
|
||||
// if the slot is empty or the unit pointer is null.
|
||||
uint32_t ReadSpellID(const uint8_t *unit, int slot);
|
||||
@@ -50,27 +81,41 @@ bool IsSlotPopulated(const uint8_t *unit, int slot);
|
||||
// UNIT_AURA_TOTAL-1 for Harmful. When `playerOnly` is true, only auras
|
||||
// the local player cast (per `IsPlayerCast`) are counted/returned.
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
bool playerOnly = false);
|
||||
Match match = {});
|
||||
|
||||
// Finds the absolute slot of a populated aura with the given spell
|
||||
// ID, optionally restricted to one filter range. `filter` of
|
||||
// nullptr searches both ranges (helpful first, then harmful).
|
||||
// `playerOnly` restricts to player-cast auras. Returns -1 if not found.
|
||||
int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
const Filter *filter, bool playerOnly = false);
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// Finds the absolute slot of a populated aura whose locale-resolved
|
||||
// spell name exactly matches `spellName`. Case-sensitive. Otherwise
|
||||
// same semantics as `FindSlotBySpellID`. Returns -1 if not found
|
||||
// or if `spellName` is null/empty.
|
||||
int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const Filter *filter, bool playerOnly = false);
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// True iff the aura at `slot` was cast by the local player, per the
|
||||
// `Aura::Source` cache. False on a cache miss (caster unknown) — so a
|
||||
// PLAYER-filtered query excludes auras whose cast we didn't observe.
|
||||
bool IsPlayerCast(const uint8_t *unit, int slot);
|
||||
|
||||
// Applies a `CasterMode` to a per-aura "was cast by the local player" answer.
|
||||
// `Any` → always true; `PlayerOnly` → the answer; `NotPlayer` → its negation.
|
||||
bool CasterMatches(CasterMode caster, bool isPlayerCast);
|
||||
|
||||
// True iff the spell's dispel type is one a dispel/purge/steal can remove —
|
||||
// Magic, Curse, Disease, or Poison (`Spell.dbc` Dispel ∈ 1..4). None,
|
||||
// Stealth, Invisibility, Enrage, etc. are not dispellable. Verified against
|
||||
// the server's DISPEL_ALL_MASK.
|
||||
bool IsDispellable(uint32_t spellID);
|
||||
|
||||
// Applies a full `Match` (caster + dispel predicates) to one aura, given the
|
||||
// per-aura "was cast by the local player" answer and its spell ID.
|
||||
bool MatchesAura(const Match &match, bool isPlayerCast, uint32_t spellID);
|
||||
|
||||
// Display stack count for the given slot (engine stores stacks-1,
|
||||
// we add 1). Returns 0 if the unit is null.
|
||||
int ReadStacks(const uint8_t *unit, int slot);
|
||||
@@ -122,7 +167,7 @@ void Push(void *L, const uint8_t *unit, int slot);
|
||||
// helpful/harmful classification matches `filter` and that aren't already in a
|
||||
// populated descriptor slot are considered.
|
||||
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
Filter filter, bool playerOnly = false);
|
||||
Filter filter, Match match = {});
|
||||
|
||||
// Appends every eligible `Aura::Source` cache fallback for `unit` matching
|
||||
// `filter` into the array table at `outerIdx`, continuing from `nextKey`
|
||||
@@ -130,7 +175,7 @@ bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
// for `GetUnitAuras`. Skips entries already present in a populated descriptor
|
||||
// slot so it never double-lists.
|
||||
void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
bool playerOnly, int outerIdx, int &nextKey);
|
||||
Match match, int outerIdx, int &nextKey);
|
||||
|
||||
// ── Out-of-range groupmate path ────────────────────────────────────────────
|
||||
//
|
||||
@@ -154,23 +199,23 @@ void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
// matching `filter`. On a hit pushes the table and returns true; otherwise
|
||||
// pushes nothing and returns false (caller pushes nil).
|
||||
bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
bool playerOnly = false);
|
||||
Match match = {});
|
||||
|
||||
// Pushes AuraData for the group-array aura with `spellID` on `guid`, optionally
|
||||
// restricted to one filter range (nullptr = both, helpful first). Returns true
|
||||
// on a hit (table pushed), false otherwise (nothing pushed).
|
||||
bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
const Filter *filter, bool playerOnly = false);
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// As `PushGroupAuraBySpellID` but matched by locale-resolved spell name
|
||||
// (case-sensitive exact match), mirroring `FindSlotBySpellName`.
|
||||
bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
const Filter *filter, bool playerOnly = false);
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// Appends every group-array aura on `guid` matching `filter` into the array
|
||||
// table at `outerIdx`, continuing from `nextKey` (updated in place). The
|
||||
// bulk-enumeration analog of `PushNthGroupAura`, for `GetUnitAuras`.
|
||||
void AppendGroupAuras(void *L, uint64_t guid, Filter filter, bool playerOnly,
|
||||
void AppendGroupAuras(void *L, uint64_t guid, Filter filter, Match match,
|
||||
int outerIdx, int &nextKey);
|
||||
|
||||
} // namespace Aura::Data
|
||||
|
||||
+112
-12
@@ -130,13 +130,70 @@ 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;
|
||||
using ResolveByGuid_t = void *(__fastcall *)(int typeMask, const char *dbg,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi, int priority);
|
||||
constexpr int kUnitOrPlayerMask =
|
||||
(1 << Offsets::OBJECT_TYPE_UNIT) | (1 << Offsets::OBJECT_TYPE_PLAYER);
|
||||
auto resolve = reinterpret_cast<ResolveByGuid_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
|
||||
const auto *unit = static_cast<const uint8_t *>(
|
||||
resolve(kUnitOrPlayerMask, nullptr, static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0));
|
||||
if (unit == nullptr)
|
||||
return false;
|
||||
const auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_CGUNIT_OBJECT_FIELDS);
|
||||
if (desc == nullptr)
|
||||
return false;
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot) {
|
||||
if (*reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_AURA + slot * 4) == spellId)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// `fromCast` true: the SpellGo hook — authoritative caster + caster-modified
|
||||
// (talented) timing. False: the OnAuraAdded application hook — timing only,
|
||||
@@ -149,9 +206,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 +226,44 @@ void Store(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Take a free slot, else an expired one, else evict round-robin.
|
||||
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 ---------------------------
|
||||
@@ -534,8 +619,18 @@ void OnWorldTick() {
|
||||
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
if (e.used && e.expirationMs != 0 && now >= e.expirationMs)
|
||||
e.used = false;
|
||||
if (!e.used || e.expirationMs == 0 || now < e.expirationMs)
|
||||
continue;
|
||||
// A timer elapse doesn't prove the aura is gone — expirationMs is only
|
||||
// a base-duration estimate for non-player casters. Keep any entry whose
|
||||
// aura the owning unit's descriptor still lists (descriptor = presence
|
||||
// authority in view; caster is immutable for the aura's life). Only
|
||||
// genuinely orphaned entries (unit out of range / stealthed / despawned
|
||||
// → no descriptor backing) are timer-reclaimed; the fallback path
|
||||
// already skips expired entries, so this can't resurface a phantom.
|
||||
if (DescriptorListsAura(e.targetGuid, e.spellId))
|
||||
continue;
|
||||
e.used = false;
|
||||
}
|
||||
// Forget snapshots for members we haven't polled recently (left the group,
|
||||
// or no longer displayed) so a later re-appearance re-baselines cleanly.
|
||||
@@ -775,9 +870,14 @@ bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
|
||||
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) {
|
||||
|
||||
+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));
|
||||
}
|
||||
|
||||
|
||||
@@ -410,6 +410,56 @@ void CallEngineMouseover(uint64_t guid) {
|
||||
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32), 0, 0);
|
||||
}
|
||||
|
||||
// True when the GUID resolves to a live unit/player object (loaded, in range).
|
||||
// False for an offline or far out-of-range party/raid member — the case the
|
||||
// engine mouseover setter skips the tooltip for. Same resolver Unit::Mouseover
|
||||
// uses for its unit test.
|
||||
bool GuidHasLiveUnit(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return false;
|
||||
constexpr int kUnitOrPlayerMask =
|
||||
(1 << Offsets::OBJECT_TYPE_UNIT) | (1 << Offsets::OBJECT_TYPE_PLAYER);
|
||||
auto resolve = reinterpret_cast<void *(__fastcall *)(int, const char *, uint32_t,
|
||||
uint32_t, int)>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
|
||||
return resolve(kUnitOrPlayerMask, "ClassicAPI",
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0) != nullptr;
|
||||
}
|
||||
|
||||
// Build the mouseover tooltip for a unit with no live object, straight from the
|
||||
// party/raid roster. FUN_00529FE0 is the same unit-tooltip builder SetUnit and
|
||||
// the engine mouseover use; with no object it fills name/level/class + the
|
||||
// "Offline" line from the roster (verified: `GameTooltip:SetUnit("party1")`
|
||||
// works for an offline member). The engine's setter (FUN_00492890) gates this
|
||||
// call on a live object, so offline members get nothing — we do it here.
|
||||
void BuildRosterUnitTooltip(uint64_t guid) {
|
||||
void *tooltip = *reinterpret_cast<void *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_GAMETOOLTIP_OBJECT_PTR));
|
||||
if (tooltip == nullptr)
|
||||
return;
|
||||
|
||||
// Fire OnTooltipSetDefaultAnchor first, exactly as the engine mouseover
|
||||
// setter (FUN_00492890) does before it builds — FrameXML's handler sets
|
||||
// the tooltip's owner + anchor, which is what actually shows it. Without
|
||||
// this, a rebuild after the tooltip was hidden on the previous leave has
|
||||
// no owner and never re-appears (only a subsequent live-unit hover, which
|
||||
// takes the engine's own anchor path, would). Use the self-contained
|
||||
// invoker FUN_FRAME_INVOKE_SCRIPT, safe to call from the WorldTick.
|
||||
const int anchorHandler = *reinterpret_cast<const int *>(
|
||||
reinterpret_cast<uint8_t *>(tooltip) +
|
||||
Offsets::OFF_TOOLTIP_SET_DEFAULT_ANCHOR_HANDLER);
|
||||
if (anchorHandler != 0)
|
||||
reinterpret_cast<void(__fastcall *)(int, void *)>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FRAME_INVOKE_SCRIPT))(anchorHandler,
|
||||
tooltip);
|
||||
|
||||
uint32_t packed[2] = {static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32)};
|
||||
reinterpret_cast<void(__thiscall *)(void *, uint32_t *)>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GAMETOOLTIP_BUILD_UNIT))(tooltip, packed);
|
||||
}
|
||||
|
||||
void MouseoverTick() {
|
||||
const void *focus = CurrentMouseFocus();
|
||||
|
||||
@@ -423,6 +473,11 @@ void MouseoverTick() {
|
||||
|
||||
if (guid != g_lastSet) {
|
||||
CallEngineMouseover(guid); // sets slot + highlight + tooltip + event
|
||||
// Offline / far out-of-range party or raid member: the setter wrote the
|
||||
// GUID slot but skipped the tooltip (no live object). Build it from the
|
||||
// roster so hovering the frame still shows name / level / "Offline".
|
||||
if (guid != 0 && !GuidHasLiveUnit(guid))
|
||||
BuildRosterUnitTooltip(guid);
|
||||
g_lastSet = guid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
// 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_Macro.CreateMacro` / `C_Macro.EditMacro` — create/edit macros whose
|
||||
// icon is given as a texture STRING (a full `Interface\Icons\<name>` path
|
||||
// or a bare `<name>` basename), not a `GetMacroIconInfo` index.
|
||||
//
|
||||
// C_Macro.CreateMacro(name, iconTexture, body, isCharacterMacro) -> index
|
||||
// C_Macro.EditMacro(index, name, iconTexture, body) -> index
|
||||
//
|
||||
// Why this exists: vanilla `CreateMacro`/`EditMacro` take an icon INDEX
|
||||
// into the `GetMacroIconInfo` list, and that list omits `INV_*` item
|
||||
// icons entirely — so item icons are unreachable through the stock API
|
||||
// even though the engine stores and renders them fine. The macro struct
|
||||
// keeps the icon as a bare basename at `entry + OFF_MACRO_ICON` and
|
||||
// `GetMacroInfo` reads it back as `Interface\Icons\%s`, so ANY texture
|
||||
// living under `Interface\Icons\` round-trips once you can write the
|
||||
// basename directly. These two functions do exactly that, unblocking a
|
||||
// pfUI macro-UI remodel that wants arbitrary icons.
|
||||
//
|
||||
// The legacy globals `CreateMacro`/`EditMacro` are left untouched
|
||||
// (index-only) — no overloading; string-icon callers opt in via the
|
||||
// `C_Macro` namespace.
|
||||
//
|
||||
// Implementation: both go straight to the engine's own workers
|
||||
// (`FUN_MACRO_CREATE` / `FUN_MACRO_EDIT`, see [[src/Offsets.h]]), which do
|
||||
// the bounded SStrCopy into the struct, re-run the primary-spell parser,
|
||||
// set the persist-on-logout dirty flag, and refresh the action bars.
|
||||
// We only translate the icon string to a basename and, for edits,
|
||||
// preserve the entry's `local` flag (the edit worker rewrites it
|
||||
// unconditionally).
|
||||
//
|
||||
// Icon input is STRING-only by design (that's the whole point — item
|
||||
// icons have no index). A numeric icon arg is treated as "no icon" on
|
||||
// create and "leave unchanged" on edit; callers who want the numeric
|
||||
// index path already have the legacy globals.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Macro::Edit {
|
||||
|
||||
namespace {
|
||||
|
||||
using MacroCreate_t = uint32_t(__fastcall *)(const char *name,
|
||||
const char *iconBasename,
|
||||
const char *body, uint32_t local,
|
||||
int perCharacter);
|
||||
using MacroEdit_t = void(__fastcall *)(uint32_t macroID, const char *name,
|
||||
const char *iconBasename,
|
||||
const char *body, uint32_t flag);
|
||||
using MacroIdToSlot_t = uint32_t(__fastcall *)(uint32_t macroID);
|
||||
using MacroNameToSlot_t = uint32_t(__fastcall *)(const char *name);
|
||||
using MacroSlotToEntry_t = uint8_t *(__fastcall *)(uint32_t slot0Based);
|
||||
|
||||
constexpr uint32_t kSlotMiss = 0xFFFFFFFFu;
|
||||
|
||||
// Last path component of `s` (after the final `\` or `/`). Turns
|
||||
// "Interface\\Icons\\INV_Sword_25" and a bare "INV_Sword_25" alike into
|
||||
// the basename the macro struct stores; the engine re-prefixes it with
|
||||
// `Interface\Icons\` on read. Aliases into `s` — no copy.
|
||||
const char *IconBasename(const char *s) {
|
||||
const char *bn = s;
|
||||
for (const char *p = s; *p != '\0'; ++p) {
|
||||
if (*p == '\\' || *p == '/')
|
||||
bn = p + 1;
|
||||
}
|
||||
return bn;
|
||||
}
|
||||
|
||||
// The icon to store, or nullptr (create: no icon; edit: leave unchanged).
|
||||
// String-only — see the file header.
|
||||
const char *IconArg(void *L, int idx) {
|
||||
if (!Game::Lua::IsString(L, idx))
|
||||
return nullptr;
|
||||
return IconBasename(Game::Lua::ToString(L, idx));
|
||||
}
|
||||
|
||||
// A body/name string arg, or nullptr when absent (edit: leave unchanged).
|
||||
const char *OptString(void *L, int idx) {
|
||||
return Game::Lua::IsString(L, idx) ? Game::Lua::ToString(L, idx) : nullptr;
|
||||
}
|
||||
|
||||
uint8_t *EntryForSlot(uint32_t slot0) {
|
||||
auto fn = reinterpret_cast<MacroSlotToEntry_t>(
|
||||
Offsets::FUN_MACRO_SLOT_TO_ENTRY);
|
||||
return fn(slot0);
|
||||
}
|
||||
|
||||
// C_Macro.CreateMacro(name, iconTexture, body, isCharacterMacro) -> index
|
||||
// Returns the new macro's 1-based slot, or nil when creation fails (empty
|
||||
// name, or the per-scope 18-macro list is full).
|
||||
int __fastcall Script_CreateMacro(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(
|
||||
L, "Usage: C_Macro.CreateMacro(name, iconTexture, body, "
|
||||
"isCharacterMacro)");
|
||||
return 0;
|
||||
}
|
||||
const char *name = Game::Lua::ToString(L, 1);
|
||||
const char *icon = IconArg(L, 2);
|
||||
const char *body = OptString(L, 3);
|
||||
const int perCharacter = Game::Lua::ToBoolean(L, 4);
|
||||
|
||||
auto create = reinterpret_cast<MacroCreate_t>(Offsets::FUN_MACRO_CREATE);
|
||||
const uint32_t macroID = create(name, icon, body, /*local*/ 0, perCharacter);
|
||||
if (macroID == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto idToSlot =
|
||||
reinterpret_cast<MacroIdToSlot_t>(Offsets::FUN_MACRO_ID_TO_SLOT);
|
||||
const uint32_t slot0 = idToSlot(macroID);
|
||||
if (slot0 == kSlotMiss) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot0 + 1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// C_Macro.EditMacro(index, name, iconTexture, body) -> index
|
||||
// `index` is a 1-based slot number or a macro name. Any of name /
|
||||
// iconTexture / body left nil is unchanged. Returns the macro's 1-based
|
||||
// slot, or nil when `index` doesn't resolve to an existing macro.
|
||||
int __fastcall Script_EditMacro(void *L) {
|
||||
uint32_t slot0;
|
||||
if (Game::Lua::IsNumber(L, 1)) {
|
||||
slot0 = static_cast<uint32_t>(static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1);
|
||||
} else if (Game::Lua::IsString(L, 1)) {
|
||||
auto nameToSlot = reinterpret_cast<MacroNameToSlot_t>(
|
||||
Offsets::FUN_MACRO_NAME_TO_SLOT);
|
||||
slot0 = nameToSlot(Game::Lua::ToString(L, 1));
|
||||
} else {
|
||||
Game::Lua::Error(
|
||||
L, "Usage: C_Macro.EditMacro(index, name, iconTexture, body)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// One validation covers both a bad slot number and a name miss:
|
||||
// FUN_MACRO_NAME_TO_SLOT returns 0xFFFFFFFF on miss, which the slot
|
||||
// resolver bounds-rejects to NULL just like an out-of-range index.
|
||||
uint8_t *entry = EntryForSlot(slot0);
|
||||
if (entry == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint32_t macroID = *reinterpret_cast<const uint32_t *>(entry);
|
||||
// The edit worker rewrites entry+OFF_MACRO_LOCAL_FLAG unconditionally;
|
||||
// read the current value and hand it back so an edit that only touches
|
||||
// name/icon/body leaves the flag genuinely unchanged.
|
||||
const uint32_t flag = *reinterpret_cast<const uint32_t *>(
|
||||
entry + Offsets::OFF_MACRO_LOCAL_FLAG);
|
||||
|
||||
const char *name = OptString(L, 2);
|
||||
const char *icon = IconArg(L, 3);
|
||||
const char *body = OptString(L, 4);
|
||||
|
||||
auto edit = reinterpret_cast<MacroEdit_t>(Offsets::FUN_MACRO_EDIT);
|
||||
edit(macroID, name, icon, body, flag);
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot0 + 1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Macro", "CreateMacro",
|
||||
&Script_CreateMacro);
|
||||
Game::Lua::RegisterTableFunction("C_Macro", "EditMacro", &Script_EditMacro);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Macro::Edit
|
||||
+1
-2
@@ -46,7 +46,6 @@ namespace Macro::Spell {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int OFF_SPELL_RANK = 0x204; // 9-locale char *[9] in Spell.dbc
|
||||
|
||||
using MacroSlotToEntry_t = const uint8_t *(__fastcall *)(unsigned slot0Based);
|
||||
|
||||
@@ -88,7 +87,7 @@ int __fastcall Script_GetMacroSpell(void *L) {
|
||||
if (name == nullptr)
|
||||
return 0;
|
||||
const char *rank = *reinterpret_cast<const char *const *>(
|
||||
record + OFF_SPELL_RANK + locale * 4);
|
||||
record + Offsets::OFF_SPELL_RECORD_RANK + locale * 4);
|
||||
|
||||
Game::Lua::PushString(L, name);
|
||||
// Rank slot is empty for most non-class spells (racials, procs,
|
||||
|
||||
+7
-1
@@ -27,7 +27,13 @@ using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
// 0xC) and returns true on first match. Mirrors the engine's loop in
|
||||
// `Script_IsUnitOnQuest` (`0x004DFE10`).
|
||||
bool UnitHasQuest(const uint8_t *unit, int target) {
|
||||
if (target <= 0 || !Unit::Flags::IsPlayerControlled(unit))
|
||||
// The quest list lives in the CGPlayer sub-struct at +0xE68, which
|
||||
// only exists on real player objects. 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 deref garbage and crash (same root cause
|
||||
// as pfUI issue #34).
|
||||
if (target <= 0 || !Unit::Flags::IsPlayerObject(unit))
|
||||
return false;
|
||||
|
||||
auto *info = *reinterpret_cast<const uint8_t *const *>(
|
||||
|
||||
+10
-12
@@ -19,13 +19,13 @@
|
||||
// vs. on-next-swing abilities like Heroic Strike or Maul. We test
|
||||
// against the engine constant directly.
|
||||
//
|
||||
// Ranged — both vanilla ranged auto-attacks (`75` Auto Shot, `5019`
|
||||
// Shoot wand) set bit 1 of `Spell.dbc.Attributes` (the
|
||||
// `SPELL_ATTR_AUTO_REPEAT` flag at value `0x02`). Verified empirically
|
||||
// against the in-game cache: Attr `0x00050012` (Auto Shot) and
|
||||
// `0x00000012` (Shoot) share that bit; on-next-swing Heroic Strike
|
||||
// (`0x00050014`) and other ranged spells like Aimed Shot / Fireball do
|
||||
// NOT. The attribute check naturally extends to any future
|
||||
// Ranged — the two vanilla ranged auto-attacks (`75` Auto Shot, `5019`
|
||||
// Shoot wand) are exactly the spells carrying SPELL_ATTR_EX2_AUTOREPEAT_FLAG
|
||||
// (AttributesEx2 bit 5, `0x20`). Verified from Spell.dbc: Auto Shot and
|
||||
// Shoot have Ex2 `0x20`; the on-cast ranged abilities (Aimed Shot 19434,
|
||||
// Multi-Shot 2643) do NOT — they only set the generic RANGED bit
|
||||
// (Attributes bit 1), which is why an earlier RANGED test wrongly matched
|
||||
// them. The AUTOREPEAT flag is the exact signal and extends to any future
|
||||
// auto-repeating spell a private server might add.
|
||||
|
||||
#include "Lookup.h"
|
||||
@@ -42,8 +42,6 @@ namespace Spell::AutoAttack {
|
||||
namespace {
|
||||
|
||||
constexpr int SPELL_AUTO_ATTACK = 6603;
|
||||
constexpr int OFF_SPELL_ATTRIBUTES = 0x18;
|
||||
constexpr uint32_t SPELL_ATTR_AUTO_REPEAT = 0x00000002;
|
||||
|
||||
bool IsMelee(int spellID) {
|
||||
return spellID == SPELL_AUTO_ATTACK;
|
||||
@@ -57,9 +55,9 @@ bool IsRanged(int spellID) {
|
||||
static_cast<uint32_t>(spellID));
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
const uint32_t attr = *reinterpret_cast<const uint32_t *>(
|
||||
rec + OFF_SPELL_ATTRIBUTES);
|
||||
return (attr & SPELL_ATTR_AUTO_REPEAT) != 0;
|
||||
const uint32_t attrEx2 = *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
return (attrEx2 & Offsets::SPELL_ATTR_EX2_AUTOREPEAT_FLAG) != 0;
|
||||
}
|
||||
|
||||
int ReadSpellID(void *L) {
|
||||
|
||||
+10
-20
@@ -89,13 +89,6 @@ using TickMs_t = uint32_t(__fastcall *)();
|
||||
using GetCastTime_t = uint32_t(__fastcall *)(int spellID, int unit, int flag);
|
||||
using GetDuration_t = int(__fastcall *)(const uint8_t *spellRecord, int unit, int skipMod);
|
||||
|
||||
// Spell.dbc record field offsets (mirrors Spell::Info's locals).
|
||||
constexpr int OFF_NAME = 0x1E0; // localized name[9]
|
||||
constexpr int OFF_ICON_ID = 0x1D4; // -> SpellIcon.dbc
|
||||
constexpr int OFF_ATTRIBUTES = 0x18; // u32 Attributes flags
|
||||
constexpr int OFF_ATTRIBUTES_EX2 = 0x24; // u32 AttributesEx2 flags
|
||||
constexpr int OFF_SUBRECORD_VALUE = 0x04; // SpellIcon path field
|
||||
|
||||
// SPELL_ATTR_TRADESPELL — set on profession recipe casts (enchant,
|
||||
// cooking with a cast, etc.). The version-stable `isTradeskill` source:
|
||||
// 3.3.5's Script_UnitCastingInfo reads bit 0x20 of Attributes (at +0x10
|
||||
@@ -106,7 +99,6 @@ constexpr uint32_t SPELL_ATTR_TRADESPELL = 0x20;
|
||||
// flat +500ms "aim time" to these (SpellEntry::GetCastTime). Auto-repeat
|
||||
// shots (Auto Shot / Shoot, AttributesEx2 bit 0x20) are the one exclusion.
|
||||
constexpr uint32_t SPELL_ATTR_RANGED = 0x2;
|
||||
constexpr uint32_t SPELL_ATTR_EX2_AUTOREPEAT = 0x20;
|
||||
|
||||
struct TrackedSpell {
|
||||
int spellID; // 0 = not casting / channeling
|
||||
@@ -188,10 +180,10 @@ int CastTimeMs(int spellID) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec != nullptr) {
|
||||
const uint32_t attr =
|
||||
*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES);
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
const uint32_t attrEx2 =
|
||||
*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES_EX2);
|
||||
if ((attr & SPELL_ATTR_RANGED) && !(attrEx2 & SPELL_ATTR_EX2_AUTOREPEAT))
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
if ((attr & SPELL_ATTR_RANGED) && !(attrEx2 & Offsets::SPELL_ATTR_EX2_AUTOREPEAT_FLAG))
|
||||
ms += 500;
|
||||
}
|
||||
}
|
||||
@@ -212,24 +204,25 @@ int ChannelDurationMs(int spellID) {
|
||||
|
||||
const char *SpellName(const uint8_t *rec) {
|
||||
const int locale = *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
return *reinterpret_cast<const char *const *>(rec + OFF_NAME + locale * 4);
|
||||
return *reinterpret_cast<const char *const *>(rec + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
}
|
||||
|
||||
bool IsTradeskill(const uint8_t *rec) {
|
||||
return (*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES) & SPELL_ATTR_TRADESPELL) != 0;
|
||||
return (*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
|
||||
SPELL_ATTR_TRADESPELL) != 0;
|
||||
}
|
||||
|
||||
// Spell icon texture path, or "" if there's no icon record. SpellIcon.dbc
|
||||
// already stores the full "Interface\Icons\..." path (unlike item icons,
|
||||
// which are bare filenames) — so it's used verbatim, no prefix.
|
||||
const char *SpellIconPath(const uint8_t *rec) {
|
||||
const int iconID = *reinterpret_cast<const int *>(rec + OFF_ICON_ID);
|
||||
const int iconID = *reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
const uint8_t *iconRec = DBC::Record(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT,
|
||||
static_cast<uint32_t>(iconID));
|
||||
if (iconRec == nullptr)
|
||||
return "";
|
||||
const char *path = *reinterpret_cast<const char *const *>(iconRec + OFF_SUBRECORD_VALUE);
|
||||
const char *path = *reinterpret_cast<const char *const *>(iconRec + Offsets::OFF_SPELLICON_PATH);
|
||||
return (path != nullptr) ? path : "";
|
||||
}
|
||||
|
||||
@@ -340,12 +333,9 @@ const Game::HookAutoRegister _castStartHook{
|
||||
// thing at a time). Regular casts back `UnitCastingInfo`; channels add real
|
||||
// times to `UnitChannelInfo` (validated against the live +0x228 field).
|
||||
|
||||
constexpr int OFF_ATTRIBUTES_EX = 0x1C; // Spell.dbc AttributesEx
|
||||
constexpr uint32_t SPELL_ATTR_EX_CHANNELED = 0x4 | 0x40; // IS_CHANNELED | SELF
|
||||
|
||||
bool IsChannelSpell(const uint8_t *rec) {
|
||||
return (*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES_EX) &
|
||||
SPELL_ATTR_EX_CHANNELED) != 0;
|
||||
return (*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX) &
|
||||
Offsets::SPELL_ATTR_EX_CHANNELED) != 0;
|
||||
}
|
||||
|
||||
// Channel duration for a non-player caster — base (skipMod=1), since we
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
namespace Spell::CastByID {
|
||||
|
||||
// Offset of the locale-resolved Name[9] table inside a Spell.dbc record.
|
||||
static constexpr int OFF_NAME = 0x1E0;
|
||||
|
||||
using NameToSlot_t = int(__fastcall *)(const char *name, void *out);
|
||||
static NameToSlot_t NameToSlot_o = nullptr;
|
||||
@@ -48,7 +46,7 @@ static const char *LocaleNameForID(int spellID) {
|
||||
return nullptr;
|
||||
const int locale = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
return *reinterpret_cast<const char *const *>(record + OFF_NAME + locale * 4);
|
||||
return *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
}
|
||||
|
||||
// Hook on the single name → spellbook-slot resolver
|
||||
|
||||
+10
-17
@@ -29,9 +29,6 @@ namespace Spell::CastEvents {
|
||||
|
||||
namespace {
|
||||
|
||||
// Spell.dbc localized string arrays (9 locale slots, 4 bytes each).
|
||||
constexpr int OFF_NAME = 0x1E0; // Name[9]
|
||||
constexpr int OFF_RANK = 0x204; // Rank[9]
|
||||
|
||||
constexpr const char *kStart = "UNIT_SPELLCAST_START";
|
||||
constexpr const char *kStop = "UNIT_SPELLCAST_STOP";
|
||||
@@ -120,8 +117,8 @@ void Fire(const char *eventName, int spellID, int guidNum, int type = 3) {
|
||||
char castGuid[48];
|
||||
BuildCastGuid(castGuid, sizeof(castGuid), type, spellID, guidNum);
|
||||
Event::Custom::Fire(slot, "%s%s%d%s%s", "player", castGuid, spellID,
|
||||
LocalizedField(rec, OFF_NAME),
|
||||
LocalizedField(rec, OFF_RANK));
|
||||
LocalizedField(rec, Offsets::OFF_SPELL_NAMES),
|
||||
LocalizedField(rec, Offsets::OFF_SPELL_RECORD_RANK));
|
||||
}
|
||||
|
||||
// SENT is the one event whose shape differs from the rest, matching modern:
|
||||
@@ -138,8 +135,8 @@ void FireSent(int spellID, int guidNum, const char *target) {
|
||||
char castGuid[48];
|
||||
BuildCastGuid(castGuid, sizeof(castGuid), /*type=*/3, spellID, guidNum);
|
||||
Event::Custom::Fire(slot, "%s%s%s%d%s%s", "player", target ? target : "",
|
||||
castGuid, spellID, LocalizedField(rec, OFF_NAME),
|
||||
LocalizedField(rec, OFF_RANK));
|
||||
castGuid, spellID, LocalizedField(rec, Offsets::OFF_SPELL_NAMES),
|
||||
LocalizedField(rec, Offsets::OFF_SPELL_RECORD_RANK));
|
||||
}
|
||||
|
||||
// Fire a RETICLE event `(unitTarget, castGUID, spellID, spellName, rank)`.
|
||||
@@ -158,8 +155,8 @@ void FireReticle(const char *eventName, int spellID) {
|
||||
if (rec == nullptr)
|
||||
return;
|
||||
Event::Custom::Fire(slot, "%s%s%d%s%s", "player", "", spellID,
|
||||
LocalizedField(rec, OFF_NAME),
|
||||
LocalizedField(rec, OFF_RANK));
|
||||
LocalizedField(rec, Offsets::OFF_SPELL_NAMES),
|
||||
LocalizedField(rec, Offsets::OFF_SPELL_RECORD_RANK));
|
||||
}
|
||||
|
||||
// spellID whose reticle we last reported active (0 = no reticle up).
|
||||
@@ -257,15 +254,11 @@ int g_pendingChanSuccSpell = 0;
|
||||
int g_pendingChanSuccTMs = 0;
|
||||
constexpr int kChanSuccDeferMs = 500;
|
||||
|
||||
// Spell.dbc AttributesEx channel bits — same test Spell::Cast uses.
|
||||
constexpr int OFF_ATTRIBUTES_EX = 0x1C;
|
||||
constexpr uint32_t SPELL_ATTR_EX_CHANNELED = 0x4 | 0x40; // IS_CHANNELED | SELF
|
||||
|
||||
bool IsChanneledSpell(int spellID) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
return rec != nullptr &&
|
||||
(*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES_EX) &
|
||||
SPELL_ATTR_EX_CHANNELED) != 0;
|
||||
(*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX) &
|
||||
Offsets::SPELL_ATTR_EX_CHANNELED) != 0;
|
||||
}
|
||||
|
||||
int NowMs() {
|
||||
@@ -390,8 +383,8 @@ void FireRemote(const char *eventName, uint64_t casterGuid, int spellID,
|
||||
return;
|
||||
char castGuid[48];
|
||||
BuildCastGuid(castGuid, sizeof(castGuid), /*type=*/3, spellID, guidNum);
|
||||
const char *name = LocalizedField(rec, OFF_NAME);
|
||||
const char *rank = LocalizedField(rec, OFF_RANK);
|
||||
const char *name = LocalizedField(rec, Offsets::OFF_SPELL_NAMES);
|
||||
const char *rank = LocalizedField(rec, Offsets::OFF_SPELL_RECORD_RANK);
|
||||
for (int i = 0; i < n; ++i)
|
||||
Event::Custom::Fire(slot, "%s%s%d%s%s", tokens[i], castGuid, spellID,
|
||||
name, rank);
|
||||
|
||||
@@ -52,10 +52,6 @@ namespace Spell::CastNoToggle {
|
||||
|
||||
namespace {
|
||||
|
||||
// Offset of the locale-resolved Name[9] table inside a Spell.dbc record
|
||||
// (same value used by `Spell::Info`; duplicated here to keep this module
|
||||
// header-free).
|
||||
constexpr int OFF_NAME = 0x1E0;
|
||||
|
||||
using CastSpellByName_t = int(__fastcall *)(void *L);
|
||||
using NameToSlot_t = int(__fastcall *)(const char *name, int *outBookType);
|
||||
@@ -75,7 +71,7 @@ const char *LocaleName(int spellID) {
|
||||
return nullptr;
|
||||
const int locale = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
return *reinterpret_cast<const char *const *>(record + OFF_NAME + locale * 4);
|
||||
return *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
}
|
||||
|
||||
// Case-insensitive name match for the auto-repeat-name gate. Walks
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "spell/CrowdControl.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "spell/Lookup.h"
|
||||
|
||||
namespace Spell::CrowdControl {
|
||||
|
||||
const char *Classify(const uint8_t *rec) {
|
||||
if (rec == nullptr)
|
||||
return nullptr;
|
||||
const int32_t *aura = reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
auto has = [&](int type) {
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i)
|
||||
if (aura[i] == type)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
if (has(Offsets::SPELL_AURA_MOD_POSSESS)) return "POSSESS";
|
||||
if (has(Offsets::SPELL_AURA_MOD_CHARM)) return "CHARM";
|
||||
if (has(Offsets::SPELL_AURA_MOD_STUN)) return "STUN";
|
||||
if (has(Offsets::SPELL_AURA_MOD_FEAR)) return "FEAR";
|
||||
if (has(Offsets::SPELL_AURA_MOD_CONFUSE)) return "CONFUSE";
|
||||
if (has(Offsets::SPELL_AURA_MOD_PACIFY_SILENCE)) return "PACIFYSILENCE";
|
||||
if (has(Offsets::SPELL_AURA_MOD_SILENCE)) return "SILENCE";
|
||||
if (has(Offsets::SPELL_AURA_MOD_PACIFY)) return "PACIFY";
|
||||
if (has(Offsets::SPELL_AURA_MOD_ROOT)) return "ROOT";
|
||||
if (has(Offsets::SPELL_AURA_MOD_DISARM)) return "DISARM";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsCrowdControl(uint32_t spellID) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellID));
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
// Crowd control is broader than loss of control: it also counts movement
|
||||
// slows / snares, which `Classify` (the LoC set) deliberately excludes.
|
||||
if (Classify(rec) != nullptr)
|
||||
return true;
|
||||
const int32_t *aura = reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i)
|
||||
if (aura[i] == Offsets::SPELL_AURA_MOD_DECREASE_SPEED)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Spell::CrowdControl
|
||||
@@ -0,0 +1,40 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Crowd-control classification of a Spell.dbc record, shared by
|
||||
// `C_LossOfControl` (which reports the control-loss type) and the
|
||||
// `C_UnitAuras` `CROWD_CONTROL` aura filter (which only needs presence). One
|
||||
// source of truth for "is this a hard control effect" so the two never drift.
|
||||
|
||||
namespace Spell::CrowdControl {
|
||||
|
||||
// Classifies a spell by its `EffectApplyAuraName` set into a control-loss
|
||||
// type string, priority-ordered (strongest control first) so a multi-effect
|
||||
// spell resolves to one type:
|
||||
// POSSESS, CHARM, STUN, FEAR, CONFUSE, PACIFYSILENCE, SILENCE, PACIFY,
|
||||
// ROOT, DISARM
|
||||
// Returns nullptr if the spell applies no control-loss aura (`spellRecord`
|
||||
// may be null → nullptr). Movement-only slows/snares are intentionally NOT
|
||||
// classified — they are not a loss of control.
|
||||
const char *Classify(const uint8_t *spellRecord);
|
||||
|
||||
// True iff `spellID` is a crowd-control aura. Broader than `Classify`: it
|
||||
// also counts movement slows / snares (`MOD_DECREASE_SPEED`), which are crowd
|
||||
// control but not loss of control. Backs the C_UnitAuras CROWD_CONTROL filter.
|
||||
bool IsCrowdControl(uint32_t spellID);
|
||||
|
||||
} // namespace Spell::CrowdControl
|
||||
+28
-39
@@ -22,24 +22,14 @@
|
||||
|
||||
namespace Spell::Info {
|
||||
|
||||
// Spell.dbc record offsets (from BuildSpellTooltip / Script_GetSpellName / Script_GetSpellTexture).
|
||||
static constexpr int OFF_ATTRIBUTES_EX = 0x1C;
|
||||
static constexpr int OFF_CASTING_TIME_INDEX = 0x48;
|
||||
static constexpr int OFF_POWER_TYPE = 0x7C;
|
||||
static constexpr int OFF_MANA_COST = 0x80;
|
||||
static constexpr int OFF_RANGE_INDEX = 0x90;
|
||||
static constexpr int OFF_ICON_ID = 0x1D4;
|
||||
static constexpr int OFF_NAME = 0x1E0;
|
||||
static constexpr int OFF_RANK = 0x204;
|
||||
|
||||
// Spell.dbc Attributes (+0x18) and AttributesEx (+0x1C) flag bits we read.
|
||||
// Both bits are 0x40, but on different fields:
|
||||
// Attributes bit 6 = SPELL_ATTR_PASSIVE — passive spell (no cast bar,
|
||||
// applies its effect as soon as learned/equipped)
|
||||
// AttributesEx bit 6 = SPELL_ATTR_EX_FUNNEL_PERCENT — funnel channel
|
||||
static constexpr int OFF_ATTRIBUTES = 0x18;
|
||||
static constexpr uint32_t SPELL_ATTR_PASSIVE = 0x40;
|
||||
static constexpr uint32_t SPELL_ATTR_EX_FUNNEL = 0x40;
|
||||
// GetSpellInfo's isFunnel means a health-funnel spell — AttributesEx2 (+0x20)
|
||||
// bit 11 = SPELL_ATTR_EX2_HEALTH_FUNNEL. Verified from Spell.dbc: Health
|
||||
// Funnel (755) Ex2=0x808 and Hellfire (1949) Ex2=0x800 carry it, while
|
||||
// channeled non-funnels (Drain Life, Mind Flay, Arcane Missiles, Rain of
|
||||
// Fire) do not. An earlier version read AttributesEx bit 6, which is
|
||||
// CHANNELED_2 — false for Health Funnel itself and true for unrelated
|
||||
// channels, so it never actually detected funnel spells.
|
||||
static constexpr uint32_t SPELL_ATTR_EX2_HEALTH_FUNNEL = 0x800;
|
||||
// Spell.dbc effect-target arrays. Each spell has 3 effects, each
|
||||
// with an implicit target A and an implicit target B (the latter
|
||||
// often 0). Used by IsSpellHarmful / IsSpellHelpful — vanilla has
|
||||
@@ -48,9 +38,6 @@ static constexpr uint32_t SPELL_ATTR_EX_FUNNEL = 0x40;
|
||||
// have it), so we classify by walking effect targets and checking
|
||||
// whether any falls into a known hostile-target or friendly-target
|
||||
// set. Same algorithm CMaNGOS uses in `SpellMgr::IsPositiveSpell`.
|
||||
static constexpr int OFF_EFFECT_IMPLICIT_TARGET_A = 0x148; // uint32[3]
|
||||
static constexpr int OFF_EFFECT_IMPLICIT_TARGET_B = 0x154; // uint32[3]
|
||||
|
||||
// Target IDs from vanilla 1.12's `SpellTarget` enum that mark a
|
||||
// spell as hostile-targeted (covers single-target damage,
|
||||
// debuffs, AoE damage, etc.). List is conservative — anything not
|
||||
@@ -142,25 +129,26 @@ static bool ReadSpellInfo(int spellID, SpellInfoData &out) {
|
||||
const int locale = ReadGlobal<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
|
||||
out.spellID = spellID;
|
||||
out.name = *reinterpret_cast<const char *const *>(record + OFF_NAME + locale * 4);
|
||||
out.rank = *reinterpret_cast<const char *const *>(record + OFF_RANK + locale * 4);
|
||||
out.name = *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
out.rank = *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_RECORD_RANK + locale * 4);
|
||||
|
||||
out.iconPath = nullptr;
|
||||
const int iconID = *reinterpret_cast<const int *>(record + OFF_ICON_ID);
|
||||
const int iconID = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
if (auto *iconRec = LookupSubRecord(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT, iconID)) {
|
||||
out.iconPath = *reinterpret_cast<const char *const *>(iconRec + 4);
|
||||
out.iconPath = *reinterpret_cast<const char *const *>(iconRec + Offsets::OFF_SPELLICON_PATH);
|
||||
}
|
||||
|
||||
out.cost = *reinterpret_cast<const int *>(record + OFF_MANA_COST);
|
||||
out.cost = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_MANA_COST);
|
||||
|
||||
const uint32_t attrEx = *reinterpret_cast<const uint32_t *>(record + OFF_ATTRIBUTES_EX);
|
||||
out.isFunnel = (attrEx & SPELL_ATTR_EX_FUNNEL) != 0;
|
||||
const uint32_t attrEx2 = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
out.isFunnel = (attrEx2 & SPELL_ATTR_EX2_HEALTH_FUNNEL) != 0;
|
||||
|
||||
out.powerType = *reinterpret_cast<const int *>(record + OFF_POWER_TYPE);
|
||||
out.powerType = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_POWER_TYPE);
|
||||
|
||||
out.castTimeMs = 0;
|
||||
const int castIndex = *reinterpret_cast<const int *>(record + OFF_CASTING_TIME_INDEX);
|
||||
const int castIndex = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_CASTING_TIME_INDEX);
|
||||
if (auto *castRec = LookupSubRecord(Offsets::VAR_SPELL_CAST_TIMES_RECORDS,
|
||||
Offsets::VAR_SPELL_CAST_TIMES_COUNT, castIndex)) {
|
||||
out.castTimeMs = *reinterpret_cast<const int *>(castRec + 4);
|
||||
@@ -168,7 +156,7 @@ static bool ReadSpellInfo(int spellID, SpellInfoData &out) {
|
||||
|
||||
out.minRange = 0.0f;
|
||||
out.maxRange = 0.0f;
|
||||
const int rangeIndex = *reinterpret_cast<const int *>(record + OFF_RANGE_INDEX);
|
||||
const int rangeIndex = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_RANGE_INDEX);
|
||||
if (auto *rangeRec = LookupSubRecord(Offsets::VAR_SPELL_RANGE_RECORDS,
|
||||
Offsets::VAR_SPELL_RANGE_COUNT, rangeIndex)) {
|
||||
out.minRange = *reinterpret_cast<const float *>(rangeRec + 4);
|
||||
@@ -288,7 +276,7 @@ static int __fastcall Script_C_GetSpellName(void *L) {
|
||||
return 0; // nil for unknown spellID
|
||||
|
||||
const int locale = ReadGlobal<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *name = *reinterpret_cast<const char *const *>(record + OFF_NAME + locale * 4);
|
||||
const char *name = *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (name == nullptr || *name == '\0')
|
||||
return 0; // empty / no name in current locale → nil
|
||||
Game::Lua::PushString(L, name);
|
||||
@@ -301,12 +289,12 @@ static int __fastcall Script_C_GetSpellTexture(void *L) {
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
|
||||
const int iconID = *reinterpret_cast<const int *>(record + OFF_ICON_ID);
|
||||
const int iconID = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
auto *iconRec = LookupSubRecord(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT, iconID);
|
||||
if (iconRec == nullptr)
|
||||
return 0;
|
||||
const char *path = *reinterpret_cast<const char *const *>(iconRec + 4);
|
||||
const char *path = *reinterpret_cast<const char *const *>(iconRec + Offsets::OFF_SPELLICON_PATH);
|
||||
if (path == nullptr || *path == '\0')
|
||||
return 0;
|
||||
Game::Lua::PushString(L, path);
|
||||
@@ -327,7 +315,7 @@ static bool BuildSpellLink(int spellID, char *out, size_t outLen) {
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
const int locale = ReadGlobal<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *name = *reinterpret_cast<const char *const *>(record + OFF_NAME + locale * 4);
|
||||
const char *name = *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (name == nullptr || *name == '\0')
|
||||
return false;
|
||||
const int n = std::snprintf(out, outLen, "|cff71d5ff|Hspell:%d:0|h[%s]|h|r",
|
||||
@@ -404,8 +392,9 @@ static int PushIsPassive(void *L, int spellID) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
const uint32_t attr = *reinterpret_cast<const uint32_t *>(record + OFF_ATTRIBUTES);
|
||||
Game::Lua::PushBool(L, (attr & SPELL_ATTR_PASSIVE) != 0);
|
||||
const uint32_t attr = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
Game::Lua::PushBool(L, (attr & Offsets::SPELL_ATTR_PASSIVE) != 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -534,9 +523,9 @@ static bool AnyEffectTarget(const uint8_t *record, Pred pred) {
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
auto *targetsA = reinterpret_cast<const uint32_t *>(
|
||||
record + OFF_EFFECT_IMPLICIT_TARGET_A);
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A);
|
||||
auto *targetsB = reinterpret_cast<const uint32_t *>(
|
||||
record + OFF_EFFECT_IMPLICIT_TARGET_B);
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_B);
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (pred(targetsA[i]) || pred(targetsB[i]))
|
||||
return true;
|
||||
|
||||
+32
-33
@@ -16,7 +16,11 @@
|
||||
// `Spell.dbc.EffectImplicitTargetA[3]` and `EffectImplicitTargetB[3]`
|
||||
// — an effect is "self-only" when both targets are either
|
||||
// `TARGET_NONE` (0) or `TARGET_SELF` (1). Effects with a zero
|
||||
// `Effect` code are skipped (unused effect slots).
|
||||
// `Effect` code are skipped (unused effect slots). The record-scan core
|
||||
// is `Spell::IsSelfBuff::IsSelfBuff` (see the header) so the aura layer
|
||||
// can share it for sourceGUID inference.
|
||||
|
||||
#include "spell/IsSelfBuff.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -31,9 +35,6 @@ namespace {
|
||||
// Spell.dbc per-effect arrays. Each is an int32[3] in the record.
|
||||
// Field-index derivation against the known `EffectApplyAuraName` at
|
||||
// field 91 / +0x16C — the implicit-target pair sits at field 82/85.
|
||||
constexpr int OFF_SPELL_EFFECT = 0xF4; // SPELL_EFFECT_* code
|
||||
constexpr int OFF_SPELL_EFFECT_TARGET_A = 0x148;
|
||||
constexpr int OFF_SPELL_EFFECT_TARGET_B = 0x154;
|
||||
constexpr int EFFECT_COUNT = 3;
|
||||
|
||||
// Vanilla 1.12 implicit-target codes (CMaNGOS `Targets` enum).
|
||||
@@ -46,41 +47,39 @@ bool IsSelfOnlyTarget(int target) {
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsSelfBuff(uint32_t spellID) {
|
||||
if (spellID == 0)
|
||||
return false;
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT, spellID);
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
|
||||
bool sawEffect = false;
|
||||
for (int i = 0; i < EFFECT_COUNT; ++i) {
|
||||
const int effect = *reinterpret_cast<const int *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT + i * 4);
|
||||
if (effect == 0)
|
||||
continue;
|
||||
sawEffect = true;
|
||||
const int targetA = *reinterpret_cast<const int *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A + i * 4);
|
||||
const int targetB = *reinterpret_cast<const int *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_B + i * 4);
|
||||
if (!IsSelfOnlyTarget(targetA) || !IsSelfOnlyTarget(targetB))
|
||||
return false;
|
||||
}
|
||||
return sawEffect;
|
||||
}
|
||||
|
||||
static int __fastcall Script_IsSelfBuff(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (spellID <= 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT,
|
||||
static_cast<uint32_t>(spellID));
|
||||
if (rec == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool sawEffect = false;
|
||||
for (int i = 0; i < EFFECT_COUNT; ++i) {
|
||||
const int effect = *reinterpret_cast<const int *>(
|
||||
rec + OFF_SPELL_EFFECT + i * 4);
|
||||
if (effect == 0)
|
||||
continue;
|
||||
sawEffect = true;
|
||||
const int targetA = *reinterpret_cast<const int *>(
|
||||
rec + OFF_SPELL_EFFECT_TARGET_A + i * 4);
|
||||
const int targetB = *reinterpret_cast<const int *>(
|
||||
rec + OFF_SPELL_EFFECT_TARGET_B + i * 4);
|
||||
if (!IsSelfOnlyTarget(targetA) || !IsSelfOnlyTarget(targetB)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Game::Lua::PushBool(L, sawEffect);
|
||||
Game::Lua::PushBool(
|
||||
L, spellID > 0 && IsSelfBuff(static_cast<uint32_t>(spellID)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Spell::IsSelfBuff {
|
||||
|
||||
// True iff every active effect of `spellID` targets only the caster — each
|
||||
// effect's implicit target A and B is `TARGET_NONE` (0) or `TARGET_SELF` (1),
|
||||
// read from `Spell.dbc.EffectImplicitTargetA/B[3]`. Such an aura can only have
|
||||
// been self-cast, so a unit carrying one is definitionally its own source.
|
||||
// False for an unknown spell or one with no active effect. Shared by the
|
||||
// `C_Spell.IsSelfBuff` Lua binding and the `C_UnitAuras` sourceGUID inference.
|
||||
bool IsSelfBuff(uint32_t spellID);
|
||||
|
||||
} // namespace Spell::IsSelfBuff
|
||||
+32
-43
@@ -84,37 +84,35 @@ int SpellBaseLevel(int spellID) {
|
||||
record + Offsets::OFF_SPELL_RECORD_BASE_LEVEL);
|
||||
}
|
||||
|
||||
// Spell.dbc attribute fields/bits used to filter learnable-spell
|
||||
// results down to what's actually user-facing.
|
||||
// Spell.dbc attribute filter for the learnable-spell walk. Only the
|
||||
// verified HIDDEN_CLIENTSIDE bit is used:
|
||||
//
|
||||
// Attributes (+0x18)
|
||||
// bit 0x80 = HIDDEN_CLIENTSIDE — engine-internal helpers like
|
||||
// "Defensive Stance Passive" (7376) that pair with
|
||||
// a user-visible spell (Defensive Stance, 71) and
|
||||
// never appear in the spellbook. Skip these.
|
||||
// Attributes (+0x18) bit 0x80 = HIDDEN_CLIENTSIDE — engine-internal
|
||||
// helpers like "Defensive Stance Passive" (7376) that pair with a
|
||||
// user-visible spell (Defensive Stance, 71) and never appear in the
|
||||
// spellbook. Skip these.
|
||||
//
|
||||
// AttributesEx2 (+0x24)
|
||||
// bit 0x02000000 = HIDE_FROM_AUTOLEARN — the same gate Cata's
|
||||
// GetCurrentLevelSpells checks
|
||||
// (FUN_00911c60's `(spellData[+0x24] &
|
||||
// 0x02000000) == 0`). Catches proc-only and
|
||||
// engine-internal spells with a BaseLevel.
|
||||
constexpr int OFF_SPELL_RECORD_ATTRIBUTES = 0x18;
|
||||
constexpr int OFF_SPELL_RECORD_ATTRIBUTES_EX2 = 0x24;
|
||||
// A prior version also tested AttributesEx3 (+0x24) bit 0x02000000 as a
|
||||
// "hide from auto-learn" gate ported from a MoP function (FUN_00911c60).
|
||||
// That was invalid on every count: FUN_00911c60 is outside 1.12's .text
|
||||
// and its `+0x24` is not 1.12's AttributesEx3. Verified against Spell.dbc,
|
||||
// the 1.12 bit is a sparse grab-bag (44 / 27925 records: passive procs,
|
||||
// odd DoT ranks, the Night Elf priest Starshards line, Turtle customs) —
|
||||
// not an auto-learn flag — so it could only wrongly hide legitimate
|
||||
// learnable spells (Starshards carries it). Removed. The authoritative
|
||||
// auto-learn signal is SkillLineAbility.learnOnGetSkill (col 9: 2 =
|
||||
// race/class, 1 = profession, 0 = trainer-taught), not a Spell.dbc bit;
|
||||
// this feature deliberately reports the broader "trainable at level" set,
|
||||
// so it applies no learnOnGetSkill filter.
|
||||
constexpr uint32_t SPELL_ATTR_HIDDEN_CLIENTSIDE = 0x80;
|
||||
constexpr uint32_t SPELL_ATTR_EX2_HIDE_FROM_AUTOLEARN = 0x02000000;
|
||||
|
||||
bool SpellHiddenFromAutoLearn(int spellID) {
|
||||
bool SpellHiddenFromSpellbook(int spellID) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
const uint32_t attributes = *reinterpret_cast<const uint32_t *>(
|
||||
record + OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
if ((attributes & SPELL_ATTR_HIDDEN_CLIENTSIDE) != 0)
|
||||
return true;
|
||||
const uint32_t attrEx2 = *reinterpret_cast<const uint32_t *>(
|
||||
record + OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
return (attrEx2 & SPELL_ATTR_EX2_HIDE_FROM_AUTOLEARN) != 0;
|
||||
record + Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
return (attributes & SPELL_ATTR_HIDDEN_CLIENTSIDE) != 0;
|
||||
}
|
||||
|
||||
// ---- Target-level gate for ranked beneficial auras ----------------------
|
||||
@@ -132,12 +130,6 @@ bool SpellHiddenFromAutoLearn(int spellID) {
|
||||
// checks are NOT in the 1.12 client's Spell.dbc — server-only columns — so
|
||||
// the spellLevel rule is the only client-readable target-level mechanism.)
|
||||
|
||||
constexpr int OFF_SPELL_MAX_LEVEL = 0x6C; // uint32 (scaling cap level)
|
||||
constexpr int OFF_SPELL_SPELL_LEVEL = 0x74; // uint32 (rank's effective level)
|
||||
constexpr int OFF_SPELL_EFFECT = 0xF4; // uint32[3] effect type
|
||||
constexpr int OFF_SPELL_EFFECT_TARGET_A = 0x148; // uint32[3] implicit target A
|
||||
constexpr int OFF_SPELL_RANK = 0x204; // char*[9] localized rank text
|
||||
constexpr uint32_t SPELL_ATTR_PASSIVE = 0x40; // Attributes bit
|
||||
constexpr uint32_t SPELL_EFFECT_APPLY_AURA = 6;
|
||||
constexpr uint32_t SPELL_EFFECT_APPLY_AREA_AURA_PARTY = 35;
|
||||
|
||||
@@ -178,7 +170,7 @@ bool IsPositiveRankTarget(uint32_t t) {
|
||||
// empty Rank and un-gated) vs ranked ones (Divine Spirit / Fortitude).
|
||||
bool HasRankString(const uint8_t *record) {
|
||||
const char *rank = *reinterpret_cast<const char *const *>(
|
||||
record + OFF_SPELL_RANK + LocaleIndex() * 4);
|
||||
record + Offsets::OFF_SPELL_RECORD_RANK + LocaleIndex() * 4);
|
||||
return rank != nullptr && *rank != '\0';
|
||||
}
|
||||
|
||||
@@ -186,19 +178,19 @@ bool HasRankString(const uint8_t *record) {
|
||||
// the spell isn't subject to the ranked-positive-aura target-level rule.
|
||||
int RequiredTargetLevel(const uint8_t *record) {
|
||||
const int spellLevel =
|
||||
*reinterpret_cast<const int *>(record + OFF_SPELL_SPELL_LEVEL);
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_SPELL_LEVEL);
|
||||
if (spellLevel <= 10) // targetLevel + 10 >= spellLevel holds for any level
|
||||
return 0;
|
||||
const uint32_t attr =
|
||||
*reinterpret_cast<const uint32_t *>(record + OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
if (attr & SPELL_ATTR_PASSIVE)
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
if (attr & Offsets::SPELL_ATTR_PASSIVE)
|
||||
return 0;
|
||||
if (!HasRankString(record)) // single-rank spell → not gated
|
||||
return 0;
|
||||
|
||||
auto *effect = reinterpret_cast<const uint32_t *>(record + OFF_SPELL_EFFECT);
|
||||
auto *effect = reinterpret_cast<const uint32_t *>(record + Offsets::OFF_SPELL_RECORD_EFFECT);
|
||||
auto *targetA =
|
||||
reinterpret_cast<const uint32_t *>(record + OFF_SPELL_EFFECT_TARGET_A);
|
||||
reinterpret_cast<const uint32_t *>(record + Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A);
|
||||
bool positiveAura = false;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if ((effect[i] == SPELL_EFFECT_APPLY_AURA &&
|
||||
@@ -229,13 +221,13 @@ int __fastcall Script_GetSpellLevelInfo(void *L) {
|
||||
return 0;
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + OFF_SPELL_SPELL_LEVEL)));
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_SPELL_LEVEL)));
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_BASE_LEVEL)));
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + OFF_SPELL_MAX_LEVEL)));
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_MAX_LEVEL)));
|
||||
return 3;
|
||||
}
|
||||
|
||||
@@ -425,12 +417,9 @@ int __fastcall Script_GetCurrentLevelSpells(void *L) {
|
||||
// GetCurrentLevelSpells excludes them too.
|
||||
if (TalentSpellSet().count(spellID) != 0)
|
||||
continue;
|
||||
// Skip spells flagged "hide from auto-learn list" — the
|
||||
// same AttributesEx2 bit Cata's GetCurrentLevelSpells
|
||||
// checks (FUN_00911c60). Catches engine-internal or proc-
|
||||
// only spells that have a BaseLevel but aren't meant to
|
||||
// surface as learnable.
|
||||
if (SpellHiddenFromAutoLearn(spellID))
|
||||
// Skip engine-internal spellbook-hidden spells (HIDDEN_CLIENTSIDE):
|
||||
// they can carry a BaseLevel but never surface as learnable.
|
||||
if (SpellHiddenFromSpellbook(spellID))
|
||||
continue;
|
||||
if (!seen.insert(spellID).second)
|
||||
continue;
|
||||
|
||||
+4
-3
@@ -51,9 +51,10 @@ float Apply(const uint8_t *spellRecord, int op, float base) {
|
||||
|
||||
const uint32_t familyName = *reinterpret_cast<const uint32_t *>(
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_FAMILY_NAME);
|
||||
const uint32_t attrEx2 = *reinterpret_cast<const uint32_t *>(
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
if (familyName == 0 || (attrEx2 & Offsets::SPELL_ATTR_EX2_NO_SPELL_MODS) != 0)
|
||||
const uint32_t attrEx3 = *reinterpret_cast<const uint32_t *>(
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX3);
|
||||
if (familyName == 0 ||
|
||||
(attrEx3 & Offsets::SPELL_ATTR_EX3_IGNORE_CASTER_MODIFIERS) != 0)
|
||||
return base;
|
||||
if (familyName != PlayerSpellFamily())
|
||||
return base;
|
||||
|
||||
@@ -49,8 +49,6 @@ namespace {
|
||||
|
||||
using GetSpellCost_t = uint32_t(__fastcall *)(int spellID, int unit);
|
||||
|
||||
constexpr int OFF_POWER_TYPE = 0x7C; // int PowerType
|
||||
constexpr int OFF_MANA_COST_PERCENT = 0x270; // int, % of base resource (0 = flat)
|
||||
|
||||
uint32_t EffectiveCost(int spellID) {
|
||||
return reinterpret_cast<GetSpellCost_t>(Offsets::FUN_GET_SPELL_COST)(spellID, 0);
|
||||
@@ -66,8 +64,8 @@ int __fastcall Script_GetSpellPowerCost(void *L) {
|
||||
if (cost == 0xFFFFFFFF)
|
||||
return 0; // engine error (no player context, etc.)
|
||||
|
||||
const int powerType = *reinterpret_cast<const int *>(rec + OFF_POWER_TYPE);
|
||||
const int costPercent = *reinterpret_cast<const int *>(rec + OFF_MANA_COST_PERCENT);
|
||||
const int powerType = *reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_RECORD_POWER_TYPE);
|
||||
const int costPercent = *reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_RECORD_MANA_COST_PERCENT);
|
||||
if (cost == 0 && costPercent == 0)
|
||||
return 0; // nil — no resource cost
|
||||
|
||||
|
||||
+1
-2
@@ -23,7 +23,6 @@ namespace {
|
||||
|
||||
// Spell.dbc RangeIndex + SpellRange.dbc min/max offsets (verified,
|
||||
// documented in CLAUDE.md under "Sub-DBC record layouts").
|
||||
constexpr int OFF_SPELL_RANGE_INDEX = 0x90;
|
||||
constexpr int OFF_SPELLRANGE_MIN = 0x04;
|
||||
constexpr int OFF_SPELLRANGE_MAX = 0x08;
|
||||
|
||||
@@ -42,7 +41,7 @@ bool SpellIDHasRange(int spellID) {
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
const uint32_t rangeIdx =
|
||||
*reinterpret_cast<const uint32_t *>(rec + OFF_SPELL_RANGE_INDEX);
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_RANGE_INDEX);
|
||||
if (rangeIdx == 0)
|
||||
return false;
|
||||
const uint8_t *rangeRec = DBC::Record(Offsets::VAR_SPELL_RANGE_RECORDS,
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Spell.GetSpellDispelType` / `IsNextMeleeSpell` / `ResetsMeleeSwing` —
|
||||
// read raw Spell.dbc fields straight off the record so pfUI can drop its
|
||||
// nampower `GetSpellRecField` dependency.
|
||||
//
|
||||
// The Spell.dbc record fields we read live in `Offsets.h` (the single source
|
||||
// of truth for the record layout): `OFF_SPELL_DISPEL_TYPE` (+0x10),
|
||||
// `OFF_SPELL_RECORD_ATTRIBUTES` (+0x18), `OFF_SPELL_RECORD_ATTRIBUTES_EX2`
|
||||
// (+0x20), `OFF_SPELL_RECORD_INTERRUPT_FLAGS` (+0x54).
|
||||
//
|
||||
// Melee rules (server core, Spell.h / Spell.cpp):
|
||||
// on-next-swing = Attributes & (0x04 | 0x400) — two bits (Spell.h:434)
|
||||
// resets swing = (InterruptFlags & 0x08) && !(AttributesEx2 & 0x20000)
|
||||
// (Spell.cpp:3964 + Spell.h:441)
|
||||
|
||||
#include "Arg.h"
|
||||
#include "Lookup.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Spell::RecordFields {
|
||||
|
||||
namespace {
|
||||
|
||||
// Attributes bits 0x04 (SPELL_ATTR_ON_NEXT_SWING) and 0x400
|
||||
// (SPELL_ATTR_ON_NEXT_SWING_2) both mark an on-next-melee-swing ability
|
||||
// (server Spell.h:434 tests the pair).
|
||||
constexpr uint32_t SPELL_ATTR_ON_NEXT_SWING = 0x04 | 0x400;
|
||||
// InterruptFlags bit 0x08 = SPELL_INTERRUPT_FLAG_AUTOATTACK.
|
||||
constexpr uint32_t SPELL_INTERRUPT_AUTOATTACK = 0x08;
|
||||
// AttributesEx2 bit 0x20000 = NOT_RESET_AUTO_ACTIONS — exempts the spell
|
||||
// from the swing reset the AUTOATTACK interrupt flag would otherwise cause.
|
||||
constexpr uint32_t SPELL_ATTR_EX2_NOT_RESET_AUTO_ACTIONS = 0x20000;
|
||||
|
||||
uint32_t ReadField(const uint8_t *rec, int offset) {
|
||||
return *reinterpret_cast<const uint32_t *>(rec + offset);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// C_Spell.GetSpellDispelType(spellId) -> number. 0 for a non-dispellable
|
||||
// spell or an unknown ID.
|
||||
static int __fastcall Script_GetSpellDispelType(void *L) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(Spell::Arg::ResolveSpellID(L, 1));
|
||||
Game::Lua::PushNumber(
|
||||
L, rec != nullptr
|
||||
? static_cast<double>(ReadField(rec, Offsets::OFF_SPELL_DISPEL_TYPE))
|
||||
: 0.0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// C_Spell.IsNextMeleeSpell(spellId) -> bool. True for on-next-swing abilities
|
||||
// (Heroic Strike, Maul, Cleave, Raptor Strike, Slam).
|
||||
static int __fastcall Script_IsNextMeleeSpell(void *L) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(Spell::Arg::ResolveSpellID(L, 1));
|
||||
const bool onNextSwing =
|
||||
rec != nullptr &&
|
||||
(ReadField(rec, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) & SPELL_ATTR_ON_NEXT_SWING) != 0;
|
||||
Game::Lua::PushBool(L, onNextSwing);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// C_Spell.ResetsMeleeSwing(spellId) -> bool. True when casting the spell
|
||||
// resets the melee swing timer (AUTOATTACK interrupt flag set and the
|
||||
// NOT_RESET_AUTO_ACTIONS exemption clear).
|
||||
static int __fastcall Script_ResetsMeleeSwing(void *L) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(Spell::Arg::ResolveSpellID(L, 1));
|
||||
const bool resets =
|
||||
rec != nullptr &&
|
||||
(ReadField(rec, Offsets::OFF_SPELL_RECORD_INTERRUPT_FLAGS) &
|
||||
SPELL_INTERRUPT_AUTOATTACK) != 0 &&
|
||||
(ReadField(rec, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2) &
|
||||
SPELL_ATTR_EX2_NOT_RESET_AUTO_ACTIONS) == 0;
|
||||
Game::Lua::PushBool(L, resets);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellDispelType",
|
||||
&Script_GetSpellDispelType);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "IsNextMeleeSpell",
|
||||
&Script_IsNextMeleeSpell);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "ResetsMeleeSwing",
|
||||
&Script_ResetsMeleeSwing);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Spell::RecordFields
|
||||
@@ -29,11 +29,6 @@ namespace Spell::Subtext {
|
||||
|
||||
namespace {
|
||||
|
||||
// `Spell.dbc.Rank[9]` — same 9-slot locale-string array shape as the
|
||||
// adjacent `Name[9]` field. Defined locally; sibling modules
|
||||
// (`Info.cpp`, `Tooltip.cpp`, `macro/Spell.cpp`) carry the same
|
||||
// constant — promoting to `Offsets.h` is a future refactor.
|
||||
constexpr int OFF_SPELL_RANK = 0x204;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -46,7 +41,7 @@ static int __fastcall Script_GetSpellSubtext(void *L) {
|
||||
const char *subtext = DBC::LocalizedField(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT,
|
||||
static_cast<uint32_t>(spellID),
|
||||
OFF_SPELL_RANK);
|
||||
Offsets::OFF_SPELL_RECORD_RANK);
|
||||
if (subtext == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
|
||||
@@ -24,11 +24,9 @@ namespace Spell::Tooltip {
|
||||
// pulled from Spell::Info — that module keeps these as `static
|
||||
// constexpr` privates and exposing them just for two readers here
|
||||
// would force a header change.
|
||||
static constexpr int OFF_SPELL_NAME = 0x1E0; // localized char *[9]
|
||||
static constexpr int OFF_SPELL_RANK = 0x204; // localized char *[9]
|
||||
|
||||
using BuildSpellTooltip_t = void(__thiscall *)(void *thisObj, int spellID, int arg2, int arg3,
|
||||
int isPet, int arg5, int arg6, int arg7);
|
||||
int isPet, int showRank, int arg6, int arg7);
|
||||
|
||||
void ShowByID(void *L, int spellID) {
|
||||
if (spellID <= 0)
|
||||
@@ -38,7 +36,7 @@ void ShowByID(void *L, int spellID) {
|
||||
return;
|
||||
auto BuildSpellTooltip =
|
||||
reinterpret_cast<BuildSpellTooltip_t>(Offsets::FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP);
|
||||
BuildSpellTooltip(tooltipObj, spellID, 0, 0, 0, 0, 0, 0);
|
||||
BuildSpellTooltip(tooltipObj, spellID, 0, 0, 0, /*showRank=*/1, 0, 0);
|
||||
}
|
||||
|
||||
static int __fastcall Script_GameTooltipSetSpellByID(void *L) {
|
||||
@@ -94,7 +92,7 @@ void AppendByID(void *L, int spellID) {
|
||||
return;
|
||||
const int locale = *reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + OFF_SPELL_NAME + locale * 4);
|
||||
record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (name == nullptr || name[0] == '\0')
|
||||
return;
|
||||
|
||||
@@ -154,9 +152,9 @@ static int __fastcall Script_GameTooltipGetSpell(void *L) {
|
||||
|
||||
const int locale = *reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + OFF_SPELL_NAME + locale * 4);
|
||||
record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
const char *rank = *reinterpret_cast<const char *const *>(
|
||||
record + OFF_SPELL_RANK + locale * 4);
|
||||
record + Offsets::OFF_SPELL_RECORD_RANK + locale * 4);
|
||||
|
||||
if (name == nullptr)
|
||||
return 0;
|
||||
|
||||
+3
-1
@@ -17,7 +17,9 @@ namespace Spell::Tooltip {
|
||||
|
||||
// Resolves the `GameTooltip` self at Lua stack[1] (via the
|
||||
// engine's `FrameScript_PushObject`/`FrameScript_GetObject` chain)
|
||||
// and dispatches the engine's spell-tooltip builder for `spellID`.
|
||||
// and dispatches the engine's spell-tooltip builder for `spellID`,
|
||||
// with the spell's rank shown in the top-right corner of the title
|
||||
// line (matching `SetAction`/`SetPetAction`; native `SetSpell` omits it).
|
||||
// Silent no-op if `spellID <= 0` or stack[1] isn't a frame object.
|
||||
//
|
||||
// Used by `GameTooltip:SetSpellByID` directly, and by
|
||||
|
||||
@@ -29,8 +29,6 @@ using GetSpellCost_t = uint32_t(__fastcall *)(int spellID, int unit);
|
||||
|
||||
// Spell.dbc record field offsets we read for usability. Fully documented
|
||||
// in CLAUDE.md.
|
||||
constexpr int OFF_SPELL_POWER_TYPE = 0x7C; // int8: 0=mana, 1=rage, 2=focus, 3=energy, 4=happiness
|
||||
constexpr int OFF_SPELL_MANA_COST = 0x80; // u32 base cost (fallback only)
|
||||
|
||||
// Returns true if the player knows `spellID` per the engine's spell-
|
||||
// knowledge bitmap at `[VAR_PLAYER_SPELL_BITMAP]`. Same check
|
||||
@@ -179,12 +177,12 @@ Usability ComputeUsability(void *L, int spellID) {
|
||||
// resolve a cost (shouldn't happen here: the player descriptor
|
||||
// resolved above, so it has a player context).
|
||||
const int powerType = static_cast<int>(*reinterpret_cast<const int8_t *>(
|
||||
record + OFF_SPELL_POWER_TYPE));
|
||||
record + Offsets::OFF_SPELL_RECORD_POWER_TYPE));
|
||||
if (powerType >= 0 && powerType <= 4) {
|
||||
auto getCost = reinterpret_cast<GetSpellCost_t>(Offsets::FUN_GET_SPELL_COST);
|
||||
uint32_t cost = getCost(spellID, 0 /* local player */);
|
||||
if (cost == 0xFFFFFFFF)
|
||||
cost = *reinterpret_cast<const uint32_t *>(record + OFF_SPELL_MANA_COST);
|
||||
cost = *reinterpret_cast<const uint32_t *>(record + Offsets::OFF_SPELL_RECORD_MANA_COST);
|
||||
if (cost > 0) {
|
||||
const int currentPower = *reinterpret_cast<const int *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_POWER1 + powerType * 4);
|
||||
|
||||
+7
-14
@@ -56,13 +56,6 @@ namespace Totem::Tracker {
|
||||
|
||||
namespace {
|
||||
|
||||
// Spell.dbc record field offsets (see spell/Info.cpp for the shared set).
|
||||
constexpr int OFF_EFFECT = 0xF4; // int32[3] — SPELL_EFFECT_*
|
||||
constexpr int OFF_TOTEM_TOOL = 0xA0; // int32[2] — Totem (required tool item IDs)
|
||||
constexpr int OFF_MISC_VALUE = 0x1A8; // int32[3] — EffectMiscValue
|
||||
constexpr int OFF_ICON_ID = 0x1D4; // int32 — SpellIconID
|
||||
constexpr int OFF_NAME = 0x1E0; // char*[9] locale array
|
||||
|
||||
// SUMMON_TOTEM_SLOT1..4 spell effects (verified against Spell.dbc: Searing
|
||||
// 87/Fire, Stoneskin 88/Earth, Healing Stream 89/Water, Windfury 90/Air).
|
||||
constexpr int kEffectSummonTotemSlot1 = 87;
|
||||
@@ -101,7 +94,7 @@ uint32_t NowMs() {
|
||||
|
||||
// The totem tool item a spell requires (`Totem[0]`), or 0.
|
||||
uint32_t TotemToolItem(const uint8_t *rec) {
|
||||
const int32_t t = *reinterpret_cast<const int32_t *>(rec + OFF_TOTEM_TOOL);
|
||||
const int32_t t = *reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELL_RECORD_TOTEM);
|
||||
return t > 0 ? static_cast<uint32_t>(t) : 0;
|
||||
}
|
||||
|
||||
@@ -131,7 +124,7 @@ void ScanTools() {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
const auto *eff = reinterpret_cast<const int32_t *>(rec + OFF_EFFECT);
|
||||
const auto *eff = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELL_RECORD_EFFECT);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
const int e = eff[i];
|
||||
if (e < kEffectSummonTotemSlot1 || e > kEffectSummonTotemSlot4)
|
||||
@@ -413,10 +406,10 @@ int __fastcall Script_GetTotemInfo(void *L) {
|
||||
*reinterpret_cast<const int *>(static_cast<uintptr_t>(
|
||||
Offsets::VAR_LOCALE_INDEX));
|
||||
const char *n = *reinterpret_cast<const char *const *>(
|
||||
rec + OFF_NAME + locale * 4);
|
||||
rec + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (n != nullptr)
|
||||
name = n;
|
||||
icon = IconPath(*reinterpret_cast<const int *>(rec + OFF_ICON_ID));
|
||||
icon = IconPath(*reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_RECORD_ICON_ID));
|
||||
}
|
||||
|
||||
Game::Lua::PushBool(L, haveTotem);
|
||||
@@ -563,7 +556,7 @@ int __fastcall Script_GameTooltipSetTotem(void *L) {
|
||||
const int locale =
|
||||
*reinterpret_cast<const int *>(static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
const char *name =
|
||||
*reinterpret_cast<const char *const *>(rec + OFF_NAME + locale * 4);
|
||||
*reinterpret_cast<const char *const *>(rec + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (name == nullptr || name[0] == '\0')
|
||||
return 0;
|
||||
|
||||
@@ -630,8 +623,8 @@ void OnPlayerSpellGo(uint32_t spellID) {
|
||||
if (rec == nullptr)
|
||||
return;
|
||||
|
||||
const auto *effects = reinterpret_cast<const int32_t *>(rec + OFF_EFFECT);
|
||||
const auto *misc = reinterpret_cast<const int32_t *>(rec + OFF_MISC_VALUE);
|
||||
const auto *effects = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELL_RECORD_EFFECT);
|
||||
const auto *misc = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELL_RECORD_EFFECT_MISC_VALUE);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
const int eff = effects[i];
|
||||
if (eff < kEffectSummonTotemSlot1 || eff > kEffectSummonTotemSlot4)
|
||||
|
||||
+23
-6
@@ -34,14 +34,23 @@ bool IsPlayerControlled(const uint8_t *unit) {
|
||||
return (unitFlags & Offsets::UNIT_FLAG_PLAYER_CONTROLLED) != 0;
|
||||
}
|
||||
|
||||
bool IsPlayerObject(const uint8_t *unit) {
|
||||
if (unit == nullptr)
|
||||
return false;
|
||||
return *reinterpret_cast<const int *>(unit + Offsets::OFF_CGOBJECT_TYPE_ID) ==
|
||||
Offsets::OBJECT_TYPE_PLAYER;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// Reads a PLAYER_FLAGS bit for any player-controlled unit. Path:
|
||||
// - Resolve unit; gate on `UNIT_FLAG_PLAYER_CONTROLLED` to avoid
|
||||
// reading the +0xE68 pointer on a creature/NPC (it's uninitialized
|
||||
// for non-player units).
|
||||
// Reads a PLAYER_FLAGS bit for a player. Path:
|
||||
// - Resolve unit; gate on the object actually being a player
|
||||
// (`OBJECT_TYPE_PLAYER`) so we never read the +0xE68 CGPlayer
|
||||
// sub-struct on a non-player object. UNIT_FLAG_PLAYER_CONTROLLED is
|
||||
// insufficient — pets/totems/MC'd creatures set it but have no
|
||||
// sub-struct there.
|
||||
// - Read `[unit + 0xE68] + 0x08` — the unit's CGPlayer-side info
|
||||
// struct, where byte +0x08 is PLAYER_FLAGS. Same struct
|
||||
// `Script_IsResting` reads at +0x05 bit (RESTING = 0x20) for the
|
||||
@@ -64,7 +73,10 @@ bool TestPlayerFlag(void *L, uint32_t flagMask) {
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
if (!IsPlayerControlled(unit))
|
||||
// PLAYER_FLAGS lives in the CGPlayer sub-struct — gate on the object
|
||||
// actually being a player, not merely player-controlled (pets/totems
|
||||
// set that flag but have no sub-struct → garbage +0xE68 deref).
|
||||
if (!IsPlayerObject(unit))
|
||||
return false;
|
||||
|
||||
auto *playerInfo = *reinterpret_cast<const uint8_t *const *>(
|
||||
@@ -134,7 +146,12 @@ int __fastcall Script_UnitIsFeignDeath(void *L) {
|
||||
// Returns 0 if the unit isn't player-controlled, isn't a player, or
|
||||
// hasn't had its guild data synced.
|
||||
uint32_t ReadGuildKey(const uint8_t *unit) {
|
||||
if (!IsPlayerControlled(unit))
|
||||
// The guild key lives in the CGPlayer sub-struct at +0xE68, which
|
||||
// only exists on real player objects. A player-controlled *creature*
|
||||
// (pet, totem, MC'd mob) has UNIT_FLAG_PLAYER_CONTROLLED set but no
|
||||
// sub-struct, so gating on IsPlayerControlled here would deref garbage
|
||||
// and crash (issue: UnitIsInMyGuild on a nameplate pet/totem).
|
||||
if (!IsPlayerObject(unit))
|
||||
return 0;
|
||||
auto *info = *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_CGPLAYER_INFO);
|
||||
|
||||
+22
-5
@@ -16,14 +16,31 @@ namespace Unit::Flags {
|
||||
|
||||
// Tests `UNIT_FIELD_FLAGS & UNIT_FLAG_PLAYER_CONTROLLED` on the unit's
|
||||
// `m_objectFields` descriptor — same predicate
|
||||
// `Script_UnitPlayerControlled` (`0x00516410`) implements. Required
|
||||
// gate before dereferencing the CGPlayer-side sub-struct at
|
||||
// `unit + OFF_CGPLAYER_INFO` (= +0xE68) because that slot is
|
||||
// uninitialized for non-player CGUnit subclasses (CGCreature_C
|
||||
// instances) and reading it AVs on the subsequent deref.
|
||||
// `Script_UnitPlayerControlled` (`0x00516410`) implements. True for
|
||||
// players AND their pets/totems/guardians/companions/charms (the
|
||||
// server sets this flag on all of them — see Turtle
|
||||
// `UnitDefines.h` UNIT_FLAG_PLAYER_CONTROLLED).
|
||||
//
|
||||
// NOT a safe gate for reading the CGPlayer sub-struct at
|
||||
// `unit + OFF_CGPLAYER_INFO` (= +0xE68): a player-controlled *creature*
|
||||
// (pet/totem/MC'd mob) is a CGCreature_C with no such sub-struct, so
|
||||
// its +0xE68 slot is garbage-but-non-null and the subsequent deref AVs
|
||||
// (pfUI issue #34 — `UnitIsInMyGuild` on a nameplate pet). Use
|
||||
// `IsPlayerObject` for that. This predicate is for minion/ownership
|
||||
// classification, where the broad flag is what's wanted.
|
||||
//
|
||||
// Returns `false` for `nullptr` units and units with no
|
||||
// descriptor pointer yet (transient pre-spawn / out-of-sync state).
|
||||
bool IsPlayerControlled(const uint8_t *unit);
|
||||
|
||||
// True only for real player objects (`OBJECT_TYPE_PLAYER`), read from
|
||||
// the base CGObject `m_objectType` field at `[obj + 0x14]`
|
||||
// (`OFF_CGOBJECT_TYPE_ID`). This is the correct gate before reading the
|
||||
// CGPlayer sub-struct at `unit + OFF_CGPLAYER_INFO` (guild key,
|
||||
// PLAYER_FLAGS, visible items, quest list) — that sub-struct exists only
|
||||
// on player objects, never on player-controlled creatures.
|
||||
//
|
||||
// Returns `false` for `nullptr` units.
|
||||
bool IsPlayerObject(const uint8_t *unit);
|
||||
|
||||
} // namespace Unit::Flags
|
||||
|
||||
Reference in New Issue
Block a user