mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 04:47:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ba82e59e4 | ||
|
|
b05c15ffa7 | ||
|
|
4daf6b9364 | ||
|
|
4c6c9deae6 | ||
|
|
983cc3bdff | ||
|
|
18ac033381 | ||
|
|
81434f7014 | ||
|
|
22605dad64 | ||
|
|
7378e0b128 | ||
|
|
94b0545631 | ||
|
|
243ab37836 | ||
|
|
a12fff6704 | ||
|
|
d747c699f5 | ||
|
|
88d76c04ad | ||
|
|
47d4b9c660 |
@@ -20,18 +20,19 @@ if C_AddOns.DoesAddOnExist('pfUI') then
|
||||
-- X-Website tag and left alone. The wrapper is removed as soon as the
|
||||
-- actionbar module finishes loading so it affects nothing else.
|
||||
local website = GetAddOnMetadata("pfUI", "X-Website")
|
||||
if not website or not string.find(GetAddOnMetadata("pfUI", "X-Website") or '', 'brues') then
|
||||
if not website or not strfind(website, 'brues') then
|
||||
local _createFrame = CreateFrame
|
||||
CreateFrame = function(frameType, name, parent, template)
|
||||
if frameType == "Button" and string.find(name or "", "pfActionBar") then
|
||||
local frame = _createFrame(frameType, name, parent, template)
|
||||
local HookedCreateFrame = function(frameType, name, parent, template)
|
||||
local frame = _createFrame(frameType, name, parent, template)
|
||||
if frameType == "Button" and strfind(name or "", "pfActionBar") then
|
||||
frame.HookScript = false
|
||||
return frame
|
||||
end
|
||||
return _createFrame(frameType, name, parent, template)
|
||||
return frame
|
||||
end
|
||||
hooksecurefunc(pfUI, 'LoadModule', function(frame, moduleName)
|
||||
if moduleName == "actionbar" then
|
||||
if moduleName == "updatenotify" then
|
||||
CreateFrame = HookedCreateFrame
|
||||
elseif moduleName == "actionbar" then
|
||||
CreateFrame = _createFrame
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -9,4 +9,45 @@ ITEMS_IN_INVENTORY = "%d |4Gegenstand:Gegenstände; im Inventar";
|
||||
ITEM_SLOTS_IGNORED = "%d |4Platz:Plätze; ignoriert";
|
||||
ITEM_MISSING = "%s fehlt";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4Gegenstand fehlt:Gegenstände fehlen;";
|
||||
TARGETFOCUS = "Fokussierte Einheit anvisieren";
|
||||
TARGETFOCUS = "Fokussierte Einheit anvisieren";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "K";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "G";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEM_DELTA_DESCRIPTION = "Wenn Ihr diesen Gegenstand ersetzt, werden sich folgende Werte ändern:";
|
||||
ITEM_MOD_MANA_SHORT = "Mana";
|
||||
ITEM_MOD_HEALTH_SHORT = "Gesundheit";
|
||||
ITEM_MOD_AGILITY_SHORT = "Beweglichkeit";
|
||||
ITEM_MOD_STRENGTH_SHORT = "Stärke";
|
||||
ITEM_MOD_INTELLECT_SHORT = "Intelligenz";
|
||||
ITEM_MOD_SPIRIT_SHORT = "Willenskraft";
|
||||
ITEM_MOD_STAMINA_SHORT = "Ausdauer";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "Angriffskraft";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "Distanzangriffskraft";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "Kritischer Trefferwert (Nah)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "Kritischer Trefferwert (Distanz)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "Trefferwert (Nah)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "Trefferwert (Distanz)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "Trefferwert (Zauber)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "Kritischer Trefferwert (Zauber)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "Bonusschaden";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "Bonusheilung";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "Manaregeneration";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Verteidigung";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Ausweichen";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parieren";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Blocken";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Blocken";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Schaden pro Sekunde";
|
||||
LARGE_NUMBER_SEPERATOR = ".";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "S";
|
||||
SLASH_CLEARFOCUS1 = "/fokusaufheben";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_EQUIP_SET1 = "/Ausrüstungsset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/fokus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Hier klicken, um einen Link zu Eurem Beruf zu erstellen.";
|
||||
|
||||
@@ -7,6 +7,30 @@ GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "g";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEM_DELTA_DESCRIPTION = "If you replace this item, the following stat changes will occur:";
|
||||
ITEM_MOD_MANA_SHORT = "Mana";
|
||||
ITEM_MOD_HEALTH_SHORT = "Health";
|
||||
ITEM_MOD_AGILITY_SHORT = "Agility";
|
||||
ITEM_MOD_STRENGTH_SHORT = "Strength";
|
||||
ITEM_MOD_INTELLECT_SHORT = "Intellect";
|
||||
ITEM_MOD_SPIRIT_SHORT = "Spirit";
|
||||
ITEM_MOD_STAMINA_SHORT = "Stamina";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "Attack Power";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "Ranged Attack Power";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "Critical Strike (Melee)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "Critical Strike (Ranged)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "Hit (Melee)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "Hit (Ranged)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "Hit (Spell)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "Critical Strike (Spell)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "Spell Damage";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "Spell Healing";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "Mana Regeneration";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Defense";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Dodge";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parry";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Block";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Block Value";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Damage Per Second";
|
||||
ITEMS_EQUIPPED = "%d |4item:items; equipped";
|
||||
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
|
||||
|
||||
@@ -9,4 +9,45 @@ ITEMS_IN_INVENTORY = "En inventario: %d |4objeto:objetos;";
|
||||
ITEM_SLOTS_IGNORED = "Ignorado: %d |4casilla:casillas;";
|
||||
ITEM_MISSING = "Falta: %s";
|
||||
ITEMS_NOT_IN_INVENTORY = "Faltan: %d |4objeto:objetos;";
|
||||
TARGETFOCUS = "Fijar unidad objetivo";
|
||||
TARGETFOCUS = "Fijar unidad objetivo";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "o";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
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";
|
||||
ITEM_MOD_AGILITY_SHORT = "agilidad";
|
||||
ITEM_MOD_STRENGTH_SHORT = "fuerza";
|
||||
ITEM_MOD_INTELLECT_SHORT = "intelecto";
|
||||
ITEM_MOD_SPIRIT_SHORT = "espíritu";
|
||||
ITEM_MOD_STAMINA_SHORT = "aguante";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "Poder de ataque";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "Poder de ataque a distancia";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "Golpe crítico (cuerpo a cuerpo)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "Golpe crítico (a distancia)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "Golpe (cuerpo a cuerpo)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "Golpe (a distancia)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "Golpe (hechizo)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "Golpe crítico (hechizo)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "Bon. daño";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "Bon. sanac.";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "Regeneración de maná";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Defensa";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Esquiva";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parada";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Bloqueo";
|
||||
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";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "p";
|
||||
SLASH_CLEARFOCUS1 = "/quitarenfoque";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_EQUIP_SET1 = "/conjequip";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/enfoque";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Haz clic aquí para crear un vínculo a tu profesión.";
|
||||
|
||||
@@ -9,4 +9,45 @@ ITEMS_IN_INVENTORY = "%d |4objet:objets; dans l'inventaire";
|
||||
ITEM_SLOTS_IGNORED = "%d |4emplacement ignoré:emplacements ignorés;";
|
||||
ITEM_MISSING = "Objet manquant : %s";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4objet:objets; manquants";
|
||||
TARGETFOCUS = "Cibler l'unité focalisée";
|
||||
TARGETFOCUS = "Cibler l'unité focalisée";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "o";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
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";
|
||||
ITEM_MOD_AGILITY_SHORT = "Agilité";
|
||||
ITEM_MOD_STRENGTH_SHORT = "Force";
|
||||
ITEM_MOD_INTELLECT_SHORT = "Intelligence";
|
||||
ITEM_MOD_SPIRIT_SHORT = "Esprit";
|
||||
ITEM_MOD_STAMINA_SHORT = "Endurance";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "Puissance d'attaque";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "Puissance d'attaque à distance";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "Score de critique (mêlée)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "Score de critique (distance)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "Toucher (mêlée)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "Toucher (distance)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "Toucher (sorts)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "Score de critique (sorts)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "Bonus aux dégâts";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "Bonus aux soins";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "Régénération du mana";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Défense";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Esquive";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parade";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Blocage";
|
||||
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";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "a";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/annulerfocal";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/équipens";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focal";
|
||||
LINK_TRADESKILL_TOOLTIP = "Cliquez ici pour créer un lien vers votre profession.";
|
||||
|
||||
@@ -9,4 +9,45 @@ ITEMS_IN_INVENTORY = "%d |4предмет:предмета:предметов;
|
||||
ITEM_SLOTS_IGNORED = "%d |4ячейка пропущена:ячейки пропущены:ячейки пропущены;";
|
||||
ITEM_MISSING = "Предмет для ячейки %s отсутствует";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4предмет отсутствует:предмета отсутствует:предметов отсутствуют;";
|
||||
TARGETFOCUS = "Восстановить цель";
|
||||
TARGETFOCUS = "Восстановить цель";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "м";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "з";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEM_DELTA_DESCRIPTION = "Если вы замените этот предмет, произойдут следующие изменения:";
|
||||
ITEM_MOD_MANA_SHORT = "к мане";
|
||||
ITEM_MOD_HEALTH_SHORT = "к здоровью";
|
||||
ITEM_MOD_AGILITY_SHORT = "к ловкости";
|
||||
ITEM_MOD_STRENGTH_SHORT = "к силе";
|
||||
ITEM_MOD_INTELLECT_SHORT = "к интеллекту";
|
||||
ITEM_MOD_SPIRIT_SHORT = "к духу";
|
||||
ITEM_MOD_STAMINA_SHORT = "к выносливости";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "к силе атаки";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "к силе атаки дальнего боя";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "к показателю критического удара в ближнем бою";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "к показателю критического удара в дальнем бою";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "к меткости в ближнем бою";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "к меткости в дальнем бою";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "к меткости заклинаний";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "к критическому эффекту заклинаний";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "к дополнительному урону";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "к дополнительному исцелению";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "к восполнению маны";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Защита";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Уклонение";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Парирование";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Блок";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "к показателю блокирования";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "к урону в секунду";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "с";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/сбросфокуса";
|
||||
SLASH_EQUIP_SET1 = "/комплект";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/фокус";
|
||||
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на вашу профессию.";
|
||||
|
||||
@@ -9,4 +9,45 @@ ITEMS_IN_INVENTORY = "背包中有%d件物品";
|
||||
ITEM_SLOTS_IGNORED = "忽略%d个栏位";
|
||||
ITEM_MISSING = "%s缺失";
|
||||
ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
TARGETFOCUS = "选中焦点目标";
|
||||
TARGETFOCUS = "选中焦点目标";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "铜";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "金";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEM_DELTA_DESCRIPTION = "如果你替换该物品,将会产生以下的属性变更:";
|
||||
ITEM_MOD_MANA_SHORT = "法力值";
|
||||
ITEM_MOD_HEALTH_SHORT = "生命值";
|
||||
ITEM_MOD_AGILITY_SHORT = "敏捷";
|
||||
ITEM_MOD_STRENGTH_SHORT = "力量";
|
||||
ITEM_MOD_INTELLECT_SHORT = "智力";
|
||||
ITEM_MOD_SPIRIT_SHORT = "精神";
|
||||
ITEM_MOD_STAMINA_SHORT = "耐力";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "攻击强度";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "远程攻击强度";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "爆击(近战)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "爆击(远程)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "命中(近战)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "命中(远程)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "命中(法术)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "爆击(法术)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "伤害加成";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "治疗加成";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "法力回复";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "防御";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "躲闪";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "招架";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "格挡";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "格挡值";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒伤害";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "银";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "点击这里以创建一个链接到你的专业的快捷方式。";
|
||||
|
||||
@@ -9,4 +9,45 @@ ITEMS_IN_INVENTORY = "%d件物品在物品欄中";
|
||||
ITEM_SLOTS_IGNORED = "忽略%d個欄位";
|
||||
ITEM_MISSING = "缺少%s物品";
|
||||
ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
TARGETFOCUS = "選定鎖定單位";
|
||||
TARGETFOCUS = "選定鎖定單位";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "銅";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "金";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEM_DELTA_DESCRIPTION = "若你更換此裝備,下列屬性將會改變:";
|
||||
ITEM_MOD_MANA_SHORT = "法力";
|
||||
ITEM_MOD_HEALTH_SHORT = "生命力";
|
||||
ITEM_MOD_AGILITY_SHORT = "敏捷";
|
||||
ITEM_MOD_STRENGTH_SHORT = "力量";
|
||||
ITEM_MOD_INTELLECT_SHORT = "智力";
|
||||
ITEM_MOD_SPIRIT_SHORT = "精神";
|
||||
ITEM_MOD_STAMINA_SHORT = "耐力";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "攻擊強度";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "遠程攻擊強度";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "致命一擊(近戰)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "致命一擊(遠程)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "命中(近戰)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "命中(遠程)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "命中(法術)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "致命一擊(法術)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "傷害加成";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "治療加成";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "法力恢復";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "防禦";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "閃躲";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "招架";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "格擋";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "格擋值";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒傷害";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "銀";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/清除專注目標";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/裝備套裝";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/專注目標";
|
||||
LINK_TRADESKILL_TOOLTIP = "點擊這裡以製造一個你專業技能的連結。";
|
||||
|
||||
@@ -39,21 +39,22 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem` (script) |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo` |
|
||||
| [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` |
|
||||
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit`, `C_Map.GetMapOverlays` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Screen](docs/API.md#screen) | `GetPhysicalScreenSize` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [Table](docs/API.md#table) | `table.wipe` |
|
||||
@@ -61,7 +62,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitGUID`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitDistanceSquared`, `UnitGUID`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterComboDuration` |
|
||||
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
|
||||
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
|
||||
|
||||
+347
-44
@@ -194,6 +194,7 @@ build instructions.
|
||||
- [`IsLeftAltKeyDown()` / `IsRightAltKeyDown()`](#isleftaltkeydown--isrightaltkeydown)
|
||||
- [`IsModifierKeyDown()`](#ismodifierkeydown)
|
||||
- [`IsMouseButtonDown([button])`](#ismousebuttondownbutton)
|
||||
- [`GetMouseButtonClicked()`](#getmousebuttonclicked)
|
||||
|
||||
- [Instance](#instance)
|
||||
- [`GetInstanceInfo()`](#getinstanceinfo)
|
||||
@@ -227,6 +228,7 @@ build instructions.
|
||||
- [`C_Item.GetStackCount(itemLocation)`](#c_itemgetstackcountitemlocation)
|
||||
- [`C_Item.IsBound(itemLocation)`](#c_itemisbounditemlocation)
|
||||
- [`C_Item.IsEquippableItem(item)`](#c_itemisequippableitemitem)
|
||||
- [`IsUsableItem(item)` / `C_Item.IsUsableItem(item)`](#isusableitemitem--c_itemisusableitemitem)
|
||||
- [`C_Item.IsEquippedItem(item)`](#c_itemisequippeditemitem)
|
||||
- [`C_Item.IsItemDataCachedByID(item)` / `C_Item.IsItemDataCached(itemLocation)`](#c_itemisitemdatacachedbyiditem--c_itemisitemdatacacheditemlocation)
|
||||
- [`C_Item.IsItemGUIDInInventory(itemGUID)`](#c_itemisitemguidininventoryitemguid)
|
||||
@@ -299,13 +301,15 @@ build instructions.
|
||||
- [`GetPlayerInfoByGUID(guid)`](#getplayerinfobyguidguid)
|
||||
- [`UnitNameFromGUID(guid)`](#unitnamefromguidguid)
|
||||
- [`C_PlayerCache.GetPlayerInfoByName(name)`](#c_playercachegetplayerinfobynamename)
|
||||
- [`C_PlayerInfo.GUIDIsPlayer(guid)` / `GUIDIsCreature` / `GUIDIsPet` / `GUIDIsGameObject`](#c_playerinfoguidisplayerguid--guidiscreature--guidispet--guidisgameobject)
|
||||
- [`C_PlayerCache.RememberPlayer(guid, name, classToken)`](#c_playercacherememberplayerguid-name-classtoken)
|
||||
- [`C_PlayerCache.SetEnabled(enabled)`](#c_playercachesetenabledenabled)
|
||||
- [`C_PlayerCache.IsEnabled()`](#c_playercacheisenabled)
|
||||
- [`C_PlayerCache.SetScanEnabled(enabled)`](#c_playercachesetscanenabledenabled)
|
||||
- [`C_PlayerCache.IsScanEnabled()`](#c_playercacheisscanenabled)
|
||||
|
||||
- [PlayerInfo](#playerinfo)
|
||||
- [`C_PlayerInfo.CanUseItem(itemID)`](#c_playerinfocanuseitemitemid)
|
||||
- [`C_PlayerInfo.GUIDIsPlayer(guid)` / `GUIDIsCreature` / `GUIDIsPet` / `GUIDIsGameObject`](#c_playerinfoguidisplayerguid--guidiscreature--guidispet--guidisgameobject)
|
||||
- [Quest](#quest)
|
||||
- [`C_QuestLog.GetQuestIDForLogIndex(index)`](#c_questlogGetQuestIDForLogIndexindex)
|
||||
- [`C_QuestLog.RequestLoadQuestByID(questID)`](#c_questlogrequestloadquestbyidquestid)
|
||||
@@ -356,6 +360,8 @@ build instructions.
|
||||
- [`C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`](#c_spellcancelspellbyidspellid--cancelspellbynamename)
|
||||
- [`C_Spell.UnitCastingInfo(unit)` / `C_Spell.CastingInfo()`](#c_spellunitcastinginfounit--c_spellcastinginfo)
|
||||
- [`C_Spell.UnitChannelInfo(unit)` / `C_Spell.ChannelInfo()`](#c_spellunitchannelinfounit--c_spellchannelinfo)
|
||||
- [`C_Spell.GetSpellLevelInfo(spellID)`](#c_spellgetspelllevelinfospellid)
|
||||
- [`GetSpellRequiredTargetLevel(spellID)`](#getspellrequiredtargetlevelspellid)
|
||||
|
||||
- [SpellBook](#spellbook)
|
||||
- [`FindSpellBookSlotByID(spellID)`](#findspellbookslotbyidspellid)
|
||||
@@ -418,8 +424,12 @@ build instructions.
|
||||
- [`UnitIsFeignDeath(unit)`](#unitisfeigndeathunit)
|
||||
- [`UnitIsInMyGuild(unitOrName)`](#unitisinmyguildunitorname)
|
||||
- [`UnitIsPossessed(unit)`](#unitispossessedunit)
|
||||
- [`UnitIsMinion(unit)`](#unitisminionunit)
|
||||
- [`UnitIsPet(unit)`](#unitispetunit)
|
||||
- [`UnitIsOtherPlayersPet(unit)`](#unitisotherplayerspetunit)
|
||||
- [`UnitStandState(unit)`](#unitstandstateunit)
|
||||
- [`UnitInRange(unit)`](#unitinrangeunit)
|
||||
- [`UnitDistanceSquared(unit)`](#unitdistancesquaredunit)
|
||||
- [`UnitPower(unit [, powerType])` / `UnitPowerMax(unit [, powerType])`](#unitpowerunit--powertype--unitpowermaxunit--powertype)
|
||||
- [`UnitPowerType(unit)`](#unitpowertypeunit)
|
||||
|
||||
@@ -2334,7 +2344,7 @@ polling. Register like any engine event
|
||||
|---|---|---|
|
||||
| `LOOT_HISTORY_ROLL_CHANGED` | `itemIndex`, `playerIndex` (numbers) | A player rolled or passed on an item. |
|
||||
| `LOOT_HISTORY_ROLL_COMPLETE` | `itemIndex` (number) | The item was decided — won or all-passed. |
|
||||
| `LOOT_HISTORY_FULL_UPDATE` | — | Reserved for API / `IsEventValid` completeness (the display frame fires it on open); the DLL doesn't dispatch it. |
|
||||
| `LOOT_HISTORY_FULL_UPDATE` | — | The item set changed structurally — a new item appeared (roll opened) or the 128-item ring evicted the oldest (every index shifted). Re-read the whole list. Also fired by `C_LootHistory.Clear()`. |
|
||||
|
||||
See the [LootHistory](#loothistory) section for the reconstruction and read API.
|
||||
|
||||
@@ -4288,6 +4298,38 @@ if IsMouseButtonDown("RightButton") then
|
||||
end
|
||||
```
|
||||
|
||||
### `GetMouseButtonClicked()`
|
||||
|
||||
Returns the name of the button responsible for the mouse handler
|
||||
currently running (`"LeftButton"`, `"RightButton"`, `"MiddleButton"`,
|
||||
`"Button4"`, `"Button5"`), or `nil` when no click is being handled.
|
||||
|
||||
Modern addons call this inside a mouse handler (`OnClick`,
|
||||
`OnMouseDown`, `OnMouseUp`, `OnDragStart`, …) to learn which button
|
||||
drove it — an alternative to reading vanilla's `arg1`, and readable
|
||||
from nested helper functions where `arg1` isn't in scope. The value
|
||||
is captured by the same `WH_GETMESSAGE` hook behind
|
||||
`GLOBAL_MOUSE_DOWN` / `GLOBAL_MOUSE_UP`, then evicted a couple of
|
||||
frames later so — matching the real API — it reads `nil` outside a
|
||||
handler rather than lingering as the last click forever.
|
||||
|
||||
The button stays readable for the whole frame its click lands in (so
|
||||
multiple addons hooking the same `OnClick` all see it), and while a
|
||||
button is physically held (so `OnDragStart` reads it). It is a
|
||||
best-effort replica: WoW dispatches these handlers deferred from the
|
||||
OS message, so eviction is time-based (per-frame) rather than exactly
|
||||
bracketed around the handler as on retail. Values can therefore
|
||||
linger ~2 frames past a click — harmless, since it's only meaningful
|
||||
inside a mouse handler.
|
||||
|
||||
```lua
|
||||
button:SetScript("OnClick", function()
|
||||
if GetMouseButtonClicked() == "RightButton" then
|
||||
-- right-click behavior
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
## Instance
|
||||
|
||||
### `GetInstanceInfo()`
|
||||
@@ -5278,6 +5320,53 @@ C_Item.IsEquippableItem(12640) -- Lionheart Helm → true
|
||||
C_Item.IsEquippableItem(6948) -- Hearthstone → false
|
||||
```
|
||||
|
||||
### `IsUsableItem(item)` / `C_Item.IsUsableItem(item)`
|
||||
|
||||
Returns `(usable, noMana)` for an item's **on-use** ability — the item
|
||||
analog of [`IsUsableSpell`](#isusablespellspell--isusablespellslot-booktype).
|
||||
This asks "can I click this item right now?", **not** "can I equip it".
|
||||
Armor proficiency, class-restricted gear, and the like are *not* this
|
||||
function's concern (that's the tooltip's red requirement lines); only an
|
||||
item's usable/right-click effect matters here.
|
||||
|
||||
The global `IsUsableItem(item)` returns `1`/`nil` pairs (matching stock
|
||||
`IsUsableAction`); `C_Item.IsUsableItem(item)` returns proper booleans
|
||||
per the `C_Item.*` convention. `item` is an itemID number or
|
||||
`"item:N..."` link.
|
||||
|
||||
```lua
|
||||
IsUsableItem(6948) -- Hearthstone → 1, nil
|
||||
local usable, noMana = C_Item.IsUsableItem(13446) -- Major Healing Potion
|
||||
-- usable=true, noMana=false → click it
|
||||
-- usable=false, noMana=false → no on-use effect, below required level,
|
||||
-- class/race-restricted, dead, on a form
|
||||
-- that blocks it, etc.
|
||||
```
|
||||
|
||||
> **What this function checks:**
|
||||
>
|
||||
> 1. The item has an **on-use** spell (an `ItemStats` spell slot with
|
||||
> trigger `ON_USE`). Items with no usable effect — plain gear,
|
||||
> materials, on-equip procs — return `(nil, nil)`.
|
||||
> 2. The item's **use requirements** are met: RequiredLevel, and the
|
||||
> AllowableClass / AllowableRace masks. (These gate *activation* —
|
||||
> a too-low-level potion, a class-specific trinket — so they belong
|
||||
> here, unlike equip-only requirements.)
|
||||
> 3. The on-use spell is castable now — delegated to the engine's own
|
||||
> spell-castability helper (`FUN_SPELL_IS_USABLE`, `0x006E3D60`),
|
||||
> which folds in shapeshift/form, mechanic immunities, aura state,
|
||||
> and the power check. Insufficient power is the *only* condition
|
||||
> that sets `noMana=true`.
|
||||
>
|
||||
> Like `C_Item.IsEquippableItem`, this is a synchronous cache peek:
|
||||
> uncached items return `(nil/false, nil/false)` with no async load
|
||||
> fired. Call `C_Item.RequestLoadItemDataByID` first and re-check on
|
||||
> `ITEM_DATA_LOAD_RESULT` if you need it to wait.
|
||||
>
|
||||
> The on-use spell lookup is shared with
|
||||
> [`C_Item.GetItemSpell`](#c_itemgetitemspellitem)
|
||||
> (`Item::Spell::FindOnUseSpellIDInRecord`).
|
||||
|
||||
### `C_Item.IsEquippedItem(item)`
|
||||
|
||||
Returns `true` if `item` is currently equipped in any of the 19
|
||||
@@ -6196,13 +6285,23 @@ Returns the number of rolled items currently in the history.
|
||||
|
||||
### `C_LootHistory.GetItem(itemIndex)`
|
||||
|
||||
`1`-based. Returns `itemLink, numPlayers, isDone, winnerName`:
|
||||
`1`-based. Returns `rollID, itemLink, numPlayers, isDone, winnerIdx, timestamp`
|
||||
(matches the MoP signature, with `timestamp` as a ClassicAPI extension):
|
||||
- `rollID` — a **stable** monotonic ID for the roll. Unlike `itemIndex` (which
|
||||
shifts as the 128-item ring saturates), `rollID` never changes, so key
|
||||
"keep this row expanded" / "highlight this roll" state on it.
|
||||
- `itemLink` — `item:<id>:0:<suffix>:<unique>` (valid for `GetItemInfo` /
|
||||
`GameTooltip:SetHyperlink`), carrying the roll's random suffix so
|
||||
"of the X" items keep their affix.
|
||||
- `numPlayers` — how many players have rolled/passed so far.
|
||||
- `isDone` — `true` once the item is decided (won or all-passed).
|
||||
- `winnerName` — the winner's name, or `nil` if undecided / all passed.
|
||||
- `winnerIdx` — the winner's `1`-based **player index**, for
|
||||
`C_LootHistory.GetPlayerInfo(itemIndex, winnerIdx)` (name, class token,
|
||||
winning roll). `nil` if undecided / all passed. The winner is guaranteed to
|
||||
be in the player list even if their individual roll packet never arrived.
|
||||
- `timestamp` — the roll's creation time in seconds, directly comparable to
|
||||
`GetTime()` (e.g. `GetTime() - timestamp` for "N seconds ago"). ClassicAPI
|
||||
extension — no MoP equivalent.
|
||||
|
||||
Returns nothing for an out-of-range index.
|
||||
|
||||
@@ -6219,6 +6318,12 @@ Both `1`-based. Returns `name, class, rollType, roll, isWinner, isMe`:
|
||||
|
||||
Returns nothing for an out-of-range item or player index.
|
||||
|
||||
### `C_LootHistory.Clear()`
|
||||
|
||||
Wipes the accumulated roll history (returns nothing). A ClassicAPI extension
|
||||
with no MoP equivalent, for a "clear history" button. Fires
|
||||
`LOOT_HISTORY_FULL_UPDATE` so any open display rebuilds empty.
|
||||
|
||||
## LossOfControl
|
||||
|
||||
Backports `C_LossOfControl` — the active crowd-control / interrupt effects on
|
||||
@@ -6965,12 +7070,18 @@ Three population sources feed the cache when fully enabled:
|
||||
Storage:
|
||||
|
||||
- `WTF\Account\<account>\ClassicAPI.txt` — account-level settings
|
||||
(`PersistentNameCacheEnabled`, `NameCacheScanEnabled`).
|
||||
- `WTF\Account\<account>\<realm>\ClassicAPI_NameCache.txt` —
|
||||
per-realm cache. Tab-separated text, ~30 bytes/entry,
|
||||
hand-editable. Shared across all characters on the same
|
||||
account+realm (a 50-character bank alt's chat scraping doesn't
|
||||
double the storage cost).
|
||||
(`PersistentNameCacheEnabled`, `NameCacheScanEnabled`). The
|
||||
opt-in toggles stay per-account.
|
||||
- `WTF\ClassicAPI\<realmList>\<realm>\ClassicAPI_NameCache.txt` —
|
||||
per-realm cache, **shared across every account on the same
|
||||
server**. Keyed by the realmlist address (unambiguous server
|
||||
identity, can't collide across unrelated private servers) plus the
|
||||
realm name — so alts on different accounts pool into one file
|
||||
rather than each rebuilding their own. Tab-separated text,
|
||||
~30 bytes/entry, hand-editable. On first load with no shared file
|
||||
yet, a pre-existing per-account cache
|
||||
(`WTF\Account\<account>\<realm>\ClassicAPI_NameCache.txt`) is
|
||||
migrated in automatically.
|
||||
|
||||
### `GetPlayerInfoByGUID(guid)`
|
||||
|
||||
@@ -7155,40 +7266,6 @@ here: GUIDs are permanent for the life of a vanilla character, so
|
||||
the new character has a different GUID and gets a different cache
|
||||
entry.
|
||||
|
||||
### `C_PlayerInfo.GUIDIsPlayer(guid)` / `GUIDIsCreature` / `GUIDIsPet` / `GUIDIsGameObject`
|
||||
|
||||
Type checks on the raw 1.12 GUID format. Vanilla GUIDs encode the
|
||||
entity type in the high 16 bits of the qword — players have
|
||||
`0x0000` (low dword = player ID), creatures `0xF130xxxx`, pets
|
||||
`0xF140xxxx`, game objects (herbs / chests / etc.) `0xF110xxxx`,
|
||||
items `0x4000xxxx`. Each function returns `true` only for its
|
||||
specific prefix; the `"0x0000000000000000"` sentinel returns
|
||||
`false` from all four.
|
||||
|
||||
```lua
|
||||
if C_PlayerInfo.GUIDIsPlayer(UnitGUID("target")) then
|
||||
-- target is a player
|
||||
end
|
||||
|
||||
-- Combat-log row triage: was that a player kill or a mob kill?
|
||||
local function OnCombatLogEvent(_, _, eventType, srcGUID, ...)
|
||||
if eventType == "PARTY_KILL" then
|
||||
if C_PlayerInfo.GUIDIsPlayer(srcGUID) then ... end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
`GUIDIsPlayer` matches modern WoW's signature exactly; the other
|
||||
three are companions for the most common type-distinction needs.
|
||||
For other types (corpse, dynamic object, transport, item) the
|
||||
underlying classifier exists internally — let us know if you need
|
||||
one exposed.
|
||||
|
||||
Accepts either the 16-digit `"0xHHHHHHHHLLLLLLLL"` form or the
|
||||
8-digit `"0xLLLLLLLL"` shortcut (high dword implicitly zero).
|
||||
Malformed input returns `false` rather than raising — matching
|
||||
modern's tolerance for stale GUIDs from addon-side caches.
|
||||
|
||||
### `C_PlayerCache.SetEnabled(enabled)`
|
||||
|
||||
Opts into (or out of) the persistent name cache. `enabled` is a
|
||||
@@ -7284,6 +7361,88 @@ if C_PlayerCache.IsScanEnabled()
|
||||
end
|
||||
```
|
||||
|
||||
## PlayerInfo
|
||||
|
||||
### `C_PlayerInfo.CanUseItem(itemID)`
|
||||
|
||||
Returns `true` if the local player meets the item's **use/equip
|
||||
requirements**, `false` otherwise. This is the "would the item be red in
|
||||
the tooltip" gate, and rounds out the item-usability trio:
|
||||
|
||||
| Function | Question it answers |
|
||||
|----------|---------------------|
|
||||
| `C_Item.IsEquippableItem(item)` | Does the item fit *some* slot? (static, player-independent) |
|
||||
| `IsUsableItem(item)` | Is the item's *on-use* ability castable right now? |
|
||||
| `C_PlayerInfo.CanUseItem(itemID)` | May *this* player equip/use it at all? |
|
||||
|
||||
So this is what answers "a Mail chest on a Mage" (armor proficiency), "a
|
||||
level-40 item at level 20" (RequiredLevel), and class/race-restricted gear.
|
||||
|
||||
`itemID` is a number (item links are also accepted). Checks performed:
|
||||
|
||||
1. **Proficiency** — for weapons/armor, the engine keeps a per-item-class
|
||||
subclass-proficiency bitmask (fed by `SMSG_SET_PROFICIENCY`); the item's
|
||||
subclass bit must be set. Item classes with no proficiency concept
|
||||
(consumables, trade goods, …) are unrestricted.
|
||||
2. **RequiredLevel** ≤ player level.
|
||||
3. **AllowableClass / AllowableRace** masks include the player.
|
||||
4. **RequiredSkill** — the player must have the item's skill line at an
|
||||
effective rank ≥ `RequiredSkillRank` (e.g. a mount that "Requires
|
||||
Riding (150)", a tool that "Requires Engineering (200)").
|
||||
5. **RequiredSpell** — the player must know the item's prerequisite spell
|
||||
(or a higher rank of it): a crafting specialization ("Requires
|
||||
Armorsmith") or a proficiency spell.
|
||||
6. **RequiredHonorRank / RequiredCityRank** — PvP-rank gates (the player's
|
||||
honor rank and earned PvP-medal bitmask).
|
||||
7. **RequiredReputation** — current standing with the item's faction must
|
||||
reach the required reaction band (e.g. "Requires Honored with …").
|
||||
|
||||
```lua
|
||||
C_PlayerInfo.CanUseItem(12640) -- Lionheart Helm (plate) → true on a Warrior, false on a Mage
|
||||
C_PlayerInfo.CanUseItem(6948) -- Hearthstone → true (no restrictions)
|
||||
C_PlayerInfo.CanUseItem(19872) -- a mount → false without Riding (150)
|
||||
C_PlayerInfo.CanUseItem(12717) -- Plans: Lionheart Helm → false without the Armorsmith spec
|
||||
```
|
||||
|
||||
> Synchronous, like `C_Item.IsEquippableItem`: an uncached item returns
|
||||
> `false` with no async load fired. Warm the cache
|
||||
> (`C_Item.RequestLoadItemDataByID`) and re-check on `ITEM_DATA_LOAD_RESULT`
|
||||
> if needed.
|
||||
|
||||
### `C_PlayerInfo.GUIDIsPlayer(guid)` / `GUIDIsCreature` / `GUIDIsPet` / `GUIDIsGameObject`
|
||||
|
||||
Type checks on the raw 1.12 GUID format. Vanilla GUIDs encode the
|
||||
entity type in the high 16 bits of the qword — players have
|
||||
`0x0000` (low dword = player ID), creatures `0xF130xxxx`, pets
|
||||
`0xF140xxxx`, game objects (herbs / chests / etc.) `0xF110xxxx`,
|
||||
items `0x4000xxxx`. Each function returns `true` only for its
|
||||
specific prefix; the `"0x0000000000000000"` sentinel returns
|
||||
`false` from all four.
|
||||
|
||||
```lua
|
||||
if C_PlayerInfo.GUIDIsPlayer(UnitGUID("target")) then
|
||||
-- target is a player
|
||||
end
|
||||
|
||||
-- Combat-log row triage: was that a player kill or a mob kill?
|
||||
local function OnCombatLogEvent(_, _, eventType, srcGUID, ...)
|
||||
if eventType == "PARTY_KILL" then
|
||||
if C_PlayerInfo.GUIDIsPlayer(srcGUID) then ... end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
`GUIDIsPlayer` matches modern WoW's signature exactly; the other
|
||||
three are companions for the most common type-distinction needs.
|
||||
For other types (corpse, dynamic object, transport, item) the
|
||||
underlying classifier exists internally — let us know if you need
|
||||
one exposed.
|
||||
|
||||
Accepts either the 16-digit `"0xHHHHHHHHLLLLLLLL"` form or the
|
||||
8-digit `"0xLLLLLLLL"` shortcut (high dword implicitly zero).
|
||||
Malformed input returns `false` rather than raising — matching
|
||||
modern's tolerance for stale GUIDs from addon-side caches.
|
||||
|
||||
## Quest
|
||||
|
||||
### `C_QuestLog.GetQuestIDForLogIndex(index)`
|
||||
@@ -8807,6 +8966,55 @@ otherwise it falls back to `name`/`displayName`/`textureID`/`spellID` with
|
||||
**`nil` times**. The player path is unchanged (full timing). Same
|
||||
placeholder fields as `C_Spell.UnitCastingInfo`.
|
||||
|
||||
### `C_Spell.GetSpellLevelInfo(spellID)`
|
||||
|
||||
Returns the raw `Spell.dbc` level fields for a spell:
|
||||
|
||||
```
|
||||
spellLevel, baseLevel, maxLevel = C_Spell.GetSpellLevelInfo(spellID)
|
||||
```
|
||||
|
||||
- `spellLevel` — the rank's effective level (drives the target-level rule
|
||||
below and per-level scaling).
|
||||
- `baseLevel` — the spell's base level.
|
||||
- `maxLevel` — the level at which per-level scaling caps (`0` = no cap).
|
||||
|
||||
Returns `nil` for an invalid / unpopulated spellID.
|
||||
|
||||
```lua
|
||||
C_Spell.GetSpellLevelInfo(14752) -- 30, 30, 40 (Divine Spirit Rank 1)
|
||||
C_Spell.GetSpellLevelInfo(14818) -- 40, 40, 50 (Divine Spirit Rank 2)
|
||||
```
|
||||
|
||||
### `GetSpellRequiredTargetLevel(spellID)`
|
||||
|
||||
Also `C_Spell.GetSpellRequiredTargetLevel(spellID)`. Returns the minimum
|
||||
level a **target** must be for this exact spell (rank) to apply — e.g.
|
||||
Divine Spirit Rank 1 can only be cast on a target level **20 or higher**.
|
||||
Returns `0` when the spell has no such restriction, or `nil` for an
|
||||
invalid spellID.
|
||||
|
||||
```lua
|
||||
GetSpellRequiredTargetLevel(14752) -- 20 (Divine Spirit Rank 1)
|
||||
GetSpellRequiredTargetLevel(14818) -- 30 (Divine Spirit Rank 2)
|
||||
GetSpellRequiredTargetLevel(1243) -- 0 (Fortitude Rank 1)
|
||||
GetSpellRequiredTargetLevel(10060) -- 0 (Power Infusion — single rank)
|
||||
GetSpellRequiredTargetLevel(133) -- 0 (Fireball — damage, not a buff)
|
||||
```
|
||||
|
||||
Mirrors the server's `SelectAuraRankForLevel` rule: a **ranked beneficial
|
||||
aura** is castable when `targetLevel + 10 >= spellLevel`, so the minimum
|
||||
target level is `spellLevel - 10`. The restriction is applied only when the
|
||||
spell is a ranked positive aura — `spellLevel > 10`, not passive, part of a
|
||||
rank chain, and applying an aura to a friendly/party/raid target (or an
|
||||
`APPLY_AREA_AURA_PARTY` effect). Below the minimum, the lowest rank fails
|
||||
with `SPELL_FAILED_LOWLEVEL`; higher ranks auto-downrank.
|
||||
|
||||
> The explicit `MinTargetLevel` / `MaxTargetLevel` spell fields the server
|
||||
> also enforces are **server-only** columns — they aren't present in the
|
||||
> 1.12 client's `Spell.dbc`, so the `spellLevel` rule is the only
|
||||
> client-readable target-level mechanism.
|
||||
|
||||
## SpellBook
|
||||
|
||||
Functions keyed on the player or pet **spellbook** (slot indices,
|
||||
@@ -9919,6 +10127,63 @@ Distinct from `UnitIsCharmed`: charm covers any charm-type effect
|
||||
(including pets summoned via mob-charm spells), possess is the
|
||||
specific spell-driven take-over effect modern WoW splits out.
|
||||
|
||||
### `UnitIsMinion(unit)`
|
||||
|
||||
Returns `true` if `unit` is a **minion of a player** — a pet, guardian,
|
||||
totem, or charmed creature — `false` otherwise. Primarily owner-based: the
|
||||
unit's owner GUID (SummonedBy / CreatedBy / CharmedBy) must resolve to a
|
||||
player, with "player-controlled but not itself a player" as a fallback.
|
||||
Owner-based is needed because summoned **guardians** (shown as "X's
|
||||
Minion") carry an owner but not the `PLAYER_CONTROLLED` flag, so a
|
||||
flag-only check would miss them. Players, NPCs, and world creatures return
|
||||
`false`.
|
||||
|
||||
A modern (12.0.0-era) addition with no vanilla or 3.3.5 equivalent —
|
||||
backported here from vanilla primitives. A bad or unresolved unit token
|
||||
returns `false`.
|
||||
|
||||
```lua
|
||||
UnitIsMinion("pet") -- your pet → true
|
||||
UnitIsMinion("player") -- you → false
|
||||
UnitIsMinion("target") -- another player's felhunter → true; a boar → false
|
||||
```
|
||||
|
||||
### `UnitIsPet(unit)`
|
||||
|
||||
Returns `true` if `unit` is a **pet** of a player (as opposed to a
|
||||
guardian/totem "minion"), `false` otherwise — the pet↔minion
|
||||
discriminator. Uses the engine's own classifier: owned by a player **and**
|
||||
the family-based pet/minion test that drives the `UNITNAME_TITLE_PET` vs
|
||||
`UNITNAME_TITLE_MINION` tooltip label. Neither the GUID (pets and guardians
|
||||
share the `0xF14…` prefix on this client) nor `UNIT_FLAG_PLAYER_CONTROLLED`
|
||||
(a "Minion" can carry it) reliably separates them, so the creature-family
|
||||
classifier is the correct signal; the owner-is-player gate also excludes
|
||||
wild tameable beasts of the same family.
|
||||
|
||||
`UnitIsMinion` is the umbrella (pet + guardian + totem); `UnitIsPet` is the
|
||||
controllable subset. A ClassicAPI extension, not a stock WoW global. A bad
|
||||
or unresolved unit token returns `false`.
|
||||
|
||||
```lua
|
||||
UnitIsPet("pet") -- your pet → true
|
||||
-- another player's "X's Pet" → UnitIsMinion true, UnitIsPet true
|
||||
-- another player's "X's Minion" → UnitIsMinion true, UnitIsPet false (guardian, not controllable)
|
||||
```
|
||||
|
||||
### `UnitIsOtherPlayersPet(unit)`
|
||||
|
||||
Returns `true` if `unit` is a minion (pet, guardian, totem, or charmed
|
||||
creature) whose owner is a player **other than you**, `false` otherwise.
|
||||
Owner-based, matching 5.4.8's `Script_UnitIsOtherPlayersPet`: it reads the
|
||||
unit's owner GUID (SummonedBy / CreatedBy / CharmedBy), requires that owner
|
||||
to be a player, and requires it to differ from you. World creatures and
|
||||
players (no owner) return `false`; your own minions (owner is you) return
|
||||
`false`. A bad or unresolved unit token returns `false`.
|
||||
|
||||
```lua
|
||||
UnitIsOtherPlayersPet("target") -- a party member's pet → true; your own pet → false
|
||||
```
|
||||
|
||||
### `UnitStandState(unit)`
|
||||
|
||||
Returns the unit's standstate as an integer, matching the modern
|
||||
@@ -9986,6 +10251,44 @@ mouseover, etc.). For raid members in a different zone or otherwise
|
||||
not in the engine's sync window, the position virtual returns null
|
||||
and the function reports `(false, false)`.
|
||||
|
||||
### `UnitDistanceSquared(unit)`
|
||||
|
||||
Returns `(distanceSquared, checkedPosition)` — the **squared** world
|
||||
distance (yards²) from the player to `unit`, and a flag indicating
|
||||
whether both positions were available.
|
||||
|
||||
```lua
|
||||
local distSq, checked = UnitDistanceSquared("target")
|
||||
if checked and distSq <= 30 * 30 then
|
||||
-- target is within 30 yards
|
||||
end
|
||||
```
|
||||
|
||||
| Return | Meaning |
|
||||
|--------|---------|
|
||||
| `distanceSquared` | Squared distance player→unit. `0` when `checkedPosition` is `false` (a placeholder — matches retail's "always a number" shape). |
|
||||
| `checkedPosition` | `true` when both positions were read. `false` when `unit`'s position isn't available (empty `partyN` slot, no target, raid member outside the sync window, etc.). |
|
||||
|
||||
The value is **squared** on purpose: nearly all distance logic is a
|
||||
threshold compare (`distSq <= range * range`) or a nearest-unit sort,
|
||||
neither of which needs the square root — so retail exposes only the
|
||||
squared form (added 5.0.4) and never a plain `UnitDistance`. Take
|
||||
`math.sqrt(distanceSquared)` only when you need a yard number to show a
|
||||
human.
|
||||
|
||||
Reads world positions via the `CGObject::GetPosition` vtable virtual
|
||||
(slot 5, offset `+0x14`) — the same path `UnitInRange` /
|
||||
`CheckInteractDistance` use. **Center-to-center**, not reach-aware (that
|
||||
edge-to-edge nicety is UnitXP_SP3's niche); the value equals what you'd
|
||||
compute from SuperWoW's `UnitPosition`, but it's self-contained (no
|
||||
sibling-DLL dependency, and no clash with SuperWoW's own `UnitPosition`,
|
||||
which is left untouched).
|
||||
|
||||
> No self-quirk (unlike `UnitInRange`): `UnitDistanceSquared("player")`
|
||||
> returns a legitimate `(0, true)`. Because a real `0` (self, or two
|
||||
> exactly co-located units) is indistinguishable by value from the miss
|
||||
> placeholder, always branch on `checkedPosition`.
|
||||
|
||||
### `UnitClassBase(unit)`
|
||||
|
||||
Returns `(classFile, classID)` — the locale-independent class
|
||||
|
||||
+2
-1
@@ -56,7 +56,8 @@ namespace Lua {
|
||||
F(NewThread, lua_newthread, LUA_NEW_THREAD) \
|
||||
F(Resume, lua_resume, LUA_RESUME) \
|
||||
F(Yield, lua_yield, LUA_YIELD) \
|
||||
F(ArgError, luaL_argerror, LUAL_ARG_ERROR)
|
||||
F(ArgError, luaL_argerror, LUAL_ARG_ERROR) \
|
||||
F(SetN, luaL_setn, LUAL_SETN)
|
||||
|
||||
#define CLASSICAPI_BIND_LUA(Name, Typedef, Offset) \
|
||||
const Typedef##_t Name = reinterpret_cast<Typedef##_t>(Offsets::Offset);
|
||||
|
||||
@@ -99,6 +99,7 @@ using lua_newthread_t = void *(__fastcall *)(void *L);
|
||||
using lua_resume_t = int(__fastcall *)(void *L, int nargs);
|
||||
using lua_yield_t = int(__fastcall *)(void *L, int nresults);
|
||||
using luaL_argerror_t = void(__fastcall *)(void *L, int narg, const char *msg);
|
||||
using luaL_setn_t = void(__fastcall *)(void *L, int t, int n);
|
||||
|
||||
extern const lua_isnumber_t IsNumber;
|
||||
extern const lua_isstring_t IsString;
|
||||
@@ -140,6 +141,7 @@ extern const lua_newthread_t NewThread;
|
||||
extern const lua_resume_t Resume;
|
||||
extern const lua_yield_t Yield;
|
||||
extern const luaL_argerror_t ArgError;
|
||||
extern const luaL_setn_t SetN;
|
||||
|
||||
// Returns the global `lua_State *` (read on demand from the engine's global).
|
||||
// Callable outside a Lua callback, e.g. during LoadScriptFunctions setup.
|
||||
|
||||
@@ -504,6 +504,29 @@ enum Offsets {
|
||||
// layout.
|
||||
OFF_UNIT_FIELD_TARGET = 0x28,
|
||||
|
||||
// Owner/controller GUID fields within `m_objectFields`, each a 64-bit
|
||||
// GUID (lo/hi). Both byte-verified from engine readers:
|
||||
// CHARMEDBY +0x10 — `Script_UnitIsCharmed`
|
||||
// (`mov ecx,[eax+0x10]; or ecx,[eax+0x14]; jz`).
|
||||
// CREATEDBY +0x20 — the unit owner/title builder `FUN_0052FD30`
|
||||
// reads `[desc+0x20]`/`[+0x24]` as the summoner GUID.
|
||||
// A pet/guardian/totem's owner lives in CREATEDBY; a charmed unit's in
|
||||
// CHARMEDBY. `Unit::Pet` reads these to find a minion's player owner.
|
||||
// (The engine uses only these two for ownership — there's a GUID slot
|
||||
// at +0x18 too, but nothing reads it as an owner, so it's left out.)
|
||||
OFF_UNIT_FIELD_CHARMEDBY = 0x10,
|
||||
OFF_UNIT_FIELD_CREATEDBY = 0x20,
|
||||
|
||||
// Pet-vs-minion discriminator for an owned unit: `int __fastcall(unit)`.
|
||||
// Returns 1 for a controllable **pet** (classified by creature family
|
||||
// at descriptor +0x212 → family DBC, with a ChrRaces fallback), a
|
||||
// non-1 value otherwise. The engine's unit-title builder `FUN_0052FD30`
|
||||
// uses it as `(fn(unit) != 1) + 1` → 1 = "X's Pet" title, 2 = "X's
|
||||
// Minion" — so it's the exact signal separating pet from minion, which
|
||||
// neither the GUID (pets and guardians share the 0xF14… prefix) nor
|
||||
// UNIT_FLAG_PLAYER_CONTROLLED reliably does.
|
||||
FUN_UNIT_PET_MINION_CLASS = 0x00605570,
|
||||
|
||||
// Party / raid roster counts and the party GUID array referenced
|
||||
// in the `FUN_TOKEN_TO_GUID` dispatch comment above. Used by
|
||||
// `UnitTokenFromGUID` to cap its candidate iteration — solo
|
||||
@@ -903,6 +926,22 @@ enum Offsets {
|
||||
// folds in the descriptor cost mods).
|
||||
FUN_GET_SPELL_COST = 0x006E31B0,
|
||||
|
||||
// Full spell-castability check for the LOCAL player:
|
||||
// `char __fastcall(const uint8_t *spellRecord /*ecx*/,
|
||||
// int *outNoMana /*edx*/)`.
|
||||
// Resolves the local player internally (no unit arg), then gates
|
||||
// on casting state, stun/confuse flags, mechanic immunities,
|
||||
// shapeshift/form + required stances, aura-state requirements,
|
||||
// combo points (finishers), spell RequiredSkill, and finally the
|
||||
// power check: `cost(FUN_GET_SPELL_COST) <= currentPower` → usable,
|
||||
// else sets `*outNoMana = 1`. Returns nonzero (low byte) when
|
||||
// usable. Does NOT check spell knowledge — so it's valid to feed
|
||||
// an item's on-use spell record (which the player never "knows").
|
||||
// This is the helper the action-usability recompute uses for
|
||||
// player spell slots; `Item::Usable` reuses it for item on-use
|
||||
// spells. Returns 0 cleanly pre-world (no player).
|
||||
FUN_SPELL_IS_USABLE = 0x006E3D60,
|
||||
|
||||
// Effective spell/channel duration (ms):
|
||||
// `__fastcall int(const void *spellRecord, int unit /*0 = player,
|
||||
// nonzero = pet*/, int skipMod /*0 = apply mods*/)`. SpellDuration
|
||||
@@ -1288,6 +1327,17 @@ enum Offsets {
|
||||
// exists on the +0xE68 sub-struct here.
|
||||
OFF_CGPLAYER_INFO = 0xE68,
|
||||
|
||||
// PvP rank fields within the CGPlayer +0xE68 sub-struct (siblings of
|
||||
// the combo-point byte at +0x1029). Read by the item tooltip's
|
||||
// "Requires <rank>" / PVP_MEDAL red-text logic (FUN_0052B650) and
|
||||
// mirrored by C_PlayerInfo.CanUseItem:
|
||||
// +0x102B u8 current PvP honor rank (compared >= item
|
||||
// RequiredHonorRank).
|
||||
// +0x1034 u32 earned PvP-medal/rank bitmask (item RequiredCityRank
|
||||
// R passes when bit (R-1) is set).
|
||||
OFF_CGPLAYER_HONOR_RANK = 0x102B,
|
||||
OFF_CGPLAYER_PVP_MEDALS = 0x1034,
|
||||
|
||||
// CGObject's `GetPosition` virtual — vtable slot 5 (offset 0x14).
|
||||
// Signature: `__thiscall(this, float outBuf[3]) → float *position`.
|
||||
// The returned float* may equal outBuf (the object filled it
|
||||
@@ -2842,6 +2892,17 @@ enum Offsets {
|
||||
// size matches Spell.dbc's row count. Pre-login the slot is NULL.
|
||||
VAR_PLAYER_SPELL_BITMAP = 0x00B710FC,
|
||||
|
||||
// Spell-rank-chain knowledge check — `char __thiscall(player /*ecx*/,
|
||||
// spellID)`. Walks `spellID`'s forward rank chain (the class/race-
|
||||
// resolved chain via `0x0060C7C0`) and returns nonzero if the player
|
||||
// knows any *higher* rank than `spellID` (each node tested through the
|
||||
// bitmap helper `0x0060C740`). The item tooltip's "Requires <spell>"
|
||||
// gate is `bitmap(spellID) OR this(spellID)`; C_PlayerInfo.CanUseItem
|
||||
// mirrors that so a RequiredSpell satisfied by a higher known rank
|
||||
// still passes. Does NOT test `spellID` itself — that's the caller's
|
||||
// direct bitmap check.
|
||||
FUN_SPELL_RANK_CHAIN_KNOWN = 0x0060C8D0,
|
||||
|
||||
// Per-spell cooldown query. `__fastcall(int spellID, int bookType,
|
||||
// int *outDuration, int *outStart, int *outEnable)`. Same path
|
||||
// `Script_GetSpellCooldown` (`0x004B40A0`) uses internally after
|
||||
@@ -3415,6 +3476,17 @@ enum Offsets {
|
||||
// '%s' (%s)" (or "calling '%s' on bad self (%s)" for method-style
|
||||
// calls) and calls `lua_error`. Doesn't return.
|
||||
LUAL_ARG_ERROR = 0x6F4810,
|
||||
// `luaL_setn(L, t, n)` — Lua 5.0 auxlib table-length setter
|
||||
// (verified: `FUN_006f4ea0`, __fastcall L/ecx, t/edx, n/stack,
|
||||
// RET 4). Matches 5.0.2 `luaL_setn`: sets `t.n = n` if a numeric
|
||||
// `n` field exists, else `sizes[t] = n` in the registry's weak
|
||||
// LUA_SIZES table. Found via `table.insert` (`FUN_007fb6b0`), which
|
||||
// calls it to record the new length; `luaL_getn` is `FUN_006f5050`.
|
||||
// Needed by `table.wipe` to reset the length after clearing keys —
|
||||
// the length is out-of-band from the keys, so a `lua_next` +
|
||||
// `rawset(nil)` clear leaves it stale and subsequent `table.insert`
|
||||
// appends past the wiped slots.
|
||||
LUAL_SETN = 0x6F4EA0,
|
||||
// `lua_iscfunction(L, idx)` — returns 1 if value is a function
|
||||
// closure with the `isC` byte set (`Closure.c.isC == 1`), 0
|
||||
// otherwise. Used by `coroutine.create` to reject C functions
|
||||
|
||||
@@ -492,6 +492,31 @@ void PushFromCache(void *L, const uint8_t *unit,
|
||||
c.casterGuid);
|
||||
}
|
||||
|
||||
// Reconciles the `Aura::Source` cache against `unit`'s descriptor: when the
|
||||
// descriptor holds at least one raw aura the unit is in view and fully synced,
|
||||
// so it's authoritative — any cached entry for the unit whose spell isn't in
|
||||
// the array was removed while we couldn't see it (e.g. cancelled out of range,
|
||||
// so no OnAuraRemoved reached us) and must be dropped before the fallback
|
||||
// resurfaces it as a phantom (an enumerated aura whose SetUnitAura tooltip is
|
||||
// empty). Reads the RAW slots (not IsSlotPopulated): a spell that's present
|
||||
// but flag-hidden is still legitimately on the unit. Skips when the array is
|
||||
// empty — that can't be told apart from an out-of-range descriptor drop, and
|
||||
// reconciling then would wipe still-valid out-of-range entries.
|
||||
void ReconcileCache(const uint8_t *unit) {
|
||||
if (unit == nullptr)
|
||||
return;
|
||||
uint32_t present[Offsets::UNIT_AURA_TOTAL];
|
||||
int n = 0;
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot) {
|
||||
const uint32_t id = ReadSpellID(unit, slot);
|
||||
if (id != 0)
|
||||
present[n++] = id;
|
||||
}
|
||||
if (n == 0)
|
||||
return;
|
||||
Aura::Source::EvictAbsent(UnitGuid(unit), present, n);
|
||||
}
|
||||
|
||||
// True if `unit`'s descriptor currently exposes any visible aura in either
|
||||
// range. The cache fallback is only trustworthy when the descriptor has been
|
||||
// wholesale-cleared: the two states it exists for — rogue stealth and party
|
||||
@@ -559,6 +584,7 @@ bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
Filter filter, bool playerOnly) {
|
||||
if (unit == nullptr)
|
||||
return false;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
if (DescriptorHasVisibleAura(unit))
|
||||
return false; // descriptor authoritative — see DescriptorHasVisibleAura
|
||||
// The descriptor held `descCount` matches; the caller already found fewer
|
||||
@@ -589,6 +615,7 @@ void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
bool playerOnly, int outerIdx, int &nextKey) {
|
||||
if (unit == nullptr)
|
||||
return;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
if (DescriptorHasVisibleAura(unit))
|
||||
return; // descriptor authoritative — see DescriptorHasVisibleAura
|
||||
const uint64_t guid = UnitGuid(unit);
|
||||
|
||||
+29
-4
@@ -325,10 +325,18 @@ const Game::HookAutoRegister _hookAuraStacks{
|
||||
reinterpret_cast<void **>(&g_origOnAuraStacksChanged)};
|
||||
|
||||
// OnAuraRemoved — a descriptor aura slot went empty: the aura fell off, was
|
||||
// dispelled, or was replaced by a higher rank (the diff dispatcher fires
|
||||
// Removed(old) + Added(new)). Evict the cache entry so the fallback stops
|
||||
// surfacing it. Same ABI as OnAuraAdded (unit in ecx, slot + spellId on the
|
||||
// stack); `slot` is unused here — we evict by spellId.
|
||||
// dispelled, was cancelled by its owner, or was replaced by a higher rank
|
||||
// (the diff dispatcher fires Removed(old) + Added(new)). Evict the cache
|
||||
// entry so the descriptor-drop fallback stops surfacing it. Same ABI as
|
||||
// OnAuraAdded (unit in ecx, slot + spellId on the stack); `slot` is unused
|
||||
// here — we evict by spellId.
|
||||
//
|
||||
// Always evict, even for a death-persistent aura (flask, world buff) on a
|
||||
// dead unit: the server keeps those through death WITHOUT changing any
|
||||
// field, so death itself fires no OnAuraRemoved. Any removal we do receive
|
||||
// for one is therefore genuine — e.g. the owner cancelled the flask while
|
||||
// dead — and must clear the cache, or the fallback keeps surfacing a phantom
|
||||
// whose SetUnitAura tooltip is empty.
|
||||
using OnAuraRemoved_t = void(__fastcall *)(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId);
|
||||
OnAuraRemoved_t g_origOnAuraRemoved = nullptr;
|
||||
@@ -361,6 +369,23 @@ bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
|
||||
return false;
|
||||
}
|
||||
|
||||
void EvictAbsent(uint64_t unitGuid, const uint32_t *presentSpellIds, int count) {
|
||||
if (unitGuid == 0 || presentSpellIds == nullptr || count <= 0)
|
||||
return;
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used || e.targetGuid != unitGuid)
|
||||
continue;
|
||||
bool present = false;
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (presentSpellIds[i] == e.spellId) {
|
||||
present = true;
|
||||
break;
|
||||
}
|
||||
if (!present)
|
||||
e.used = false;
|
||||
}
|
||||
}
|
||||
|
||||
int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut) {
|
||||
if (unitGuid == 0 || out == nullptr || maxOut <= 0)
|
||||
return 0;
|
||||
|
||||
@@ -50,6 +50,18 @@ struct CachedAura {
|
||||
uint32_t durationMs; // applied duration (incl. caster mods); 0 = none
|
||||
};
|
||||
|
||||
// Evicts every cached entry for `unitGuid` whose spellId is NOT present in
|
||||
// `presentSpellIds[0..count)`. Used to reconcile the cache against a unit's
|
||||
// authoritative descriptor when it is back in view (a populated aura array
|
||||
// means the unit is fully synced): an entry the descriptor no longer lists
|
||||
// was removed while we couldn't observe it — e.g. a buff the owner cancelled
|
||||
// while out of our range, whose `OnAuraRemoved` we never received — so drop
|
||||
// it before the descriptor-drop fallback resurfaces it as a phantom. The
|
||||
// caller must only invoke this when the descriptor is populated (count > 0);
|
||||
// an empty array can't distinguish "out of range" from "genuinely buffless",
|
||||
// so reconciling then would wrongly wipe still-valid out-of-range entries.
|
||||
void EvictAbsent(uint64_t unitGuid, const uint32_t *presentSpellIds, int count);
|
||||
|
||||
// Fills `out` with up to `maxOut` cached, non-expired auras on `unitGuid`
|
||||
// whose helpful/harmful classification matches `harmful`. Returns the count
|
||||
// written. The classification is recorded from the aura's descriptor slot at
|
||||
|
||||
@@ -24,6 +24,26 @@
|
||||
// as the events (single source of truth — events firing and the
|
||||
// state being readable happen at the same point in the pipeline).
|
||||
//
|
||||
// `GetMouseButtonClicked()` — the button name of the most recent
|
||||
// mouse button message (`"LeftButton"` / … / `"Button5"`), or nil
|
||||
// when no click is being handled. Modern addons call it inside a
|
||||
// mouse handler to learn which button drove it; the value is evicted
|
||||
// shortly after, so — like the real API — it reads nil outside a
|
||||
// handler rather than returning a stale last-click forever.
|
||||
//
|
||||
// Modern WoW backs this with a frame-manager field set right before a
|
||||
// mouse handler runs and cleared right after (verified in 3.3.5:
|
||||
// `GetMouseButtonClicked` pushes `[frameMgr + 0x1234]`, nil when that
|
||||
// field is null). We can't bracket the handler the same way here: the
|
||||
// `WH_GETMESSAGE` hook sees the button at message-dequeue, but WoW
|
||||
// dispatches the frame-script mouse handlers *deferred* (during the
|
||||
// frame update, not the message pump). So instead we capture the
|
||||
// button on the message and evict it from the per-frame `WorldTick`
|
||||
// once a couple of quiet ticks pass with no button held — the value
|
||||
// survives the whole frame its click lands in (so every handler,
|
||||
// including multiple addons hooking one `OnClick`, sees it) then
|
||||
// clears. A held button keeps it set so `OnDragStart` still reads it.
|
||||
//
|
||||
// Same `WH_GETMESSAGE` thread-message hook pattern as
|
||||
// [Input::Modifier](Modifier.cpp): we intercept the WM_*BUTTON*
|
||||
// messages on the engine's main thread *before* dispatch. The hook
|
||||
@@ -33,6 +53,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -59,6 +80,24 @@ constexpr uint32_t MASK_ANY_BUTTON = 0x1F;
|
||||
|
||||
uint32_t g_mouseButtonMask = 0;
|
||||
|
||||
// Name of the button from the most recent mouse-button message, or
|
||||
// nullptr when no click is being handled. Points at one of
|
||||
// DecodeButton's string literals (stable for the process lifetime),
|
||||
// so storing the pointer is safe. `lua_pushstring(L, nullptr)`
|
||||
// tail-jumps to pushnil, so a null here surfaces as nil to the Lua
|
||||
// caller with no extra branch. Set on each button message; evicted by
|
||||
// EvictTick (see the header note on GetMouseButtonClicked).
|
||||
const char *g_lastButton = nullptr;
|
||||
|
||||
// Set true whenever g_lastButton is (re)assigned, so EvictTick can
|
||||
// tell a just-clicked frame from a quiet one. Number of consecutive
|
||||
// quiet ticks (no activity, nothing held) required before eviction —
|
||||
// >1 so a handler dispatched a frame after its message still reads
|
||||
// the button.
|
||||
bool g_buttonActivity = false;
|
||||
int g_quietTicks = 0;
|
||||
constexpr int kQuietTicksToEvict = 2;
|
||||
|
||||
HHOOK g_msgHook = nullptr;
|
||||
|
||||
// Maps WM_*BUTTON{DOWN,UP} → ("LeftButton"|..., isDown). Returns
|
||||
@@ -101,6 +140,13 @@ void ProcessMouseMessage(UINT msg, WPARAM wParam) {
|
||||
if (name == nullptr)
|
||||
return;
|
||||
|
||||
// Capture for GetMouseButtonClicked on both press and release —
|
||||
// during an OnClick / OnMouseUp handler the "clicked" button is the
|
||||
// one just released, same as the press for a normal click.
|
||||
g_lastButton = name;
|
||||
g_buttonActivity = true;
|
||||
g_quietTicks = 0;
|
||||
|
||||
const int bitIdx = NameToBitIdx(name);
|
||||
if (bitIdx >= 0) {
|
||||
const uint32_t bit = 1u << bitIdx;
|
||||
@@ -136,6 +182,39 @@ int __fastcall Script_IsMouseButtonDown(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetMouseButtonClicked(void *L) {
|
||||
// Null → pushnil (the engine's pushstring tail-jumps on NULL), so
|
||||
// callers outside a mouse handler see nil, matching the modern API.
|
||||
Game::Lua::PushString(L, g_lastButton);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Per-frame eviction of g_lastButton. Runs at the tail of each frame's
|
||||
// world tick. While a button is held we keep the value (drag handlers
|
||||
// still read it) and reset the quiet counter. A frame that saw a fresh
|
||||
// button message consumes its activity flag but doesn't yet evict —
|
||||
// that one-frame grace lets a handler dispatched the frame after its
|
||||
// message still read the button. Only after kQuietTicksToEvict ticks
|
||||
// with nothing held and no activity do we clear, so outside a click
|
||||
// GetMouseButtonClicked reads nil instead of a stale last-click.
|
||||
void EvictTick() {
|
||||
if (g_lastButton == nullptr)
|
||||
return;
|
||||
if (g_mouseButtonMask != 0) {
|
||||
g_quietTicks = 0;
|
||||
return;
|
||||
}
|
||||
if (g_buttonActivity) {
|
||||
g_buttonActivity = false;
|
||||
g_quietTicks = 0;
|
||||
return;
|
||||
}
|
||||
if (++g_quietTicks >= kQuietTicksToEvict) {
|
||||
g_lastButton = nullptr;
|
||||
g_quietTicks = 0;
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK GetMsgHook(int code, WPARAM wParam, LPARAM lParam) {
|
||||
// Only process when the message is being removed from the queue
|
||||
// (`PM_REMOVE` / `GetMessage`); `PM_NOREMOVE` peeks would deliver
|
||||
@@ -157,11 +236,14 @@ void InstallHook() {
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("IsMouseButtonDown",
|
||||
&Script_IsMouseButtonDown);
|
||||
Game::Lua::RegisterGlobalFunction("GetMouseButtonClicked",
|
||||
&Script_GetMouseButtonClicked);
|
||||
InstallHook();
|
||||
}
|
||||
|
||||
const Event::Custom::AutoReserve _reserveDown{kDownEvent};
|
||||
const Event::Custom::AutoReserve _reserveUp{kUpEvent};
|
||||
const Tick::WorldTick::AutoSubscribe _evict{&EvictTick};
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
+23
-23
@@ -48,29 +48,6 @@ const uint8_t *FetchItemRecord(uint32_t itemID) {
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, false);
|
||||
}
|
||||
|
||||
// Walks the item's 5 spell slots looking for the first ON_USE
|
||||
// (trigger=0) entry. Modern `GetItemSpell` returns only this kind;
|
||||
// passive-on-equip procs (trigger=1), weapon procs (trigger=2),
|
||||
// soulstones (trigger=4), and recipe-learn entries (trigger=6) all
|
||||
// stay invisible to the API.
|
||||
//
|
||||
// Vanilla server data on Turtle WoW has been observed to put the
|
||||
// ON_USE entry at slot index 0 for most items, but a few customized
|
||||
// items use later slots, so we scan all 5.
|
||||
int FindOnUseSpellIDInRecord(const uint8_t *record) {
|
||||
auto *spellIDs = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_ID);
|
||||
auto *triggers = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_TRIGGER);
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; i++) {
|
||||
if (spellIDs[i] != 0 &&
|
||||
triggers[i] == Offsets::ITEM_SPELLTRIGGER_ON_USE) {
|
||||
return static_cast<int>(spellIDs[i]);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *SpellNameForID(int spellID) {
|
||||
const uint8_t *record = ::Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
@@ -110,6 +87,29 @@ int __fastcall Script_GetItemSpell(void *L) {
|
||||
|
||||
} // namespace
|
||||
|
||||
// Walks the item's 5 spell slots looking for the first ON_USE
|
||||
// (trigger=0) entry. Modern `GetItemSpell` returns only this kind;
|
||||
// passive-on-equip procs (trigger=1), weapon procs (trigger=2),
|
||||
// soulstones (trigger=4), and recipe-learn entries (trigger=6) all
|
||||
// stay invisible to the API.
|
||||
//
|
||||
// Vanilla server data on Turtle WoW has been observed to put the
|
||||
// ON_USE entry at slot index 0 for most items, but a few customized
|
||||
// items use later slots, so we scan all 5.
|
||||
int FindOnUseSpellIDInRecord(const uint8_t *record) {
|
||||
auto *spellIDs = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_ID);
|
||||
auto *triggers = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_TRIGGER);
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; i++) {
|
||||
if (spellIDs[i] != 0 &&
|
||||
triggers[i] == Offsets::ITEM_SPELLTRIGGER_ON_USE) {
|
||||
return static_cast<int>(spellIDs[i]);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int OnUseSpellIDForItemID(uint32_t itemID) {
|
||||
if (itemID == 0)
|
||||
return 0;
|
||||
|
||||
@@ -29,4 +29,12 @@ namespace Item::Spell {
|
||||
// the same use-spell still register).
|
||||
int OnUseSpellIDForItemID(uint32_t itemID);
|
||||
|
||||
// Walks a cached `ItemStats_C` record's 5 spell slots and returns the
|
||||
// first ON_USE (trigger=0) spell ID, or 0 if none. Split out so callers
|
||||
// that already hold the record (e.g. `Item::Usable`, which also reads the
|
||||
// record's use requirements) don't peek the cache twice or re-derive the
|
||||
// slot walk. `OnUseSpellIDForItemID` is the peek-by-itemID convenience
|
||||
// wrapper over this.
|
||||
int FindOnUseSpellIDInRecord(const uint8_t *record);
|
||||
|
||||
} // namespace Item::Spell
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// 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/>.
|
||||
|
||||
// `IsUsableItem(item)` / `C_Item.IsUsableItem(item)` — is the item's
|
||||
// *on-use* ability usable by the local player right now, returning
|
||||
// `(usable, noMana)`. This is the item analog of `IsUsableSpell`; it has
|
||||
// nothing to do with whether the item can be *equipped* (armor
|
||||
// proficiency / class-restricted gear is not this function's concern —
|
||||
// that's the tooltip's red requirement lines, evaluated separately).
|
||||
//
|
||||
// Semantics (mirrors 3.3.5's `Script_IsUsableItem` structure, rebuilt on
|
||||
// 1.12 primitives — vanilla ships no `IsUsableItem` and its item-action
|
||||
// usability path never computed on-use power/noMana):
|
||||
// 1. Item must have an on-use spell (a `SPELL_TRIGGER[i] == ON_USE(0)`
|
||||
// slot with a non-zero `SPELL_ID[i]`). No on-use ability → not
|
||||
// usable (there's nothing to "use").
|
||||
// 2. The item's own *use* requirements must be met: RequiredLevel, and
|
||||
// the AllowableClass / AllowableRace masks. These gate activation
|
||||
// (e.g. a potion below your level, a class-restricted trinket) and
|
||||
// are use — not equip — requirements, so they belong here.
|
||||
// 3. The on-use spell must be castable now: delegated to the engine's
|
||||
// own spell-castability helper `FUN_SPELL_IS_USABLE`, which folds in
|
||||
// form/stance, mechanic immunities, aura state, and the power check.
|
||||
// Insufficient power is the only condition that flips `noMana` (and
|
||||
// leaves `usable` false), exactly like `IsUsableSpell`.
|
||||
//
|
||||
// Cache-miss handling matches `C_Item.IsEquippableItem`: a sync peek with
|
||||
// no load fired, returning `(false, false)` if the item isn't cached yet.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Spell.h"
|
||||
#include "spell/Lookup.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Item::Usable {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using SpellIsUsable_t = int(__fastcall *)(const uint8_t *spellRecord, int *outNoMana);
|
||||
|
||||
// Same item-cache peek pattern as the other item modules (no shared
|
||||
// helper — each call site is one line different). Sync, no load fired.
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
const uint8_t *PlayerDescriptor() {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(resolve("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
}
|
||||
|
||||
// True iff `mask` restricts a 1-based `index` (class/race) the player
|
||||
// isn't part of. Mask 0 and 0xFFFFFFFF both mean "no restriction".
|
||||
bool RestrictedOut(uint32_t mask, uint32_t index1Based) {
|
||||
if (mask == 0 || mask == 0xFFFFFFFF || index1Based == 0)
|
||||
return false;
|
||||
return (mask & (1u << (index1Based - 1))) == 0;
|
||||
}
|
||||
|
||||
struct Usability {
|
||||
bool usable;
|
||||
bool noMana;
|
||||
};
|
||||
|
||||
Usability ComputeUsability(int itemID) {
|
||||
Usability r{false, false};
|
||||
if (itemID <= 0)
|
||||
return r;
|
||||
|
||||
auto *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return r; // not cached → sync "false", no load fired
|
||||
|
||||
const int onUseSpellID = Item::Spell::FindOnUseSpellIDInRecord(record);
|
||||
if (onUseSpellID == 0)
|
||||
return r; // no on-use ability → nothing to use
|
||||
|
||||
auto *desc = PlayerDescriptor();
|
||||
if (desc == nullptr)
|
||||
return r; // pre-login / no player
|
||||
|
||||
// Use requirements (NOT equip requirements): level + class/race.
|
||||
const int playerLevel = *reinterpret_cast<const int *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_LEVEL);
|
||||
const uint32_t reqLevel = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_LEVEL);
|
||||
if (static_cast<int>(reqLevel) > playerLevel)
|
||||
return r;
|
||||
|
||||
const uint32_t allowClass = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_ALLOWABLE_CLASS);
|
||||
const uint32_t playerClass = *(desc + Offsets::OFF_UNIT_DESCRIPTOR_CLASS_BYTE);
|
||||
if (RestrictedOut(allowClass, playerClass))
|
||||
return r;
|
||||
|
||||
const uint32_t allowRace = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_ALLOWABLE_RACE);
|
||||
const uint32_t playerRace = *(desc + Offsets::OFF_UNIT_DESCRIPTOR_RACE_BYTE);
|
||||
if (RestrictedOut(allowRace, playerRace))
|
||||
return r;
|
||||
|
||||
// On-use spell castability + noMana, from the engine's own helper.
|
||||
// `::Spell::Lookup` (leading `::`) — inside `Item::Usable`, a bare
|
||||
// `Spell::` binds to the included `Item::Spell` namespace.
|
||||
auto *spellRec = ::Spell::Lookup::RecordForID(onUseSpellID);
|
||||
if (spellRec == nullptr)
|
||||
return r;
|
||||
|
||||
auto isUsable = reinterpret_cast<SpellIsUsable_t>(Offsets::FUN_SPELL_IS_USABLE);
|
||||
int noMana = 0;
|
||||
const int usable = isUsable(spellRec, &noMana) & 0xFF;
|
||||
r.usable = usable != 0;
|
||||
r.noMana = noMana != 0;
|
||||
return r;
|
||||
}
|
||||
|
||||
// `IsUsableItem(item)` — legacy global. Returns `(usable, noMana)` as
|
||||
// 1/nil pairs, matching stock 1.12's `IsUsableAction` / our
|
||||
// `IsUsableSpell` convention.
|
||||
int __fastcall Script_IsUsableItem(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
const Usability r = ComputeUsability(itemID);
|
||||
if (r.usable) {
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
Game::Lua::PushNil(L);
|
||||
} else if (r.noMana) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
} else {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `C_Item.IsUsableItem(item)` — modern table-namespace form. Returns
|
||||
// proper booleans (`usable`, `noMana`) per the `C_Item.*` convention.
|
||||
int __fastcall Script_C_Item_IsUsableItem(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
const Usability r = ComputeUsability(itemID);
|
||||
Game::Lua::PushBool(L, r.usable);
|
||||
Game::Lua::PushBool(L, r.noMana);
|
||||
return 2;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("IsUsableItem", &Script_IsUsableItem);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "IsUsableItem",
|
||||
&Script_C_Item_IsUsableItem);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Item::Usable
|
||||
+125
-32
@@ -62,6 +62,15 @@ T Read(const void *base, int off) {
|
||||
return *reinterpret_cast<const T *>(static_cast<const uint8_t *>(base) + off);
|
||||
}
|
||||
|
||||
// Engine millisecond clock — same source (and epoch) as Lua's `GetTime()`
|
||||
// (which reads this scaled by 0.001), so a value stored here is directly
|
||||
// comparable to `GetTime()` once divided by 1000.
|
||||
uint32_t NowMs() {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
return reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
}
|
||||
|
||||
// Modern rollType constants (what C_LootHistory.GetPlayerInfo returns).
|
||||
constexpr int ROLL_PASS = 0;
|
||||
constexpr int ROLL_NEED = 1;
|
||||
@@ -152,15 +161,16 @@ struct PlayerRoll {
|
||||
constexpr int kMaxPlayers = 40;
|
||||
|
||||
struct HistItem {
|
||||
uint32_t rollID; // stable monotonic ID — survives ring-index shifts
|
||||
uint64_t sourceGuid; // looted-object GUID — the roll's identity
|
||||
uint32_t slot;
|
||||
uint32_t itemId;
|
||||
int32_t itemSuffix; // random suffix/property ID (link suffix field)
|
||||
uint32_t itemSeed; // random seed (link unique field)
|
||||
uint64_t winnerGuid;
|
||||
char winnerName[kNameLen];
|
||||
uint64_t winnerGuid; // winner's roller GUID (0 if undecided)
|
||||
bool hasWinner;
|
||||
bool isDone;
|
||||
uint32_t createdMs; // engine tick at creation (GetTime()*1000 epoch)
|
||||
PlayerRoll players[kMaxPlayers];
|
||||
int playerCount;
|
||||
bool used;
|
||||
@@ -175,6 +185,13 @@ constexpr int kMaxItems = 128;
|
||||
HistItem g_items[kMaxItems];
|
||||
int g_write = 0;
|
||||
int g_count = 0;
|
||||
uint32_t g_nextRollId = 1; // 0 is reserved as "invalid" (never assigned)
|
||||
|
||||
// Set by FindOrCreate for the most recent call, read by the hooks to decide
|
||||
// whether a structural LOOT_HISTORY_FULL_UPDATE is warranted (a new item
|
||||
// appeared, or the ring evicted an old one so every index shifted).
|
||||
bool g_createdNew = false;
|
||||
bool g_evicted = false;
|
||||
|
||||
HistItem *Find(uint64_t sourceGuid, uint32_t slot) {
|
||||
for (auto &e : g_items) {
|
||||
@@ -185,6 +202,8 @@ HistItem *Find(uint64_t sourceGuid, uint32_t slot) {
|
||||
}
|
||||
|
||||
HistItem *FindOrCreate(uint64_t sourceGuid, uint32_t slot, uint32_t itemId) {
|
||||
g_createdNew = false;
|
||||
g_evicted = false;
|
||||
HistItem *e = Find(sourceGuid, slot);
|
||||
if (e != nullptr) {
|
||||
if (itemId != 0)
|
||||
@@ -192,17 +211,54 @@ HistItem *FindOrCreate(uint64_t sourceGuid, uint32_t slot, uint32_t itemId) {
|
||||
return e;
|
||||
}
|
||||
e = &g_items[g_write];
|
||||
g_evicted = e->used; // overwriting a live slot => ring full => eviction
|
||||
g_write = (g_write + 1) % kMaxItems;
|
||||
if (g_count < kMaxItems)
|
||||
++g_count;
|
||||
*e = HistItem{};
|
||||
e->used = true;
|
||||
e->rollID = g_nextRollId++;
|
||||
e->createdMs = NowMs();
|
||||
e->sourceGuid = sourceGuid;
|
||||
e->slot = slot;
|
||||
e->itemId = itemId;
|
||||
g_createdNew = true;
|
||||
return e;
|
||||
}
|
||||
|
||||
// Find the player-roll slot for `guid`, appending a fresh one (name/class
|
||||
// resolved from the NameCache) if absent and there's room. Returns the
|
||||
// 0-based index, or -1 when the roster is full. Shared by the roll recorder
|
||||
// and the winner recorder — the winner may never have sent an individual
|
||||
// SMSG_LOOT_ROLL (joined late / master loot), so ensuring they're present
|
||||
// lets GetItem's winnerIdx resolve through GetPlayerInfo.
|
||||
int FindOrAddPlayer(HistItem *e, uint64_t guid) {
|
||||
for (int i = 0; i < e->playerCount; ++i) {
|
||||
if (e->players[i].guid == guid)
|
||||
return i;
|
||||
}
|
||||
if (e->playerCount >= kMaxPlayers)
|
||||
return -1;
|
||||
const int idx = e->playerCount++;
|
||||
const uint8_t *rec = LookupPlayerRecord(guid); // cache loaded at hook time
|
||||
e->players[idx] = PlayerRoll{};
|
||||
e->players[idx].guid = guid;
|
||||
CopyName(e->players[idx].name, NameFromRecord(rec));
|
||||
e->players[idx].classId = ClassIdFromRecord(rec);
|
||||
return idx;
|
||||
}
|
||||
|
||||
// 1-based index within players[] of the winner, or 0 if undecided / absent.
|
||||
int WinnerIndex(const HistItem *e) {
|
||||
if (!e->hasWinner || e->winnerGuid == 0)
|
||||
return 0;
|
||||
for (int i = 0; i < e->playerCount; ++i) {
|
||||
if (e->players[i].guid == e->winnerGuid)
|
||||
return i + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 1-based access in oldest→newest order (matching GetNumItems' count).
|
||||
HistItem *GetEntry(int index1Based) {
|
||||
if (index1Based < 1 || index1Based > g_count)
|
||||
@@ -251,20 +307,7 @@ int RecordRoll(const void *msg, int *outPlayerIdx) {
|
||||
if (roller == 0)
|
||||
return IndexOf(e);
|
||||
|
||||
int slotIdx = -1;
|
||||
for (int i = 0; i < e->playerCount; ++i) {
|
||||
if (e->players[i].guid == roller) {
|
||||
slotIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slotIdx < 0 && e->playerCount < kMaxPlayers) {
|
||||
slotIdx = e->playerCount++;
|
||||
const uint8_t *rec = LookupPlayerRecord(roller); // cache loaded now
|
||||
e->players[slotIdx].guid = roller;
|
||||
CopyName(e->players[slotIdx].name, NameFromRecord(rec));
|
||||
e->players[slotIdx].classId = ClassIdFromRecord(rec);
|
||||
}
|
||||
const int slotIdx = FindOrAddPlayer(e, roller);
|
||||
if (slotIdx >= 0) {
|
||||
e->players[slotIdx].rollType = rollType;
|
||||
e->players[slotIdx].roll = rollNum;
|
||||
@@ -281,9 +324,10 @@ int RecordWinner(const void *msg) {
|
||||
|
||||
HistItem *e = FindOrCreate(src, slot, itemId);
|
||||
e->winnerGuid = winner;
|
||||
CopyName(e->winnerName, NameFromRecord(LookupPlayerRecord(winner)));
|
||||
e->hasWinner = true;
|
||||
e->isDone = true;
|
||||
if (winner != 0)
|
||||
FindOrAddPlayer(e, winner); // so GetItem's winnerIdx resolves
|
||||
return IndexOf(e);
|
||||
}
|
||||
|
||||
@@ -330,22 +374,39 @@ WonFn_t g_origWon = nullptr;
|
||||
PassedFn_t g_origPassed = nullptr;
|
||||
StartFn_t g_origStart = nullptr;
|
||||
|
||||
// Fire LOOT_HISTORY_FULL_UPDATE (no payload) — the "rebuild everything"
|
||||
// signal, matching the UI's full refresh. Called when the item set changes
|
||||
// structurally: a new item appears or the ring evicts an old one (shifting
|
||||
// every index).
|
||||
void FireFullUpdate() {
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kFullUpdate), "");
|
||||
}
|
||||
|
||||
// START_ROLL: let the original build the store node, then read the item's
|
||||
// random suffix off it. Adds the item to the history so a UI can show it as
|
||||
// soon as the roll opens (before any roll comes in).
|
||||
// soon as the roll opens (before any roll comes in) — hence the FULL_UPDATE.
|
||||
void __fastcall Start_h(void *packet) {
|
||||
g_origStart(packet);
|
||||
RecordStart();
|
||||
if (g_createdNew || g_evicted)
|
||||
FireFullUpdate();
|
||||
}
|
||||
|
||||
// Record fields (message is freed by the original), run the original, THEN
|
||||
// fire the event — firing after the original keeps the engine's own handler
|
||||
// off our re-entrancy (same order Item::Data uses from its packet hook).
|
||||
// off our re-entrancy (same order Item::Data uses from its packet hook). A
|
||||
// structural FULL_UPDATE (if any) precedes the granular event so a listener
|
||||
// that rebuilds on FULL_UPDATE sees the new row before the row-level refresh.
|
||||
void __fastcall Roll_h(void *msg, void *ctx, int a3) {
|
||||
int itemIdx = 0, playerIdx = 0;
|
||||
if (msg != nullptr)
|
||||
bool structural = false;
|
||||
if (msg != nullptr) {
|
||||
itemIdx = RecordRoll(msg, &playerIdx);
|
||||
structural = g_createdNew || g_evicted;
|
||||
}
|
||||
g_origRoll(msg, ctx, a3);
|
||||
if (structural)
|
||||
FireFullUpdate();
|
||||
if (itemIdx > 0)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kRollChanged), "%d%d", itemIdx,
|
||||
playerIdx);
|
||||
@@ -353,18 +414,28 @@ void __fastcall Roll_h(void *msg, void *ctx, int a3) {
|
||||
|
||||
void __fastcall Won_h(void *ctx, void *msg, int a3) {
|
||||
int itemIdx = 0;
|
||||
if (msg != nullptr)
|
||||
bool structural = false;
|
||||
if (msg != nullptr) {
|
||||
itemIdx = RecordWinner(msg);
|
||||
structural = g_createdNew || g_evicted;
|
||||
}
|
||||
g_origWon(ctx, msg, a3);
|
||||
if (structural)
|
||||
FireFullUpdate();
|
||||
if (itemIdx > 0)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kRollComplete), "%d", itemIdx);
|
||||
}
|
||||
|
||||
void __fastcall Passed_h(void *ctx, void *msg) {
|
||||
int itemIdx = 0;
|
||||
if (msg != nullptr)
|
||||
bool structural = false;
|
||||
if (msg != nullptr) {
|
||||
itemIdx = RecordAllPassed(msg);
|
||||
structural = g_createdNew || g_evicted;
|
||||
}
|
||||
g_origPassed(ctx, msg);
|
||||
if (structural)
|
||||
FireFullUpdate();
|
||||
if (itemIdx > 0)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kRollComplete), "%d", itemIdx);
|
||||
}
|
||||
@@ -390,10 +461,15 @@ int __fastcall Script_GetNumItems(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_LootHistory.GetItem(itemIndex)` -> itemLink, numPlayers, isDone, winner.
|
||||
// itemLink is the `item:id:enchant:suffix:unique` form (valid for GetItemInfo
|
||||
// / SetHyperlink), carrying the roll's random suffix so "of the X" items keep
|
||||
// their affix; `winner` is the winner's name, or nil if undecided.
|
||||
// `C_LootHistory.GetItem(itemIndex)` ->
|
||||
// rollID, itemLink, numPlayers, isDone, winnerIdx, timestamp
|
||||
// Matches the MoP signature: `rollID` is a stable ID (unlike itemIndex, which
|
||||
// shifts as the ring saturates — key expanded/highlighted rows on it);
|
||||
// `itemLink` is the `item:id:enchant:suffix:unique` form (valid for
|
||||
// GetItemInfo / SetHyperlink), carrying the roll's random suffix so "of the X"
|
||||
// items keep their affix; `winnerIdx` is the winner's 1-based playerIndex for
|
||||
// GetPlayerInfo(itemIndex, winnerIdx) (nil if undecided). `timestamp` is a
|
||||
// ClassicAPI extension: the `GetTime()`-comparable creation time in seconds.
|
||||
int __fastcall Script_GetItem(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetItem(itemIndex)");
|
||||
@@ -407,14 +483,17 @@ int __fastcall Script_GetItem(void *L) {
|
||||
// random suffix/seed so "of the Bear"-style items keep their affix.
|
||||
std::snprintf(link, sizeof(link), "item:%u:0:%d:%u", e->itemId,
|
||||
e->itemSuffix, e->itemSeed);
|
||||
Game::Lua::PushString(L, link); // 1 itemLink
|
||||
Game::Lua::PushNumber(L, static_cast<double>(e->playerCount)); // 2 numPlayers
|
||||
Game::Lua::PushBool(L, e->isDone); // 3 isDone
|
||||
if (e->hasWinner && e->winnerName[0] != '\0')
|
||||
Game::Lua::PushString(L, e->winnerName); // 4 winner
|
||||
Game::Lua::PushNumber(L, static_cast<double>(e->rollID)); // 1 rollID
|
||||
Game::Lua::PushString(L, link); // 2 itemLink
|
||||
Game::Lua::PushNumber(L, static_cast<double>(e->playerCount)); // 3 numPlayers
|
||||
Game::Lua::PushBool(L, e->isDone); // 4 isDone
|
||||
const int winnerIdx = WinnerIndex(e);
|
||||
if (winnerIdx > 0)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(winnerIdx)); // 5 winnerIdx
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 4;
|
||||
Game::Lua::PushNumber(L, e->createdMs / 1000.0); // 6 timestamp
|
||||
return 6;
|
||||
}
|
||||
|
||||
// `C_LootHistory.GetPlayerInfo(itemIndex, playerIndex)` ->
|
||||
@@ -450,6 +529,19 @@ int __fastcall Script_GetPlayerInfo(void *L) {
|
||||
return 6;
|
||||
}
|
||||
|
||||
// `C_LootHistory.Clear()` -> wipe the accumulated history. A ClassicAPI
|
||||
// extension (no MoP equivalent) so a UI can offer a "clear history" button.
|
||||
// Fires LOOT_HISTORY_FULL_UPDATE so any open display rebuilds empty.
|
||||
int __fastcall Script_Clear(void *L) {
|
||||
(void)L;
|
||||
for (auto &e : g_items)
|
||||
e.used = false;
|
||||
g_write = 0;
|
||||
g_count = 0;
|
||||
FireFullUpdate();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_LootHistory", "GetNumItems",
|
||||
&Script_GetNumItems);
|
||||
@@ -457,6 +549,7 @@ void RegisterLuaFunctions() {
|
||||
&Script_GetItem);
|
||||
Game::Lua::RegisterTableFunction("C_LootHistory", "GetPlayerInfo",
|
||||
&Script_GetPlayerInfo);
|
||||
Game::Lua::RegisterTableFunction("C_LootHistory", "Clear", &Script_Clear);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
// 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_PlayerInfo.CanUseItem(itemID)` — true iff the local player meets the
|
||||
// item's *use/equip* requirements. This is the "is the item red in the
|
||||
// tooltip" gate, and is distinct from its siblings:
|
||||
// - `C_Item.IsEquippableItem` — does the item fit *some* slot (static).
|
||||
// - `IsUsableItem` — is the item's on-use ability castable now.
|
||||
// - `C_PlayerInfo.CanUseItem` — may the player equip/use it at all.
|
||||
//
|
||||
// So this is the answer to "mail chest on a Mage" (proficiency), "level-40
|
||||
// item on a level-20" (RequiredLevel), and class/race-restricted gear.
|
||||
//
|
||||
// Not in vanilla 1.12 nor in our 3.3.5 reference client (the modern API
|
||||
// landed in 1.14.4 / 3.4.2 / 10.0.5), so there's no cross-binary source;
|
||||
// the logic mirrors the requirement checks the 1.12 item tooltip builder
|
||||
// (`FUN_0052B650`) inlines for its red/green line coloring — the same
|
||||
// `bVar23` "can't use this" flag, minus the display.
|
||||
//
|
||||
// Checks (all must pass):
|
||||
// 1. Proficiency — for weapon/armor the engine keeps a per-item-class
|
||||
// subclass-proficiency bitmask (VAR_PROFICIENCY_TABLE, fed by
|
||||
// SMSG_SET_PROFICIENCY). `mask[m_class] & (1 << m_subClass)` must be
|
||||
// set. Item classes with no proficiency concept store mask 0 and are
|
||||
// unrestricted (matches the tooltip's `mask != 0` gate).
|
||||
// 2. RequiredLevel <= player level.
|
||||
// 3. AllowableClass / AllowableRace masks include the player (0 or all-
|
||||
// bits = no restriction).
|
||||
// 4. RequiredSkill — the player must have the item's skill line at an
|
||||
// effective rank (current + temp bonus) >= RequiredSkillRank (e.g. a
|
||||
// mount "Requires Riding (150)"). Via FUN_SKILL_LINE_TO_SLOT.
|
||||
// 5. RequiredSpell — the player must know the item's prerequisite spell,
|
||||
// or a higher rank of it: a crafting specialization ("Requires
|
||||
// Armorsmith") or a proficiency spell. Via the spell-knowledge bitmap
|
||||
// plus the engine's rank-chain walk.
|
||||
// 6. RequiredHonorRank / RequiredCityRank — PvP-rank gates: the player's
|
||||
// honor rank (>= required) and earned PvP-medal bitmask.
|
||||
// 7. RequiredReputation — current standing with the item's faction must
|
||||
// reach the required reaction band.
|
||||
//
|
||||
// Cache-miss handling matches `C_Item.IsEquippableItem`: a synchronous
|
||||
// peek that returns false without firing a load.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "spell/Lookup.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Player::CanUseItem {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
// `__thiscall(player /*ecx*/, skillLineID) -> slot` (-1 if the player
|
||||
// doesn't have the skill line). See VAR docs on FUN_SKILL_LINE_TO_SLOT.
|
||||
using SkillLineToSlot_t = int(__thiscall *)(void *player, uint32_t skillLineID);
|
||||
// `__fastcall(factionID) -> current total reputation` (base + bonus), 0
|
||||
// for factions not in the player's list. See FUN_REPUTATION_GET_STANDING.
|
||||
using RepStanding_t = int(__fastcall *)(int factionID);
|
||||
// `__thiscall(player /*ecx*/, spellID) -> nonzero` if the player knows a
|
||||
// higher rank in spellID's chain. See FUN_SPELL_RANK_CHAIN_KNOWN.
|
||||
using SpellRankChainKnown_t = int(__thiscall *)(void *player, uint32_t spellID);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
// The local player's CGUnit/CGPlayer object (holds the descriptor at
|
||||
// +OFF_UNIT_DESCRIPTOR and the CGPlayer sub-struct at +OFF_CGPLAYER_INFO).
|
||||
const uint8_t *ResolvePlayer() {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return static_cast<const uint8_t *>(resolve("player"));
|
||||
}
|
||||
|
||||
// True iff `mask` restricts a 1-based `index` (class/race) the player
|
||||
// isn't part of. Mask 0 and 0xFFFFFFFF both mean "no restriction".
|
||||
bool RestrictedOut(uint32_t mask, uint32_t index1Based) {
|
||||
if (mask == 0 || mask == 0xFFFFFFFF || index1Based == 0)
|
||||
return false;
|
||||
return (mask & (1u << ((index1Based - 1) & 31))) == 0;
|
||||
}
|
||||
|
||||
// Per-item-class proficiency bitmask lookup. Returns false only when the
|
||||
// item's class *has* a proficiency concept (mask != 0) and the item's
|
||||
// subclass bit is unset.
|
||||
bool ProficiencyMet(const uint8_t *record) {
|
||||
const uint32_t itemClass = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CLASS);
|
||||
const uint32_t subClass = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
if (itemClass > 16) // table is 17 entries (m_class 0..16)
|
||||
return true;
|
||||
auto *table = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PROFICIENCY_TABLE));
|
||||
const uint32_t mask = table[itemClass];
|
||||
if (mask == 0)
|
||||
return true; // class has no proficiency concept (consumables, …)
|
||||
return (mask & (1u << (subClass & 31))) != 0;
|
||||
}
|
||||
|
||||
// RequiredSkill + RequiredSkillRank gate (e.g. a mount "Requires Riding
|
||||
// (150)", a profession tool "Requires Engineering (200)"). Maps the
|
||||
// item's SkillLine.dbc row to the player's skill slot; the player must
|
||||
// have the line at an effective rank (current + temp bonus) >= required.
|
||||
bool RequiredSkillMet(const uint8_t *player, const uint8_t *record) {
|
||||
const uint32_t reqSkill = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_SKILL);
|
||||
if (reqSkill == 0)
|
||||
return true; // no skill requirement
|
||||
const uint32_t reqRank = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_SKILL_RANK);
|
||||
|
||||
auto toSlot = reinterpret_cast<SkillLineToSlot_t>(Offsets::FUN_SKILL_LINE_TO_SLOT);
|
||||
const int slot = toSlot(const_cast<uint8_t *>(player), reqSkill);
|
||||
if (slot < 0)
|
||||
return false; // player doesn't have the skill line at all
|
||||
|
||||
auto *sub = *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_CGPLAYER_INFO);
|
||||
if (sub == nullptr)
|
||||
return false;
|
||||
const uint8_t *rec = sub + Offsets::OFF_SKILL_INFO_TABLE +
|
||||
slot * Offsets::SKILL_INFO_STRIDE;
|
||||
uint32_t rank = *reinterpret_cast<const uint16_t *>(rec + Offsets::OFF_SKILL_INFO_CUR);
|
||||
if (rank != 0) // add the temp bonus only when there's a base value
|
||||
rank += *reinterpret_cast<const uint16_t *>(rec + Offsets::OFF_SKILL_INFO_BONUS);
|
||||
return rank >= reqRank;
|
||||
}
|
||||
|
||||
// True if the local player knows `spellID`, via the engine's spell-
|
||||
// knowledge bitmap (the active-player path of `FUN_0060c740`, read
|
||||
// directly — no player object needed). Covers trained abilities, talents,
|
||||
// racials, profession recipes, and crafting specializations (Armorsmith,
|
||||
// Weaponsmith, the Engineering branches, …), which are all single learned
|
||||
// spells that land in this bitmap.
|
||||
bool PlayerKnowsSpell(int spellID) {
|
||||
if (spellID <= 0)
|
||||
return false;
|
||||
auto *bitmap = *reinterpret_cast<const uint32_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_SPELL_BITMAP));
|
||||
if (bitmap == nullptr)
|
||||
return false;
|
||||
const int spellCount = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SPELL_RECORD_COUNT));
|
||||
if (spellID > spellCount)
|
||||
return false;
|
||||
return (bitmap[spellID >> 5] & (1u << (spellID & 31))) != 0;
|
||||
}
|
||||
|
||||
// RequiredSpell gate — the item's `RequiredSpell` (a Spell.dbc row) is the
|
||||
// prerequisite the "Requires <spell>" tooltip line reflects: a crafting
|
||||
// specialization like Armorsmith, or an armor/weapon proficiency spell.
|
||||
// The player must know it. An unset (0) or invalid (no Spell.dbc record)
|
||||
// requirement is no gate — matching the tooltip's validity guard, which
|
||||
// skips the line for a missing row.
|
||||
//
|
||||
// Mirrors the tooltip's `bitmap(reqSpell) OR rank-chain(reqSpell)`: met if
|
||||
// the player knows the exact RequiredSpell, or a higher rank of it (the
|
||||
// latter covers a ranked RequiredSpell — `FUN_SPELL_RANK_CHAIN_KNOWN`
|
||||
// walks the forward chain). Item RequiredSpell is a non-ranked
|
||||
// specialization/proficiency in practice, so the chain walk is
|
||||
// belt-and-suspenders, but it makes the check exact.
|
||||
bool RequiredSpellMet(const uint8_t *player, const uint8_t *record) {
|
||||
const int reqSpell = *reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_SPELL);
|
||||
if (reqSpell <= 0)
|
||||
return true; // no requirement
|
||||
if (::Spell::Lookup::RecordForID(reqSpell) == nullptr)
|
||||
return true; // invalid Spell.dbc row → tooltip skips the gate
|
||||
if (PlayerKnowsSpell(reqSpell))
|
||||
return true; // knows the exact spell
|
||||
auto chainKnown = reinterpret_cast<SpellRankChainKnown_t>(
|
||||
Offsets::FUN_SPELL_RANK_CHAIN_KNOWN);
|
||||
return chainKnown(const_cast<uint8_t *>(player),
|
||||
static_cast<uint32_t>(reqSpell)) != 0;
|
||||
}
|
||||
|
||||
// RequiredHonorRank — the player's PvP honor rank must be >= the item's.
|
||||
// `sub` is the CGPlayer +0xE68 sub-struct (may be null pre-world).
|
||||
bool RequiredHonorRankMet(const uint8_t *sub, const uint8_t *record) {
|
||||
const uint32_t req = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_HONOR_RANK);
|
||||
if (req == 0)
|
||||
return true;
|
||||
if (sub == nullptr)
|
||||
return false;
|
||||
const uint8_t rank = *(sub + Offsets::OFF_CGPLAYER_HONOR_RANK);
|
||||
return rank >= req;
|
||||
}
|
||||
|
||||
// RequiredCityRank — a PvP-medal bitmask gate: the item's rank R passes
|
||||
// when the player has earned bit (R-1) in the medal mask.
|
||||
bool RequiredCityRankMet(const uint8_t *sub, const uint8_t *record) {
|
||||
const uint32_t req = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_CITY_RANK);
|
||||
if (req == 0)
|
||||
return true;
|
||||
if (sub == nullptr)
|
||||
return false;
|
||||
const uint32_t medals = *reinterpret_cast<const uint32_t *>(
|
||||
sub + Offsets::OFF_CGPLAYER_PVP_MEDALS);
|
||||
return (medals & (1u << ((req - 1) & 31))) != 0;
|
||||
}
|
||||
|
||||
// RequiredReputation — current standing with the item's faction must
|
||||
// reach the required reaction band. The band index (0=Hated … 7=Exalted)
|
||||
// indexes the engine's reaction-min threshold table; the player's total
|
||||
// rep with the faction must be >= that threshold.
|
||||
bool RequiredReputationMet(const uint8_t *record) {
|
||||
const int faction = *reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_FACTION);
|
||||
if (faction == 0)
|
||||
return true;
|
||||
const uint32_t band = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_FACTION_RANK);
|
||||
if (band > 7) // reaction-min table is 8 bands (0..7); guard OOB
|
||||
return true;
|
||||
auto getStanding = reinterpret_cast<RepStanding_t>(Offsets::FUN_REPUTATION_GET_STANDING);
|
||||
const int current = getStanding(faction);
|
||||
const int threshold = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_REACTION_MIN_TABLE) + band * 4);
|
||||
return current >= threshold;
|
||||
}
|
||||
|
||||
bool ComputeCanUse(int itemID) {
|
||||
if (itemID <= 0)
|
||||
return false;
|
||||
auto *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return false; // not cached → sync false, no load fired
|
||||
auto *player = ResolvePlayer();
|
||||
if (player == nullptr)
|
||||
return false; // pre-login / no player
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return false;
|
||||
|
||||
if (!ProficiencyMet(record))
|
||||
return false;
|
||||
|
||||
const int playerLevel = *reinterpret_cast<const int *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_LEVEL);
|
||||
const uint32_t reqLevel = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_LEVEL);
|
||||
if (static_cast<int>(reqLevel) > playerLevel)
|
||||
return false;
|
||||
|
||||
const uint32_t allowClass = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_ALLOWABLE_CLASS);
|
||||
const uint32_t playerClass = *(desc + Offsets::OFF_UNIT_DESCRIPTOR_CLASS_BYTE);
|
||||
if (RestrictedOut(allowClass, playerClass))
|
||||
return false;
|
||||
|
||||
const uint32_t allowRace = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_ALLOWABLE_RACE);
|
||||
const uint32_t playerRace = *(desc + Offsets::OFF_UNIT_DESCRIPTOR_RACE_BYTE);
|
||||
if (RestrictedOut(allowRace, playerRace))
|
||||
return false;
|
||||
|
||||
if (!RequiredSkillMet(player, record))
|
||||
return false;
|
||||
|
||||
if (!RequiredSpellMet(player, record))
|
||||
return false;
|
||||
|
||||
// PvP rank + reputation gates read the CGPlayer +0xE68 sub-struct.
|
||||
auto *sub = *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_CGPLAYER_INFO);
|
||||
if (!RequiredHonorRankMet(sub, record))
|
||||
return false;
|
||||
if (!RequiredCityRankMet(sub, record))
|
||||
return false;
|
||||
if (!RequiredReputationMet(record))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int __fastcall Script_CanUseItem(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
Game::Lua::PushBool(L, ComputeCanUse(itemID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_PlayerInfo", "CanUseItem", &Script_CanUseItem);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Player::CanUseItem
|
||||
+117
-22
@@ -23,13 +23,19 @@
|
||||
// parse callbacks, and the registry handles load/save so neither
|
||||
// this module nor `Charlist::Order` clobbers the other's lines.
|
||||
//
|
||||
// `WTF\Account\<acct>\<realm>\ClassicAPI_NameCache.txt`
|
||||
// Per-realm cache contents. v2 format. One entry per line:
|
||||
// `guidHi guidLo classID raceID sex name`
|
||||
// `WTF\ClassicAPI\<realmList>\<realm>\ClassicAPI_NameCache.txt`
|
||||
// Per-realm cache contents, SHARED across every account on the
|
||||
// same server. Keyed by realmlist address (unambiguous server
|
||||
// identity — can't collide across unrelated private servers) plus
|
||||
// the realm name, rather than by account, so alts on different
|
||||
// accounts pool into one file. Not under `Account\`, so we create
|
||||
// the directory tree ourselves. v4 format. One entry per line:
|
||||
// `name guid classID raceID sex`
|
||||
// (fields tab-separated). Loaded once the realm name is
|
||||
// available post-login. Saved when (a) a recent write hasn't
|
||||
// been flushed AND (b) at least 30 seconds have elapsed since
|
||||
// the prior save, OR (c) DLL teardown / `Flush()` is called.
|
||||
// available post-login (migrating a pre-existing per-account file
|
||||
// in on first load). Saved when (a) a recent write hasn't been
|
||||
// flushed AND (b) at least 30 seconds have elapsed since the prior
|
||||
// save, OR (c) DLL teardown / `Flush()` is called.
|
||||
//
|
||||
// Population sources:
|
||||
// 1. The engine's NameCache writer (`FUN_PLAYER_INFO_CACHE_WRITE`)
|
||||
@@ -69,6 +75,7 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <istream>
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -139,7 +146,57 @@ const char *ReadRealmName() {
|
||||
|
||||
// ----- path resolution -----
|
||||
|
||||
// realmList CVar value (the realmlist address, e.g.
|
||||
// "logon.turtle-wow.org") — the unambiguous server identity. Unlike the
|
||||
// friendly realm name it can't collide across unrelated private servers,
|
||||
// so it's the outer key for the shared cache path. Mirrors
|
||||
// `Security::CredStore`'s realm scoping.
|
||||
const char *ReadRealmList() {
|
||||
using GetRealmList_t = const char *(__fastcall *)(int forceRefresh);
|
||||
return reinterpret_cast<GetRealmList_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_CVAR_REALM_LIST))(0);
|
||||
}
|
||||
|
||||
// Filesystem-safe path component. We create these directories ourselves
|
||||
// (WoW doesn't), so any character Windows rejects in a path — or that
|
||||
// could surprise, like the space/apostrophe in "N'Zoth" — becomes '_'.
|
||||
// Deterministic, so a realm always maps to the same folder.
|
||||
std::string SanitizeComponent(const char *s) {
|
||||
std::string out;
|
||||
for (const char *p = s; p != nullptr && *p; ++p) {
|
||||
const char c = *p;
|
||||
const bool safe = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') || c == '.' || c == '-' ||
|
||||
c == '_';
|
||||
out.push_back(safe ? c : '_');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Shared, server-specific cache path:
|
||||
// WTF\ClassicAPI\<realmList>\<realmName>\ClassicAPI_NameCache.txt
|
||||
// Not under Account\, so alts on different accounts of the same server
|
||||
// share one pool. Empty (defer) until both realmlist + realm are known.
|
||||
std::string CachePath() {
|
||||
const char *realmList = ReadRealmList();
|
||||
if (realmList == nullptr || realmList[0] == '\0')
|
||||
return {};
|
||||
const char *realm = ReadRealmName();
|
||||
if (realm == nullptr || realm[0] == '\0')
|
||||
return {};
|
||||
std::string out = "WTF\\ClassicAPI\\";
|
||||
out += SanitizeComponent(realmList);
|
||||
out += '\\';
|
||||
out += SanitizeComponent(realm);
|
||||
out += "\\ClassicAPI_NameCache.txt";
|
||||
return out;
|
||||
}
|
||||
|
||||
// The pre-shared per-account path, used only to migrate an existing
|
||||
// cache into the shared location on first load. Built exactly as it was
|
||||
// written (raw account + realm, unsanitized) so it matches the file
|
||||
// sitting in WoW's own folders.
|
||||
std::string OldCachePath() {
|
||||
const char *account = ReadAccountName();
|
||||
if (account == nullptr || account[0] == '\0')
|
||||
return {};
|
||||
@@ -154,6 +211,21 @@ std::string CachePath() {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Creates every parent directory of `filePath` (mkdir -p). WoW makes
|
||||
// `WTF\` but not `WTF\ClassicAPI\...`, and ofstream won't create
|
||||
// directories, so a save into a fresh realm folder would silently fail
|
||||
// without this. CreateDirectory on an existing dir no-ops.
|
||||
void EnsureParentDirs(const std::string &filePath) {
|
||||
for (size_t pos = 0;;) {
|
||||
const size_t slash = filePath.find('\\', pos);
|
||||
if (slash == std::string::npos)
|
||||
break; // remainder is the filename
|
||||
if (slash > 0)
|
||||
CreateDirectoryA(filePath.substr(0, slash).c_str(), nullptr);
|
||||
pos = slash + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- account settings (registered with Settings::Account) -----
|
||||
|
||||
void SettingsReset() {
|
||||
@@ -259,21 +331,12 @@ bool Upsert(uint64_t guid, const std::string &name,
|
||||
return changed;
|
||||
}
|
||||
|
||||
void LoadCacheIfNeeded() {
|
||||
if (g_cacheLoaded)
|
||||
return;
|
||||
const std::string path = CachePath();
|
||||
if (path.empty())
|
||||
return; // pre-realm; retry on next call (g_cacheLoaded stays false)
|
||||
g_cacheLoaded = true;
|
||||
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open())
|
||||
return; // first run for this realm
|
||||
|
||||
// v4 format: `name\tguid\tclass\trace\tsex`. Name first since
|
||||
// it's the primary key; GUID is a peripheral field carried for
|
||||
// GUID-keyed lookups. Variable-width hex GUID (no leading zeros).
|
||||
// Parses v4 cache lines (`name\tguid\tclass\trace\tsex`) from `file`
|
||||
// into the store. Name first since it's the primary key; GUID is a
|
||||
// peripheral field carried for GUID-keyed lookups. Variable-width hex
|
||||
// GUID (no leading zeros). Shared by the normal load and the one-time
|
||||
// migration from the old per-account file.
|
||||
void LoadFromStream(std::istream &file) {
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == '\n'))
|
||||
@@ -301,10 +364,42 @@ void LoadCacheIfNeeded() {
|
||||
}
|
||||
}
|
||||
|
||||
void LoadCacheIfNeeded() {
|
||||
if (g_cacheLoaded)
|
||||
return;
|
||||
const std::string path = CachePath();
|
||||
if (path.empty())
|
||||
return; // pre-realm; retry on next call (g_cacheLoaded stays false)
|
||||
g_cacheLoaded = true;
|
||||
|
||||
std::ifstream file(path);
|
||||
if (file.is_open()) {
|
||||
LoadFromStream(file);
|
||||
return;
|
||||
}
|
||||
|
||||
// No shared file yet. One-time migration: seed from the old
|
||||
// per-account file if it exists, then mark dirty so the next save
|
||||
// writes the merged data into the shared path. Only when the shared
|
||||
// file is absent — once it exists it's authoritative (a second
|
||||
// account's old-only entries just repopulate via scan / SMSG).
|
||||
const std::string oldPath = OldCachePath();
|
||||
if (oldPath.empty())
|
||||
return;
|
||||
std::ifstream oldFile(oldPath);
|
||||
if (oldFile.is_open()) {
|
||||
LoadFromStream(oldFile);
|
||||
g_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
void SaveCache() {
|
||||
const std::string path = CachePath();
|
||||
if (path.empty())
|
||||
return;
|
||||
// `WTF\ClassicAPI\<realmList>\<realm>\` isn't created by WoW — make
|
||||
// the tree first, else the ofstream open below silently fails.
|
||||
EnsureParentDirs(path);
|
||||
const std::string tmp = path + ".tmp";
|
||||
{
|
||||
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
|
||||
@@ -319,7 +414,7 @@ void SaveCache() {
|
||||
// sort/scan key when hand-editing. GUID is variable-width
|
||||
// hex — 1-8 digits for player GUIDs, up to 16 if a non-player
|
||||
// GUID ever lands here.
|
||||
out << "# ClassicAPI name cache v4 (per-realm)\n";
|
||||
out << "# ClassicAPI name cache v4 (per-realm, shared across accounts)\n";
|
||||
out << "# name\tguid\tclassID\traceID\tsex\n";
|
||||
for (const auto &kv : g_entries) {
|
||||
const std::string &name = kv.first;
|
||||
|
||||
@@ -117,6 +117,139 @@ bool SpellHiddenFromAutoLearn(int spellID) {
|
||||
return (attrEx2 & SPELL_ATTR_EX2_HIDE_FROM_AUTOLEARN) != 0;
|
||||
}
|
||||
|
||||
// ---- Target-level gate for ranked beneficial auras ----------------------
|
||||
//
|
||||
// A ranked beneficial aura can't be applied to a target too low for the
|
||||
// rank — e.g. Divine Spirit Rank 1 (14752) requires the target to be level
|
||||
// >= 20. Server-enforced via `SpellMgr::SelectAuraRankForLevel`, whose rule
|
||||
// is: a ranked positive aura is castable when `targetLevel + 10 >=
|
||||
// spellLevel`, i.e. the minimum target level is `spellLevel - 10`. Below
|
||||
// it the lowest rank fails (SPELL_FAILED_LOWLEVEL); higher ranks auto-
|
||||
// downrank. The gate only applies to *ranked* *positive* *auras* — see
|
||||
// RequiredTargetLevel. Fully derivable from the client's Spell.dbc.
|
||||
//
|
||||
// (The explicit MinTargetLevel / MaxTargetLevel fields the server also
|
||||
// checks are NOT in the 1.12 client's Spell.dbc — server-only columns — so
|
||||
// the spellLevel rule is the only client-readable target-level mechanism.)
|
||||
|
||||
constexpr int OFF_SPELL_MAX_LEVEL = 0x6C; // uint32 (scaling cap level)
|
||||
constexpr int OFF_SPELL_SPELL_LEVEL = 0x74; // uint32 (rank's effective level)
|
||||
constexpr int OFF_SPELL_EFFECT = 0xF4; // uint32[3] effect type
|
||||
constexpr int OFF_SPELL_EFFECT_TARGET_A = 0x148; // uint32[3] implicit target A
|
||||
constexpr int OFF_SPELL_RANK = 0x204; // char*[9] localized rank text
|
||||
constexpr uint32_t SPELL_ATTR_PASSIVE = 0x40; // Attributes bit
|
||||
constexpr uint32_t SPELL_EFFECT_APPLY_AURA = 6;
|
||||
constexpr uint32_t SPELL_EFFECT_APPLY_AREA_AURA_PARTY = 35;
|
||||
|
||||
int LocaleIndex() {
|
||||
return *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
}
|
||||
|
||||
// The friendly-target IDs the server's SelectAuraRankForLevel treats as
|
||||
// "positive" for rank selection: IsExplicitPositiveTarget (21/35/45/57/61)
|
||||
// plus IsAreaEffectPossitiveTarget (20/30/31/33/34/37/56/61). Narrower than
|
||||
// a general "is helpful" set — this is the exact gate the rank rule uses
|
||||
// (self-cast / pet targets don't count).
|
||||
bool IsPositiveRankTarget(uint32_t t) {
|
||||
switch (t) {
|
||||
case 21: // TARGET_UNIT_FRIEND
|
||||
case 35: // TARGET_UNIT_PARTY
|
||||
case 45: // TARGET_UNIT_FRIEND_CHAIN_HEAL
|
||||
case 57: // TARGET_UNIT_RAID
|
||||
case 61: // TARGET_UNIT_RAID_AND_CLASS
|
||||
case 20: // TARGET_ENUM_UNITS_PARTY_WITHIN_CASTER_RANGE
|
||||
case 30: // TARGET_ENUM_UNITS_FRIEND_AOE_AT_SRC_LOC
|
||||
case 31: // TARGET_ENUM_UNITS_FRIEND_AOE_AT_DEST_LOC
|
||||
case 33: // TARGET_ENUM_UNITS_PARTY_AOE_AT_SRC_LOC
|
||||
case 34: // TARGET_ENUM_UNITS_PARTY_AOE_AT_DEST_LOC
|
||||
case 37: // TARGET_UNIT_FRIEND_AND_PARTY
|
||||
case 56: // TARGET_ENUM_UNITS_RAID_WITHIN_CASTER_RANGE
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// True if the record carries a localized "Rank N" string — the client's
|
||||
// marker for a spell in a ranked line (single-rank spells leave it empty).
|
||||
// Proxy for the server's `GetSpellRank(id) != 0`, verified against
|
||||
// single-rank buffs (Power Infusion / Innervate / Blessing of Kings, all
|
||||
// empty Rank and un-gated) vs ranked ones (Divine Spirit / Fortitude).
|
||||
bool HasRankString(const uint8_t *record) {
|
||||
const char *rank = *reinterpret_cast<const char *const *>(
|
||||
record + OFF_SPELL_RANK + LocaleIndex() * 4);
|
||||
return rank != nullptr && *rank != '\0';
|
||||
}
|
||||
|
||||
// Minimum target level required to apply this exact spell (rank), or 0 if
|
||||
// the spell isn't subject to the ranked-positive-aura target-level rule.
|
||||
int RequiredTargetLevel(const uint8_t *record) {
|
||||
const int spellLevel =
|
||||
*reinterpret_cast<const int *>(record + OFF_SPELL_SPELL_LEVEL);
|
||||
if (spellLevel <= 10) // targetLevel + 10 >= spellLevel holds for any level
|
||||
return 0;
|
||||
const uint32_t attr =
|
||||
*reinterpret_cast<const uint32_t *>(record + OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
if (attr & SPELL_ATTR_PASSIVE)
|
||||
return 0;
|
||||
if (!HasRankString(record)) // single-rank spell → not gated
|
||||
return 0;
|
||||
|
||||
auto *effect = reinterpret_cast<const uint32_t *>(record + OFF_SPELL_EFFECT);
|
||||
auto *targetA =
|
||||
reinterpret_cast<const uint32_t *>(record + OFF_SPELL_EFFECT_TARGET_A);
|
||||
bool positiveAura = false;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if ((effect[i] == SPELL_EFFECT_APPLY_AURA &&
|
||||
IsPositiveRankTarget(targetA[i])) ||
|
||||
effect[i] == SPELL_EFFECT_APPLY_AREA_AURA_PARTY) {
|
||||
positiveAura = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!positiveAura)
|
||||
return 0;
|
||||
return spellLevel - 10;
|
||||
}
|
||||
|
||||
int ArgSpellID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0;
|
||||
return static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
}
|
||||
|
||||
// `C_Spell.GetSpellLevelInfo(spellID)` → spellLevel, baseLevel, maxLevel.
|
||||
// Raw Spell.dbc level fields: spellLevel (this rank's effective level),
|
||||
// baseLevel, maxLevel (level at which scaling caps). nil for an unknown
|
||||
// spellID.
|
||||
int __fastcall Script_GetSpellLevelInfo(void *L) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(ArgSpellID(L));
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + OFF_SPELL_SPELL_LEVEL)));
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_BASE_LEVEL)));
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + OFF_SPELL_MAX_LEVEL)));
|
||||
return 3;
|
||||
}
|
||||
|
||||
// `GetSpellRequiredTargetLevel(spellID)` → the minimum level a target must
|
||||
// be for this spell (rank) to apply (0 = no restriction). nil for an
|
||||
// unknown spellID. Ranked beneficial auras only; see RequiredTargetLevel.
|
||||
int __fastcall Script_GetSpellRequiredTargetLevel(void *L) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(ArgSpellID(L));
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(RequiredTargetLevel(record)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_SpellBook.GetSpellLevelLearned(spellID)` — returns the level
|
||||
// at which a spell becomes available (the `BaseLevel` field in
|
||||
// Spell.dbc). Direct read off the record — no class/race filtering,
|
||||
@@ -315,6 +448,12 @@ void RegisterLuaFunctions() {
|
||||
&Script_GetSpellLevelLearned);
|
||||
Game::Lua::RegisterTableFunction("C_SpellBook", "GetCurrentLevelSpells",
|
||||
&Script_GetCurrentLevelSpells);
|
||||
Game::Lua::RegisterGlobalFunction("GetSpellRequiredTargetLevel",
|
||||
&Script_GetSpellRequiredTargetLevel);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellLevelInfo",
|
||||
&Script_GetSpellLevelInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellRequiredTargetLevel",
|
||||
&Script_GetSpellRequiredTargetLevel);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -29,6 +29,11 @@ namespace Table::Wipe {
|
||||
// guarantee holds in 1.12's Lua 5.0 (`luaH_next` at `0x006FA2A0`),
|
||||
// so the pattern ports verbatim.
|
||||
//
|
||||
// One divergence from the 3.3.5 source: 1.12's Lua 5.0 keeps a table's
|
||||
// `table.insert`/`getn` length out of band from its keys (see the
|
||||
// `luaL_setn` note below), so after the key-clear loop we must also
|
||||
// reset that counter — the 5.1 twipe had no such legacy to clear.
|
||||
//
|
||||
// Stack discipline matches 3.3.5 to the call:
|
||||
// 1. `lua_pushnil(L)` — initial "no key" sentinel for next
|
||||
// 2. `lua_next(L, 1)` — pops key, pushes (newKey, value)
|
||||
@@ -51,6 +56,17 @@ static int __fastcall Script_table_wipe(void *L) {
|
||||
Game::Lua::RawSet(L, 1); // t[key] = nil
|
||||
}
|
||||
|
||||
// Reset the Lua 5.0 length counter. In 5.0 a table's `table.insert`/
|
||||
// `getn` length is stored OUT of band from its keys (in a `t.n`
|
||||
// field or a weak `sizes[t]` registry entry), so the rawset(nil)
|
||||
// loop above — which only clears keys — leaves it at its old value.
|
||||
// Without this, `table.insert` after a wipe appends at
|
||||
// `getn(t)+1`, past the now-nil slots, and `t[1]` stays nil. 5.1's
|
||||
// wipe doesn't need this (no getn legacy), which is why the ported
|
||||
// logic looked complete. `luaL_setn(L, 1, 0)` puts it back to 0 so
|
||||
// inserts resume at `t[1]`.
|
||||
Game::Lua::SetN(L, 1, 0);
|
||||
|
||||
// The table is still at stack[1] and is the result we return.
|
||||
Game::Lua::SetTop(L, 1);
|
||||
return 1;
|
||||
|
||||
+139
-21
@@ -126,8 +126,9 @@ int SlotsForInvType(int invType, int *out) {
|
||||
// English fallback labels, used when the client doesn't define the
|
||||
// modern `_G[key]` global-string (vanilla 1.12 largely doesn't). Phrasing
|
||||
// mirrors pfUI's eqcompare so the deltas read consistently on this
|
||||
// client. Percent stats keep a "%" suffix since the values are native
|
||||
// vanilla percentages.
|
||||
// client. Percent stats carry NO "%" here — `IsPercentStat` appends the
|
||||
// "%" to the value ("-5% Block"), so the unit is correct regardless of
|
||||
// which label (Blizzard `_SHORT` or this fallback) is used.
|
||||
struct KeyLabel {
|
||||
const char *key;
|
||||
const char *label;
|
||||
@@ -149,25 +150,46 @@ constexpr KeyLabel kFallbackLabels[] = {
|
||||
{"RESISTANCE6_NAME", "Arcane Resistance"},
|
||||
{"ITEM_MOD_ATTACK_POWER_SHORT", "Attack Power"},
|
||||
{"ITEM_MOD_RANGED_ATTACK_POWER_SHORT", "Ranged Attack Power"},
|
||||
{"ITEM_MOD_CRIT_MELEE_RATING", "Melee Crit %"},
|
||||
{"ITEM_MOD_CRIT_RANGED_RATING", "Ranged Crit %"},
|
||||
{"ITEM_MOD_HIT_MELEE_RATING", "Melee Hit %"},
|
||||
{"ITEM_MOD_HIT_RANGED_RATING", "Ranged Hit %"},
|
||||
{"ITEM_MOD_HIT_SPELL_RATING", "Spell Hit %"},
|
||||
{"ITEM_MOD_CRIT_SPELL_RATING", "Spell Crit %"},
|
||||
{"ITEM_MOD_CRIT_MELEE_RATING", "Melee Crit"},
|
||||
{"ITEM_MOD_CRIT_RANGED_RATING", "Ranged Crit"},
|
||||
{"ITEM_MOD_HIT_MELEE_RATING", "Melee Hit"},
|
||||
{"ITEM_MOD_HIT_RANGED_RATING", "Ranged Hit"},
|
||||
{"ITEM_MOD_HIT_SPELL_RATING", "Spell Hit"},
|
||||
{"ITEM_MOD_CRIT_SPELL_RATING", "Spell Crit"},
|
||||
{"ITEM_MOD_SPELL_DAMAGE_DONE_SHORT", "Spell Damage"},
|
||||
{"ITEM_MOD_SPELL_HEALING_DONE_SHORT", "Spell Healing"},
|
||||
{"ITEM_MOD_MANA_REGENERATION", "Mana Regen"},
|
||||
{"ITEM_MOD_DEFENSE_SKILL_RATING", "Defense"},
|
||||
{"ITEM_MOD_DODGE_RATING", "Dodge %"},
|
||||
{"ITEM_MOD_PARRY_RATING", "Parry %"},
|
||||
{"ITEM_MOD_BLOCK_RATING", "Block %"},
|
||||
{"ITEM_MOD_DODGE_RATING", "Dodge"},
|
||||
{"ITEM_MOD_PARRY_RATING", "Parry"},
|
||||
{"ITEM_MOD_BLOCK_RATING", "Block"},
|
||||
{"ITEM_MOD_BLOCK_VALUE", "Block Value"},
|
||||
{"ITEM_MOD_DAMAGE_PER_SECOND_SHORT", "Damage Per Second"},
|
||||
{"ITEM_DELTA_DESCRIPTION",
|
||||
"If you replace this item, the following stat changes will occur:"},
|
||||
};
|
||||
|
||||
// Percent-valued stat keys: the block/dodge/parry *chances* and the
|
||||
// crit/hit percentages. Vanilla stores these as native percentages (a
|
||||
// value of 5 means 5%), so the delta must read "-5% Block", not "-5
|
||||
// Block". The "%" is attached to the NUMBER rather than the label, so it
|
||||
// stays correct in every locale and requires no `_SHORT` change.
|
||||
// NB: ITEM_MOD_DEFENSE_SKILL_RATING (skill points) and
|
||||
// ITEM_MOD_BLOCK_VALUE (flat block amount) are NOT percentages.
|
||||
bool IsPercentStat(const char *key) {
|
||||
static const char *const kPercent[] = {
|
||||
"ITEM_MOD_CRIT_MELEE_RATING", "ITEM_MOD_CRIT_RANGED_RATING",
|
||||
"ITEM_MOD_CRIT_SPELL_RATING", "ITEM_MOD_HIT_MELEE_RATING",
|
||||
"ITEM_MOD_HIT_RANGED_RATING", "ITEM_MOD_HIT_SPELL_RATING",
|
||||
"ITEM_MOD_DODGE_RATING", "ITEM_MOD_PARRY_RATING",
|
||||
"ITEM_MOD_BLOCK_RATING",
|
||||
};
|
||||
for (const char *k : kPercent)
|
||||
if (std::strcmp(k, key) == 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *FallbackLabel(const char *key) {
|
||||
for (const auto &e : kFallbackLabels)
|
||||
if (std::strcmp(e.key, key) == 0)
|
||||
@@ -175,23 +197,112 @@ const char *FallbackLabel(const char *key) {
|
||||
return key; // last resort — raw key beats crashing
|
||||
}
|
||||
|
||||
// Resolve a display label for `key` into `out`. Prefers the client's
|
||||
// `_G[key]` when it's a plain, non-empty string (respects localization /
|
||||
// server-defined labels), read via a direct globals-table access — no
|
||||
// pcall. Ignores format strings (a "%"-bearing global like "+%d Strength"
|
||||
// would render garbage), falling back to the built-in English label.
|
||||
void LabelFor(void *L, const char *key, char *out, int outSize) {
|
||||
// True if `s` contains a printf conversion (`%d`, `%s`, `%1$s`, …) as
|
||||
// opposed to a literal `%` used as a unit. A `%` at end-of-string, before
|
||||
// a space, or escaped as `%%` is treated as literal. Lets locale labels
|
||||
// like "Dodge %" through while still rejecting a format-string global
|
||||
// such as "+%d Strength" (which would render garbage via snprintf "%s").
|
||||
bool HasFormatSpecifier(const char *s) {
|
||||
for (const char *p = std::strchr(s, '%'); p != nullptr;
|
||||
p = std::strchr(p + 1, '%')) {
|
||||
const char n = p[1];
|
||||
if (n == '%') { // escaped "%%" — skip both, keep scanning
|
||||
++p;
|
||||
continue;
|
||||
}
|
||||
if (n != '\0' && n != ' ')
|
||||
return true; // "%d", "%s", "%1$..." → real conversion
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EndsWithShort(const char *key) {
|
||||
const size_t n = std::strlen(key);
|
||||
return n >= 6 && std::strcmp(key + n - 6, "_SHORT") == 0;
|
||||
}
|
||||
|
||||
// Raw `_G[key]` string fetch — no format-specifier filtering. Direct
|
||||
// globals-table access, no pcall. Returns whether a non-empty string was
|
||||
// copied into `out`.
|
||||
bool TryGlobalRaw(void *L, const char *key, char *out, int outSize) {
|
||||
const int saved = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
const char *g = (Game::Lua::Type(L, -1) == Game::Lua::TYPE_STRING)
|
||||
? Game::Lua::ToString(L, -1)
|
||||
: nullptr;
|
||||
if (g != nullptr && *g != '\0' && std::strchr(g, '%') == nullptr)
|
||||
const bool ok = (g != nullptr && *g != '\0');
|
||||
if (ok)
|
||||
std::snprintf(out, outSize, "%s", g);
|
||||
else
|
||||
std::snprintf(out, outSize, "%s", FallbackLabel(key));
|
||||
Game::Lua::SetTop(L, saved);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Copies `_G[key]` into `out` iff it's a plain, non-empty, non-format
|
||||
// string. Returns whether it took.
|
||||
bool TryGlobalLabel(void *L, const char *key, char *out, int outSize) {
|
||||
char tmp[192];
|
||||
if (!TryGlobalRaw(L, key, tmp, sizeof(tmp)) || HasFormatSpecifier(tmp))
|
||||
return false;
|
||||
std::snprintf(out, outSize, "%s", tmp);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Formats a signed value token (e.g. "-5") as a localized percentage via
|
||||
// the PERCENTAGE_STRING global (typically "%s%%" → "-5%"). The locale
|
||||
// format is NEVER passed to snprintf — we substitute `num` into its first
|
||||
// conversion slot (`%s`/`%d`) by hand and collapse `%%` → `%`, so a
|
||||
// hostile or unexpected format can't misread the argument list. Falls back
|
||||
// to a plain "%" suffix when the global is absent.
|
||||
void FormatPercent(void *L, const char *num, char *out, int outSize) {
|
||||
char fmt[64];
|
||||
if (!TryGlobalRaw(L, "PERCENTAGE_STRING", fmt, sizeof(fmt))) {
|
||||
std::snprintf(out, outSize, "%s%%", num);
|
||||
return;
|
||||
}
|
||||
int o = 0;
|
||||
bool inserted = false;
|
||||
for (const char *p = fmt; *p != '\0' && o < outSize - 1; ++p) {
|
||||
if (p[0] == '%' && p[1] == '%') { // literal percent
|
||||
out[o++] = '%';
|
||||
++p;
|
||||
} else if (p[0] == '%' && p[1] != '\0' && !inserted) { // value slot
|
||||
for (const char *q = num; *q != '\0' && o < outSize - 1; ++q)
|
||||
out[o++] = *q;
|
||||
++p; // consume the conversion letter (s / d)
|
||||
inserted = true;
|
||||
} else {
|
||||
out[o++] = *p;
|
||||
}
|
||||
}
|
||||
out[o] = '\0';
|
||||
}
|
||||
|
||||
// Resolve a display label for a stat `key` into `out`, respecting
|
||||
// localization. The rating/regen tokens GetItemStats keys on
|
||||
// (`ITEM_MOD_CRIT_MELEE_RATING`, …) carry a "%s" *description* as their
|
||||
// own global; their `_SHORT` sibling is the compact label a delta wants.
|
||||
// So for a key that isn't already `_SHORT`, prefer `_G[key.."_SHORT"]`:
|
||||
// 1. `_G[key.."_SHORT"]` — the compact label (skipped if `key` already
|
||||
// ends in `_SHORT`, e.g. the primary stats, or has no `_SHORT`
|
||||
// variant like `RESISTANCE*_NAME` / `CURRENTLY_EQUIPPED`).
|
||||
// 2. `_G[key]` — the key itself (primary stats resolve here; rating
|
||||
// tokens only land here if no `_SHORT` global exists).
|
||||
// 3. Built-in English `FallbackLabel(key)`.
|
||||
// Preferring `_SHORT` first (rather than only when `_G[key]` fails) means
|
||||
// a server/addon defining the base rating token as a plain string can't
|
||||
// shadow the compact `_SHORT` label. StatAccum keys stay the canonical
|
||||
// GetItemStats tokens; this `_SHORT` mapping is display-only.
|
||||
void LabelFor(void *L, const char *key, char *out, int outSize) {
|
||||
if (!EndsWithShort(key)) {
|
||||
char shortKey[160];
|
||||
std::snprintf(shortKey, sizeof(shortKey), "%s_SHORT", key);
|
||||
if (TryGlobalLabel(L, shortKey, out, outSize))
|
||||
return;
|
||||
}
|
||||
if (TryGlobalLabel(L, key, out, outSize))
|
||||
return;
|
||||
std::snprintf(out, outSize, "%s", FallbackLabel(key));
|
||||
}
|
||||
|
||||
// Delta colors — the canonical GlobalColor.dbc tags modern WoW uses for
|
||||
@@ -237,7 +348,14 @@ void RenderDeltas(void *self, void *L, const Accum *acc, double dpsDelta) {
|
||||
if (v == 0)
|
||||
continue;
|
||||
LabelFor(L, acc[i].key, label, sizeof(label));
|
||||
std::snprintf(line, sizeof(line), "%s%ld %s", v > 0 ? "+" : "", v, label);
|
||||
char num[48];
|
||||
std::snprintf(num, sizeof(num), "%s%ld", v > 0 ? "+" : "", v);
|
||||
if (IsPercentStat(acc[i].key)) {
|
||||
char pct[64];
|
||||
FormatPercent(L, num, pct, sizeof(pct));
|
||||
std::snprintf(num, sizeof(num), "%s", pct);
|
||||
}
|
||||
std::snprintf(line, sizeof(line), "%s %s", num, label);
|
||||
AddColoredLine(self, line, v > 0 ? kGain : kLoss);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
// 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/>.
|
||||
|
||||
// Pet / minion classification of a unit:
|
||||
// - `UnitIsMinion(unit)` — modern backport: is the unit a
|
||||
// minion of a player (pet, guardian, totem, charmed creature)?
|
||||
// - `UnitIsOtherPlayersPet(unit)` — the real API: is the unit a pet
|
||||
// controlled by a player OTHER than you?
|
||||
//
|
||||
// Neither exists in vanilla 1.12 nor in our 3.3.5 reference client
|
||||
// (`UnitIsMinion` is a 12.0.0/5.5.4 addition; `UnitIsOtherPlayersPet`
|
||||
// is 5.0.4/1.13.2), so both are derived from vanilla primitives:
|
||||
// * `Unit::Flags::IsPlayerControlled` — UNIT_FLAG_PLAYER_CONTROLLED,
|
||||
// set on players and every unit a player controls (pets, guardians,
|
||||
// totems, charmed mobs).
|
||||
// * The unit's GUID type — vanilla packs the entity type in the GUID's
|
||||
// high bits (`Guid::Classify`): players `0x0000`, pets `0xF140`,
|
||||
// creatures `0xF130`. A pet-type GUID is intrinsically a player's pet.
|
||||
//
|
||||
// `UnitIsMinion` = owned by a player (its SummonedBy/CreatedBy/CharmedBy
|
||||
// GUID resolves to a player), OR player-controlled-and-not-a-player as a
|
||||
// fallback. Owner-based is primary because the player-controlled flag
|
||||
// alone misses summoned guardians — units shown as "X's Minion" that carry
|
||||
// an owner GUID but not the flag (a real hunter/warlock "X's Pet" has
|
||||
// both). The union keeps flagged pets working regardless of the owner
|
||||
// fields.
|
||||
//
|
||||
// `UnitIsOtherPlayersPet` is **owner-based**, matching 5.4.8's real
|
||||
// `Script_UnitIsOtherPlayersPet` (`0x0089FD70`): resolve the unit, read
|
||||
// its owner GUID (SummonedBy / CreatedBy / CharmedBy), and return true iff
|
||||
// that owner is a *player* other than you. Owner-based (not a pet-GUID
|
||||
// heuristic) so it covers pets, guardians, totems, and charmed units of
|
||||
// other players alike, and excludes *all* of your own minions (owner ==
|
||||
// you), not just the active pet. The owner-field offsets are byte-verified
|
||||
// / adjacency-derived — see `OFF_UNIT_FIELD_CHARMEDBY`.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "unit/Flags.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::Pet {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
// `int __fastcall(unit)` → 1 for a controllable pet, non-1 otherwise.
|
||||
// The engine's pet-vs-minion title discriminator. See
|
||||
// FUN_UNIT_PET_MINION_CLASS.
|
||||
using PetMinionClass_t = int(__fastcall *)(const void *unit);
|
||||
|
||||
const uint8_t *ResolveUnit(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1))
|
||||
return nullptr;
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return static_cast<const uint8_t *>(resolve(token));
|
||||
}
|
||||
|
||||
// The unit's owner/controller GUID: CharmedBy, else CreatedBy (the two
|
||||
// fields the engine itself uses for ownership). 0 for players, world
|
||||
// creatures, and anything unowned.
|
||||
uint64_t OwnerGuid(const uint8_t *unit) {
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
const int owners[] = {
|
||||
Offsets::OFF_UNIT_FIELD_CHARMEDBY,
|
||||
Offsets::OFF_UNIT_FIELD_CREATEDBY,
|
||||
};
|
||||
for (int off : owners) {
|
||||
const uint64_t g = *reinterpret_cast<const uint64_t *>(desc + off);
|
||||
if (g != 0)
|
||||
return g;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `UnitIsMinion(unit)` — true iff the unit is a player's minion (pet,
|
||||
// guardian, totem, or charmed creature): player-controlled but not itself
|
||||
// a player. NPCs, world creatures, and players return false. A bad or
|
||||
// unresolved unit token returns false (no error), matching the tolerant
|
||||
// modern predicate convention.
|
||||
int __fastcall Script_UnitIsMinion(void *L) {
|
||||
const uint8_t *unit = ResolveUnit(L);
|
||||
if (unit == nullptr) {
|
||||
Game::Lua::PushBool(L, 0);
|
||||
return 1;
|
||||
}
|
||||
// Primary: owned by a player (pet, guardian, totem, charmed).
|
||||
bool minion = Guid::IsPlayer(OwnerGuid(unit));
|
||||
// Fallback: a player-controlled non-player with no owner GUID.
|
||||
if (!minion && Unit::Flags::IsPlayerControlled(unit))
|
||||
minion = !Guid::IsPlayer(Unit::Identity::GuidForObject(unit));
|
||||
Game::Lua::PushBool(L, minion);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `UnitIsPet(unit)` — true iff the unit is a **pet** of a player (as
|
||||
// opposed to a guardian/totem "minion"): owned by a player AND the
|
||||
// engine's pet/minion classifier `FUN_UNIT_PET_MINION_CLASS` returns 1.
|
||||
// That classifier — reading the creature *family*, not the GUID or the
|
||||
// player-controlled flag — is exactly what the engine uses to title a unit
|
||||
// "X's Pet" vs "X's Minion". On 1.12/Turtle the GUID can't be used (pets
|
||||
// and guardians share the `0xF14…` prefix) and the PLAYER_CONTROLLED flag
|
||||
// is unreliable (a "Minion" can carry it), so the family classifier is the
|
||||
// only correct signal. The owner-is-player gate also rules out wild
|
||||
// tameable beasts of the same family (which aren't anyone's pet).
|
||||
// `UnitIsMinion` is the broad umbrella (pet + guardian + totem); this is
|
||||
// the pet subset. A ClassicAPI extension — not a stock WoW global.
|
||||
int __fastcall Script_UnitIsPet(void *L) {
|
||||
const uint8_t *unit = ResolveUnit(L);
|
||||
if (unit == nullptr) {
|
||||
Game::Lua::PushBool(L, 0);
|
||||
return 1;
|
||||
}
|
||||
// Owned by a player (rules out wild same-family beasts), AND the
|
||||
// engine's own pet/minion classifier says PET (1). The classifier —
|
||||
// not the GUID, not the player-controlled flag — is what the engine
|
||||
// uses to title "X's Pet" vs "X's Minion".
|
||||
bool pet = false;
|
||||
if (Guid::IsPlayer(OwnerGuid(unit))) {
|
||||
auto classify = reinterpret_cast<PetMinionClass_t>(
|
||||
Offsets::FUN_UNIT_PET_MINION_CLASS);
|
||||
pet = (classify(unit) == 1);
|
||||
}
|
||||
Game::Lua::PushBool(L, pet);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `UnitIsOtherPlayersPet(unit)` — true iff the unit is a minion (pet,
|
||||
// guardian, totem, charmed) whose owner is a player other than you.
|
||||
// Owner-based, mirroring 5.4.8: read the unit's owner GUID, require it to
|
||||
// be a player, and require it to differ from the local player. World
|
||||
// creatures and players (no owner) return false; your own minions (owner
|
||||
// == you) return false.
|
||||
int __fastcall Script_UnitIsOtherPlayersPet(void *L) {
|
||||
const uint8_t *unit = ResolveUnit(L);
|
||||
if (unit == nullptr) {
|
||||
Game::Lua::PushBool(L, 0);
|
||||
return 1;
|
||||
}
|
||||
const uint64_t owner = OwnerGuid(unit);
|
||||
const bool otherPlayers = Guid::IsPlayer(owner) &&
|
||||
owner != Unit::Identity::PlayerGuid();
|
||||
Game::Lua::PushBool(L, otherPlayers);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitIsMinion", &Script_UnitIsMinion);
|
||||
Game::Lua::RegisterGlobalFunction("UnitIsPet", &Script_UnitIsPet);
|
||||
Game::Lua::RegisterGlobalFunction("UnitIsOtherPlayersPet",
|
||||
&Script_UnitIsOtherPlayersPet);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Unit::Pet
|
||||
+62
-14
@@ -8,11 +8,21 @@
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
// `UnitInRange(unit) → (inRange, checkedRange)` — 40-yard healing-
|
||||
// range check, modern's `UnitInRange` shape. Vanilla 1.12 doesn't
|
||||
// ship the function but has all the underlying machinery — the
|
||||
// same `GetPosition` virtual (vtable slot 5) `CheckInteractDistance`
|
||||
// uses, applied with a fixed 40-yard threshold.
|
||||
// Player↔unit proximity, modern's shape. Vanilla 1.12 ships neither
|
||||
// function but has all the underlying machinery — the object's
|
||||
// `GetPosition` virtual (vtable slot 5) `CheckInteractDistance` uses:
|
||||
//
|
||||
// `UnitInRange(unit) → (inRange, checkedRange)` — 40-yard healing-
|
||||
// range check (fixed threshold on the squared distance).
|
||||
// `UnitDistanceSquared(unit) → (distanceSquared, checkedPosition)` —
|
||||
// the raw squared world distance player→unit. Squared because
|
||||
// nearly every consumer compares against a threshold (`distSq <=
|
||||
// range*range`) or ranks by nearest, neither of which needs the
|
||||
// sqrt; that's why retail exposes only the squared form (added
|
||||
// 5.0.4). Center-to-center — NOT reach-aware (that's UnitXP_SP3's
|
||||
// niche); equivalent to computing it from SuperWoW `UnitPosition`,
|
||||
// but self-contained (reads the position in C, no sibling-DLL
|
||||
// dependency, no clash with SuperWoW's own `UnitPosition`).
|
||||
|
||||
#include "Game.h"
|
||||
#include "unit/Position.h"
|
||||
@@ -26,6 +36,24 @@ namespace {
|
||||
constexpr float kRangeYards = 40.0f;
|
||||
constexpr float kRangeYardsSq = kRangeYards * kRangeYards;
|
||||
|
||||
// Squared world distance from the player to `token`'s unit. Returns true
|
||||
// and writes *outSq on success; false when either position is unavailable
|
||||
// (unresolved / absent token, or an object with no known position yet —
|
||||
// e.g. a party member outside the client's sync range). This false is
|
||||
// exactly the `checkedPosition = false` case retail flags.
|
||||
bool PlayerToUnitDistSq(const char *token, float *outSq) {
|
||||
float unitPos[3] = {};
|
||||
float playerPos[3] = {};
|
||||
if (!Unit::Position::ReadToken(token, unitPos) ||
|
||||
!Unit::Position::ReadToken("player", playerPos))
|
||||
return false;
|
||||
const float dx = unitPos[0] - playerPos[0];
|
||||
const float dy = unitPos[1] - playerPos[1];
|
||||
const float dz = unitPos[2] - playerPos[2];
|
||||
*outSq = dx * dx + dy * dy + dz * dz;
|
||||
return true;
|
||||
}
|
||||
|
||||
int __fastcall Script_UnitInRange(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitInRange(\"unit\")");
|
||||
@@ -44,27 +72,47 @@ int __fastcall Script_UnitInRange(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
float unitPos[3] = {};
|
||||
float playerPos[3] = {};
|
||||
if (!Unit::Position::ReadToken(token, unitPos) ||
|
||||
!Unit::Position::ReadToken("player", playerPos)) {
|
||||
float distSq = 0.0f;
|
||||
if (!PlayerToUnitDistSq(token, &distSq)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const float dx = unitPos[0] - playerPos[0];
|
||||
const float dy = unitPos[1] - playerPos[1];
|
||||
const float dz = unitPos[2] - playerPos[2];
|
||||
const float distSq = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
Game::Lua::PushBool(L, distSq <= kRangeYardsSq);
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `UnitDistanceSquared("unit") → (distanceSquared, checkedPosition)`.
|
||||
// Unlike UnitInRange there's no self-quirk: `UnitDistanceSquared("player")`
|
||||
// is a legitimate `(0, true)`. On a position miss returns `(0, false)` —
|
||||
// matching retail's "always a number" shape; consumers must branch on
|
||||
// `checkedPosition`, since a real `0` (self, or exactly co-located units)
|
||||
// is indistinguishable from the miss placeholder by value alone.
|
||||
int __fastcall Script_UnitDistanceSquared(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitDistanceSquared(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
|
||||
float distSq = 0.0f;
|
||||
if (!PlayerToUnitDistSq(token, &distSq)) {
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
}
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(distSq));
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 2;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitInRange", &Script_UnitInRange);
|
||||
Game::Lua::RegisterGlobalFunction("UnitDistanceSquared",
|
||||
&Script_UnitDistanceSquared);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
Reference in New Issue
Block a user