Compare commits

...
11 Commits
Author SHA1 Message Date
Brues 983cc3bdff unit: add UnitDistanceSquared
Backport retail's UnitDistanceSquared(unit) -> (distanceSquared,
checkedPosition): the squared world distance from the player to the unit,
plus a flag for whether both positions were readable. Squared because
almost all distance logic is a threshold compare or nearest-unit sort,
neither of which needs the sqrt — which is why retail never shipped a plain
UnitDistance.

Added to the existing Unit::Range module alongside UnitInRange (both now
share one PlayerToUnitDistSq compute off the CGObject::GetPosition vtable
virtual). Center-to-center, self-contained (reads the position in C — no
SuperWoW dependency, and leaves SuperWoW's own UnitPosition untouched).
Verified in-game to match SuperWoW UnitPosition math to float precision.

On a position miss returns (0, false); no self-quirk, so
UnitDistanceSquared("player") is a legitimate (0, true).
2026-07-13 00:23:58 -05:00
Brues 18ac033381 spell: add GetSpellLevelInfo + GetSpellRequiredTargetLevel
Expose the target-level gate for ranked beneficial auras — e.g. Divine
Spirit Rank 1 (14752) can only be applied to a target level >= 20.

- C_Spell.GetSpellLevelInfo(spellID) -> spellLevel, baseLevel, maxLevel
  (raw Spell.dbc level fields; nil for an unknown spell).
- GetSpellRequiredTargetLevel(spellID) (global, plus C_Spell mirror) ->
  the minimum castable target level (0 = no restriction; nil if unknown).

The computed helper mirrors the server's SpellMgr::SelectAuraRankForLevel:
returns spellLevel - 10 only when the spell is a ranked positive aura
(spellLevel > 10, not passive, has a Rank string, and has an APPLY_AURA
effect on a friendly/party/raid target or an APPLY_AREA_AURA_PARTY effect),
else 0. All fields read directly from the client's Spell.dbc.

The explicit MinTargetLevel / MaxTargetLevel fields the server also checks
are server-only columns (absent from the 1.12 client DBC, whose tail is
SpellFamilyName / SpellFamilyFlags / MaxAffectedTargets), so the spellLevel
rule is the only client-readable target-level mechanism.
2026-07-12 23:28:41 -05:00
Brues 81434f7014 aura: evict phantom cache entries on removal and descriptor reconcile
C_UnitAuras' descriptor-drop fallback could surface a "phantom" aura that
was actually gone — enumerated with a spellId but SetUnitAura rendering an
empty tooltip. Two cases, both fixed:

- Owner cancels a buff (e.g. a flask) while dead: the server keeps
  death-persistent auras through death without changing any field, so death
  itself fires no OnAuraRemoved. Any OnAuraRemoved we do receive for one is
  genuine (cancel/dispel/expiry), so always evict the cache entry — including
  death-persistent auras on dead units. Fixes the phantom for the caster and
  nearby observers (both receive the removal packet).

- A buff cancelled while its owner is out of an observer's range sends no
  packet, so the stale entry lingers. Reconcile on return: when a unit's
  descriptor holds >=1 raw aura it's in view and fully synced, so it's
  authoritative — evict any cached entry whose spell isn't in the array
  (Aura::Data::ReconcileCache -> Aura::Source::EvictAbsent, run at the top of
  the fallback path). Reads raw slots so a flag-hidden aura still counts as
  present; skips an empty array (can't tell out-of-range from buffless).

The only residual is a buff cancelled while the owner is still out of range,
which vanilla can't report until they return.
2026-07-12 17:10:27 -05:00
Brues 22605dad64 tooltip: format compare-item percent stats via PERCENTAGE_STRING
Block/dodge/parry chance and crit/hit are native vanilla percentages, so
their compare-tooltip delta read as a bare number ("-5 Block"). Attach the
percent via the locale-aware PERCENTAGE_STRING global so it renders
"-5% Block" without touching any _SHORT label (correct in every locale).

IsPercentStat marks the percent keys; FormatPercent substitutes the value
into PERCENTAGE_STRING's first conversion slot by hand (never feeds the
locale format to snprintf) with a plain "%" fallback. Flat stats (Defense,
Attack Power, Block Value) stay bare; dropped the now-redundant trailing
" %" from the English fallback labels.
2026-07-12 14:23:54 -05:00
Brues 7378e0b128 unit: add UnitIsMinion / UnitIsPet / UnitIsOtherPlayersPet
Player-minion classification for a unit, all owner-based:

- UnitIsMinion(unit): owned by a player (pet/guardian/totem). Primary
  check is the owner GUID, since summoned guardians ("X's Minion") carry
  an owner but not UNIT_FLAG_PLAYER_CONTROLLED; the flag is a fallback.
- UnitIsPet(unit): a player's controllable pet specifically -- owned by a
  player AND the engine's creature-family pet/minion classifier
  FUN_00605570 == 1, the same signal driving the UNITNAME_TITLE_PET vs
  _MINION tooltip label. The GUID can't be used (pets and guardians share
  the 0xF14 prefix) nor can PLAYER_CONTROLLED (a Minion can have it).
  ClassicAPI extension.
- UnitIsOtherPlayersPet(unit): owner is a player other than you; matches
  5.4.8's owner-based Script_UnitIsOtherPlayersPet.

Owner GUID read from CHARMEDBY (+0x10, verified via Script_UnitIsCharmed)
and CREATEDBY (+0x20, verified via the unit title builder FUN_0052FD30).
Verified in-game: Salvatory's Pet is a pet; Jimmyf's/Methl's Minion is a
minion but not a pet.
2026-07-12 03:06:11 -05:00
Brues 94b0545631 player: add C_PlayerInfo.CanUseItem (equip/use requirement gate)
The proficiency/level/class/skill/spell/rep gate that answers "can this
player equip or use this item" -- the tooltip's red-requirement logic,
distinct from IsEquippableItem (fits a slot) and IsUsableItem (on-use
castable). Mirrors the requirement checks FUN_0052B650 inlines:

  1. proficiency  - VAR_PROFICIENCY_TABLE[m_class] & (1<<m_subClass)
  2. RequiredLevel
  3. AllowableClass / AllowableRace
  4. RequiredSkill  - effective rank via FUN_SKILL_LINE_TO_SLOT
  5. RequiredSpell  - spell-knowledge bitmap (crafting specializations
                      like Armorsmith, proficiency spells)
  6. RequiredHonorRank / RequiredCityRank - PvP honor rank + medal mask
  7. RequiredReputation - current standing vs the reaction-min band

Verified in-game: mail boots false (proficiency), Riding mount false
without skill 150, Plans: Lionheart Helm false until Armorsmith learned.
Residual gap: a ranked RequiredSpell known only at a different rank.

Adds OFF_CGPLAYER_HONOR_RANK / OFF_CGPLAYER_PVP_MEDALS.
2026-07-12 01:54:05 -05:00
Brues 243ab37836 tooltip: localize item-compare stat labels via GlobalStrings keys
Compare-tooltip stat deltas now resolve their display label through
_G[key] (the WoW GlobalString), falling back to the built-in English
only when undefined. LabelFor tries _G[key], then _G[key.."_SHORT"],
then the English fallback: primary stats / resistances / CURRENTLY_EQUIPPED
resolve directly, while the rating/regen tokens GetItemStats keys on
(ITEM_MOD_CRIT_MELEE_RATING, ...) — whose own global is a "%s"
description — map to their compact, translatable _SHORT sibling for
display. StatAccum keys are left untouched (the GetItemStats contract,
confirmed against a TBC dump: ratings key on the non-_SHORT token).

The format guard now rejects only real printf conversions (%d/%s/%1$s),
letting a literal "%" unit through.

enUS.lua gains the ITEM_MOD_*_SHORT, ITEM_DELTA_DESCRIPTION and format
keys (native RESISTANCE*/CURRENTLY_EQUIPPED left to the client). The
non-US locales (deDE/esES/frFR/ruRU/zhCN/zhTW) are auto-filled from
Blizzard Classic (1.15) per-locale GlobalStrings; only the custom
BINDING_NAME_AOELOOT stays commented as an enUS fallback.

Fixes #8
2026-07-11 16:40:10 -05:00
Brues a12fff6704 Small refactor for clarity 2026-07-11 16:28:46 -05:00
Brues d747c699f5 Tighten up pfUI HookScript compat
Defer CreateFrame hook application until the updatenotify module loads, rather than applying it globally so it only affects the actionar module
2026-07-11 16:21:43 -05:00
Brues 88d76c04ad item: add IsUsableItem / C_Item.IsUsableItem (on-use usability)
Item analog of IsUsableSpell, returning (usable, noMana) for an item's
on-use ability -- not equippability. Gates on the item having an ON_USE
spell, its use requirements (RequiredLevel, AllowableClass/Race), then
the on-use spell's castability via the engine helper FUN_SPELL_IS_USABLE
(0x006E3D60), which sets noMana on power shortfall.

Shares the on-use spell-slot walk with C_Item.GetItemSpell: promoted
Item::Spell::FindOnUseSpellIDInRecord to the header so both use it
instead of re-deriving the trigger scan.
2026-07-11 14:55:20 -05:00
Brues 47d4b9c660 loot: stable rollID, winnerIdx, FULL_UPDATE dispatch, timestamp, Clear
Bring C_LootHistory closer to the MoP contract and make a UI keying on it
robust across the ring saturating.

- GetItem now returns rollID, itemLink, numPlayers, isDone, winnerIdx,
  timestamp (was itemLink-first, with the winner as a name string). rollID
  is a stable monotonic ID assigned at creation, so addons can key expanded
  / highlighted rows on it instead of the volatile ring index. winnerIdx is
  the winner 1-based player index for GetPlayerInfo, matching MoP; the winner
  is inserted into the roster if their own roll packet never arrived, so it
  always resolves. timestamp is a ClassicAPI extension (GetTime()-comparable
  seconds).
- LOOT_HISTORY_FULL_UPDATE is now actually dispatched (was reserved-only):
  on a new item appearing (roll open) and on ring eviction, fired before the
  granular ROLL_CHANGED / ROLL_COMPLETE.
- Add C_LootHistory.Clear() (ClassicAPI extension) to wipe history and fire
  FULL_UPDATE.
- Factor roll/winner roster insertion into a shared FindOrAddPlayer helper.

Docs updated for the new signature, Clear, and the FULL_UPDATE semantics.
2026-07-10 21:03:51 -05:00
23 changed files with 1885 additions and 149 deletions
+8 -7
View File
@@ -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)
+42 -1
View File
@@ -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.";
+24
View File
@@ -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";
+42 -1
View File
@@ -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.";
+42 -1
View File
@@ -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.";
+42 -1
View File
@@ -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 = "Щелкните здесь, чтобы создать ссылку на вашу профессию.";
+42 -1
View File
@@ -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 = "点击这里以创建一个链接到你的专业的快捷方式。";
+42 -1
View File
@@ -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 = "點擊這裡以製造一個你專業技能的連結。";
+5 -4
View File
@@ -43,17 +43,18 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [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` |
+302 -38
View File
@@ -227,6 +227,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 +300,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 +359,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 +423,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 +2343,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.
@@ -5278,6 +5287,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 +6252,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 +6285,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
@@ -7155,40 +7227,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 +7322,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 +8927,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 +10088,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 +10212,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
+61
View File
@@ -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
+27
View File
@@ -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
View File
@@ -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;
+12
View File
@@ -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
+23 -23
View File
@@ -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;
+8
View File
@@ -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
+180
View File
@@ -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
View File
@@ -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};
+312
View File
@@ -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
+139
View File
@@ -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};
+139 -21
View File
@@ -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);
}
}
+177
View File
@@ -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
View File
@@ -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};