Compare commits

...
3 Commits
Author SHA1 Message Date
Brues b8bd0bce4e fix(unit): gate +0xE68 sub-struct reads on OBJECT_TYPE_PLAYER, not PLAYER_CONTROLLED
The CGPlayer sub-struct at [unit+0xE68] (guild key, PLAYER_FLAGS, quest
list, visible items) exists only on real player objects. Reads of it were
gated on UNIT_FLAG_PLAYER_CONTROLLED, but the server sets that flag on
pets, totems, guardians, companions, and MC'd creatures too (Turtle
UnitDefines.h; Pet.cpp/SpellEffects.cpp/SpellAuras.cpp setters) -- all
TYPEID_UNIT creatures with no such sub-struct. On one of them the +0xE68
pointer is garbage-but-non-null, so the null check passes and the deref
AVs (ERROR #132, pfUI issue #34: UnitIsInMyGuild on a nameplate pet/totem,
frequent in crowded dungeons/raids).

Add Unit::Flags::IsPlayerObject (m_objectType == OBJECT_TYPE_PLAYER via
[obj+0x14]) and use it as the gate at every arbitrary-unit +0xE68 reader:

  - UnitIsInMyGuild  (ReadGuildKey)      -- the reported crash
  - UnitIsAFK/UnitIsDND (TestPlayerFlag) -- same latent crash on a pet
  - IsUnitOnQuest    (Quest::Log::UnitHasQuest)
  - GetInventoryItemID (non-local unit visible-items path)

Local-player-only readers are unaffected. IsPlayerControlled stays for
minion/ownership classification (Unit::Pet), which wants the broad flag.
2026-08-05 20:53:30 -05:00
Brues c3a80053e9 equipment: Backported missing localization strings 2026-08-05 15:52:02 -05:00
Brues 5a3badb5bd feat(macro): add C_Macro.CreateMacro / EditMacro with string icons
The vanilla CreateMacro / EditMacro globals 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. C_Macro.CreateMacro / EditMacro
accept the icon as a texture string (a full Interface\Icons\<name> path
or a bare <name> basename) instead, writing the basename directly to the
macro struct; GetMacroInfo re-prefixes it as Interface\Icons\%s on read,
so any Interface\Icons\ texture round-trips.

Both go straight to the engine's own workers (FUN_004f1010 create /
FUN_004f1280 edit), which do the bounded copy, re-run the primary-spell
parser, set the persist-on-logout dirty flag, and refresh the action
bars. EditMacro resolves its index arg as a slot number or a macro name
(via the GetMacroIndexByName resolver), leaves any nil field unchanged,
and reads back the entry's local flag so the worker's unconditional
rewrite of it is a no-op.

The legacy globals stay untouched (index-only); string-icon callers opt
in through the C_Macro namespace. Verified in-game incl. an INV_* item
icon surviving a relog. Unblocks a pfUI macro-UI remodel.
2026-08-05 01:53:08 -05:00
15 changed files with 529 additions and 18 deletions
+25
View File
@@ -2,7 +2,26 @@ 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?";
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 +34,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 +65,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";
+25
View File
@@ -1,11 +1,34 @@
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";
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 +62,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";
+25
View File
@@ -2,7 +2,26 @@ 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\"?";
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 +34,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 +65,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";
+25
View File
@@ -2,6 +2,24 @@ 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 ?";
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 +30,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 +65,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";
+25
View File
@@ -2,6 +2,24 @@ if GetLocale() ~= "ruRU" then return end
BINDING_NAME_FOCUSTARGET = "Запомненная цель";
BINDING_NAME_TARGETFOCUS = "Восстановить цель";
CONFIRM_DELETE_EQUIPMENT_SET = "Вы действительно хотите удалить комплект экипировки \"%s\"?";
CONFIRM_SAVE_EQUIPMENT_SET = "Хотите сохранить комплект экипировки \"%s\"?";
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 +30,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 +65,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";
+25
View File
@@ -2,6 +2,24 @@ if GetLocale() ~= "zhCN" then return end
BINDING_NAME_FOCUSTARGET = "焦点目标";
BINDING_NAME_TARGETFOCUS = "目标焦点";
CONFIRM_DELETE_EQUIPMENT_SET = "你确认要删除装备方案%s吗?";
CONFIRM_SAVE_EQUIPMENT_SET = "你想要保存装备方案\"%s\"吗?";
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 +30,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 +65,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";
+25
View File
@@ -2,6 +2,24 @@ if GetLocale() ~= "zhTW" then return end
BINDING_NAME_FOCUSTARGET = "鎖定目標";
BINDING_NAME_TARGETFOCUS = "選定鎖定目標";
CONFIRM_DELETE_EQUIPMENT_SET = "你確定要刪除裝備設定:%s嗎?";
CONFIRM_SAVE_EQUIPMENT_SET = "你想要儲存裝備設定「%s」嗎?";
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 +30,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 +65,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";
+1 -1
View File
@@ -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` |
+55
View File
@@ -335,6 +335,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)
@@ -8117,6 +8118,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
+48
View File
@@ -3407,6 +3407,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
+8 -5
View File
@@ -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)
+190
View File
@@ -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
View File
@@ -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 *>(
+23 -6
View File
@@ -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
View File
@@ -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