mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 04:47:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fd1295679 | ||
|
|
e4a75e68ca | ||
|
|
28bf41c647 | ||
|
|
ac97f9b960 | ||
|
|
7f6c6e2dd4 | ||
|
|
9d09266fc6 | ||
|
|
d972433ab0 | ||
|
|
b5513c6b22 | ||
|
|
8a434fea0e | ||
|
|
dbbba88aad | ||
|
|
b8bd0bce4e | ||
|
|
c3a80053e9 | ||
|
|
5a3badb5bd |
+42
-14
@@ -4,6 +4,18 @@ 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
|
||||
@@ -16,36 +28,52 @@ jobs:
|
||||
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
|
||||
shell: bash
|
||||
run: |
|
||||
$version = '${{ github.ref_name }}' -replace '^v',''
|
||||
$toc = 'AddOns/!!!ClassicAPI/!!!ClassicAPI.toc'
|
||||
(Get-Content $toc) -replace '^## Version:.*', "## Version: $version" |
|
||||
Set-Content $toc
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.tag }}" ]; then
|
||||
tag="${{ inputs.tag }}"
|
||||
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
tag="${{ github.ref_name }}"
|
||||
else
|
||||
tag=""
|
||||
fi
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
if [ -n "$tag" ]; then
|
||||
echo "is_release=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_release=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- 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")
|
||||
|
||||
|
||||
+70
@@ -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
|
||||
|
||||
@@ -1498,6 +1498,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 +1518,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 +3425,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
|
||||
|
||||
+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);
|
||||
|
||||
+22
-4
@@ -130,6 +130,7 @@ 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;
|
||||
};
|
||||
@@ -138,6 +139,16 @@ constexpr int kCacheSize = 256;
|
||||
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;
|
||||
|
||||
// `fromCast` true: the SpellGo hook — authoritative caster + caster-modified
|
||||
// (talented) timing. False: the OnAuraAdded application hook — timing only,
|
||||
// no caster, and it must not clobber an entry SpellGo already owns (that
|
||||
@@ -149,9 +160,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.
|
||||
@@ -167,18 +181,17 @@ void Store(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
}
|
||||
}
|
||||
// Take a free slot, else an expired one, else evict round-robin.
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used || (e.expirationMs != 0 && now >= e.expirationMs)) {
|
||||
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};
|
||||
slot = {targetGuid, casterGuid, spellId, expirationMs, durationMs, now,
|
||||
kind, true};
|
||||
}
|
||||
|
||||
// ---- Out-of-range group-member aura snapshots ---------------------------
|
||||
@@ -775,9 +788,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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
+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 *>(
|
||||
|
||||
@@ -28,7 +28,7 @@ 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 +38,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) {
|
||||
|
||||
+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
|
||||
|
||||
+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