Compare commits

..
3 Commits
Author SHA1 Message Date
Brues e760d5f07e Drop focus when out of render distance 2026-05-27 23:44:22 -05:00
Brues d0b6a7c0f3 C_Container.GetContainerItemCharges 2026-05-27 23:43:28 -05:00
brues-codeandBrues 96b0cf6075 FocusUnit / ClearFocus + focus unit token (#2)
* FocusUnit

* UnitTokenFromGUID: place focus after target, not after mouseover

Retail's documented scan order is
  player -> vehicle -> pet -> party -> partypet -> raid -> raidpet
        -> nameplate -> arena -> arenapet -> boss -> target -> focus
        -> npc -> mouseover -> softenemy -> softfriend -> softinteract

Earlier commit placed focus at the very end. Moves it into the
target-adjacent slot so a focused unit reverse-resolves to "focus"
the moment it isn't also "target", matching retail.

---------

Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
2026-05-27 19:34:12 -05:00
10 changed files with 545 additions and 70 deletions
+4 -1
View File
@@ -23,12 +23,13 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
| [Combat](docs/API.md#combat) | `InCombatLockdown` |
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
| [Container](docs/API.md#container) | `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
| [Container](docs/API.md#container) | `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` (sets persist to `WTF\Account\...\ClassicAPI_EquipmentSets.txt`) |
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid` |
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura` |
@@ -98,6 +99,7 @@ functions, just behavior the stock 1.12 engine didn't have. See the
| `NAME_PLATE_CREATED` | `nameplateFrame` |
| `NAME_PLATE_UNIT_ADDED` | `unitToken` ("nameplateN") |
| `NAME_PLATE_UNIT_REMOVED` | `unitToken` ("nameplateN") |
| `PLAYER_FOCUS_CHANGED` | *(none)* |
| `PLAYER_STARTED_LOOKING` | *(none)* |
| `PLAYER_STOPPED_LOOKING` | *(none)* |
| `PLAYER_STARTED_MOVING` | *(none)* |
@@ -125,6 +127,7 @@ functions, just behavior the stock 1.12 engine didn't have. See the
| Token | Resolves to |
|-------|-------------|
| `nameplate1`..`nameplateN` | Unit behind the Nth visible nameplate, in creation-order. Works with every `UnitX` function — `UnitName`, `UnitGUID`, `UnitClass`, `UnitHealth`, etc. Suffix chains (`nameplate1target`, `nameplate1targettarget`) compose. See [NamePlate / Unit tokens](docs/API.md#unit-tokens-nameplaten). |
| `focus` / `focustarget` | Sticky target set via [`FocusUnit`](docs/API.md#focusunitunit), cleared via [`ClearFocus`](docs/API.md#clearfocus). Same `UnitX` coverage as `nameplateN`. Fires [`PLAYER_FOCUS_CHANGED`](docs/API.md#player_focus_changed-event) on transition. See [Focus](docs/API.md#focus). |
## Installation
+176 -10
View File
@@ -34,6 +34,7 @@ build instructions.
- [`GetItemCooldown(itemInfo)` / `C_Container.GetItemCooldown(itemID)`](#getitemcooldowniteminfo--c_containergetitemcooldownitemid)
- [`C_Container.GetContainerItemDurability(containerIndex, slotIndex)`](#c_containergetcontaineritemdurabilitycontainerindex-slotindex)
- [`C_Container.GetContainerItemRepairCost(containerIndex, slotIndex)`](#c_containergetcontaineritemrepaircostcontainerindex-slotindex)
- [`C_Container.GetContainerItemCharges(containerIndex, slotIndex)`](#c_containergetcontaineritemchargescontainerindex-slotindex)
- [`C_Container.GetContainerNumFreeSlots(bagID)`](#c_containergetcontainernumfreeslotsbagid)
- [`C_Container.PlayerHasHearthstone()`](#c_containerplayerhashearthstone)
- [`C_Container.UseHearthstone()`](#c_containerusehearthstone)
@@ -74,6 +75,7 @@ build instructions.
- [`FACTION_STANDING_CHANGED` event](#faction_standing_changed-event)
- [`MODIFIER_STATE_CHANGED` event](#modifier_state_changed-event)
- [`NAME_PLATE_CREATED` / `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` events](#name_plate_created--name_plate_unit_added--name_plate_unit_removed-events)
- [`PLAYER_FOCUS_CHANGED` event](#player_focus_changed-event)
- [`QUEST_ACCEPTED` event](#quest_accepted-event)
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
- [`UPDATE_SHAPESHIFT_FORM` event](#update_shapeshift_form-event)
@@ -93,6 +95,11 @@ build instructions.
- [`C_Reputation.SetWatchedFactionByID(factionID)`](#c_reputationsetwatchedfactionbyidfactionid)
- [`C_Reputation.GetLastStandingChange()`](#c_reputationgetlaststandingchange)
- [Focus](#focus)
- [`FocusUnit(unit)`](#focusunitunit)
- [`ClearFocus()`](#clearfocus)
- [Unit token (`focus` / `focustarget`)](#unit-token-focus--focustarget)
- [FriendList](#friendlist)
- [`C_FriendList.SendWhoQueryByName(name)`](#c_friendlistsendwhoquerybynamename)
- [`C_FriendList.IsWhoQueryPending()`](#c_friendlistiswhoquerypending)
@@ -764,6 +771,62 @@ Useful for "repair only items above N copper" smart-repair logic
without scanning tooltips. ClassicAPI addition; modern WoW has no
direct equivalent.
### `C_Container.GetContainerItemCharges(containerIndex, slotIndex)`
Per-slot equivalent of
[`C_Item.GetItemCount`](#c_itemgetitemcountitem-includebank-includecharges)'s
`includeCharges=true` mode — returns the total uses available in
*this single slot*, where `GetItemCount` totals the same value
across every matching slot.
```
uses = C_Container.GetContainerItemCharges(containerIndex, slotIndex)
```
`containerIndex` accepts the same values as
[`C_Container.GetContainerItemDurability`](#c_containergetcontaineritemdurabilitycontainerindex-slotindex)
(`0` = backpack, `1..4` = equipped bags, `-1`/`5..11` = bank, etc.).
`slotIndex` is 1-based.
Math is `stack * usesPerItem`, where `usesPerItem` is
`abs(SPELL_CHARGES[0])` for items with a negative charges field
(consume-on-use: wands, healthstones, mana gems, sapper charges)
and `1` for everything else. Worked examples:
| Slot contents | Stack | Raw charges | Returns |
|---|---:|---:|---:|
| Wand of Decay at 50 charges | 1 | -50 | **50** |
| Wand at 1 charge | 1 | -1 | **1** |
| Healthstone | 1 | -1 | **1** |
| Stack of 20 water | 20 | -1 | **20** |
| Stack of 5 mana potions | 5 | -1 | **5** |
| Hearthstone (cooldown-only) | 1 | 0 | **1** |
| Any other item in the slot | 1 | 0 | **1** |
Returns `nil` only when the slot is empty.
```lua
-- "how many charges does my wand have left?"
local charges = C_Container.GetContainerItemCharges(0, 1)
print("Wand charges remaining: " .. charges)
-- "how many drinks across the bags?" — same as GetItemCount(water, false, true)
local total = 0
for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) do
if C_Container.GetContainerItemID(bag, slot) == waterID then
total = total + C_Container.GetContainerItemCharges(bag, slot)
end
end
end
```
ClassicAPI addition; modern WoW has no direct equivalent (modern
addons read charges off tooltip text). Useful when you need a
per-slot number rather than `GetItemCount`'s rollup — e.g. to
display "X charges" on the slot UI for a wand without re-walking
every other matching item.
### `C_Container.GetContainerNumFreeSlots(bagID)`
Returns the number of empty slots in the given bag, plus the bag's
@@ -1649,6 +1712,33 @@ show path re-allocates from the pool. Those transient zeroes never
become events because the unit appears in both the previous and
current tick's snapshot.
### `PLAYER_FOCUS_CHANGED` event
Fires whenever the player's focus changes — assignment via
[`FocusUnit`](#focusunitunit), clear via [`ClearFocus`](#clearfocus),
or a `FocusUnit(token)` call where the token resolves to no GUID
(implicit clear). No payload args; the new focus GUID is read off
[`UnitGUID("focus")`](#unitguidunit) in the handler.
```lua
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_FOCUS_CHANGED")
f:SetScript("OnEvent", function()
local guid = UnitGUID("focus")
if guid then
print("focusing", UnitName("focus"))
else
print("focus cleared")
end
end)
```
**Identity-checked**: assigning the same GUID twice is a no-op
(no event refires), matching 3.3.5's `FUN_0051FF20` behavior.
Mirrors modern's documented semantics — "fired whenever the
player's focus target is changed, including when the focus target
is lost or cleared".
### `QUEST_ACCEPTED` event
Fires once per quest the player just accepted, with two payload args:
@@ -2065,6 +2155,82 @@ This is a ClassicAPI-only call; modern WoW has no equivalent (modern
addons get the factionID from `FACTION_STANDING_CHANGED` directly, so
they don't need a separate getter).
## Focus
Polyfills modern WoW's focus-target system: a single sticky GUID
that addons can pin to a unit and address via the `"focus"` unit
token across the entire `UnitX` API surface. Backed by a hook on
`FUN_TOKEN_TO_GUID` (shared with the [`nameplateN`](#unit-tokens-nameplaten)
tokens — see `unit/TokenExtensions.cpp`).
State is **session-only** — drops on `/reload`, `/logout`, and zone
loads that recreate Lua state. Modern Classic Era behaves the same;
addons that want persistence have to re-`FocusUnit` from `SavedVariables`
at `ADDON_LOADED`.
**Auto-clear on despawn.** When the focused unit leaves the client's
object table — out of rendering range, full despawn, dies and
decays — focus drops and [`PLAYER_FOCUS_CHANGED`](#player_focus_changed-event)
fires. The unit re-entering range does NOT auto-refocus. Matches
modern WoW's documented behavior. Implementation: per-tick
`ObjectByGUID(g_focusGUID)` probe; on null result, `Set(0)`.
### `FocusUnit(unit)`
Sets focus to the given unit. Argument is a unit token —
`"target"`, `"mouseover"`, `"party1"`, `"nameplate1"`, anything the
resolver accepts. Calling with no argument is shorthand for
`FocusUnit("target")` (matches modern's `/focus` slash command
default).
```lua
FocusUnit("target") -- pin the current target
FocusUnit("nameplate1") -- pin the unit behind nameplate1
FocusUnit() -- same as FocusUnit("target")
```
Fires [`PLAYER_FOCUS_CHANGED`](#player_focus_changed-event) if the
resolved GUID differs from the current focus. No-op (no event) if
the same unit is already focused — matches the
identity-check-first behavior of 3.3.5's `FUN_0051FF20`.
Passing a token that doesn't resolve to a unit (e.g. `"target"`
with nothing targeted, or `"party5"` solo) clears focus — same as
calling `ClearFocus()`.
### `ClearFocus()`
Drops the focus. Fires `PLAYER_FOCUS_CHANGED` if there was one;
no-op otherwise.
```lua
ClearFocus()
```
### Unit token (`focus` / `focustarget`)
`"focus"` resolves to whatever GUID `FocusUnit` last stashed.
Accepted by every `UnitX` function:
```lua
local name = UnitName("focus") -- nil if no focus
local hp = UnitHealth("focus")
local _, class = UnitClass("focus")
```
Chains compose through the engine's standard suffix walker:
`"focustarget"`, `"focustargettarget"`, etc. — same behavior as
`"targettarget"`, mirrored instruction-for-instruction in our hook
so addons get the engine semantics they expect.
Returns `nil` cleanly when no focus is set; doesn't raise the
"Unknown unit name" error.
[`UnitTokenFromGUID`](#unittokenfromguidguid) scans `"focus"` right
after `"target"` (matching retail order), so a focused unit's GUID
reverse-resolves to `"focus"` only if it isn't already addressable
as `"player"` / `"party*"` / `"raid*"` / `"nameplate*"` / `"target"`.
## FriendList
### `C_FriendList.SendWhoQueryByName(name)`
@@ -6769,19 +6935,19 @@ known unit tokens and return the first one currently mapped to that
GUID, or `nil` if none of them point at it.
The search order matches modern retail with post-1.12 tokens
omitted (`vehicle`, `arenaN`, `arenapetN`, `bossN`, `focus`,
`softenemy`, `softfriend`, `softinteract` all post-date vanilla and
the engine's resolver doesn't recognize them). `nameplateN` is
included between `raidpetN` and `target` — we hook the resolver so
the engine recognizes the token form (see
[Unit tokens](#unit-tokens-nameplaten)). As with the other
positional tokens, the returned `"nameplate3"`-style result is only
valid at that instant; the slot may shift to a different unit
between calls, so re-verify with `UnitGUID(token)` before reusing.
omitted (`vehicle`, `arenaN`, `arenapetN`, `bossN`, `softenemy`,
`softfriend`, `softinteract` all post-date vanilla and the engine's
resolver doesn't recognize them). `nameplateN` and `focus` are
included — we hook the resolver so the engine recognizes both token
forms. As with the other positional tokens, a returned
`"nameplate3"` result is only valid at that instant; the slot may
shift to a different unit between calls, so re-verify with
`UnitGUID(token)` before reusing.
```
player → pet → party1..4 → partypet1..4 → raid1..40
→ raidpet1..40 → nameplate1..N → target → npc → mouseover
→ raidpet1..40 → nameplate1..N → target → focus
→ npc → mouseover
```
```lua
+55
View File
@@ -0,0 +1,55 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Container.GetContainerItemCharges(containerIndex, slotIndex)` —
// returns the `abs(SPELL_CHARGES[0])` value of the item in the
// specified bag slot, or `nil` for empty slots and items that don't
// carry a charge concept. Wands return their remaining cast count
// (e.g. 50 → 49 → …); single-use items (Healthstone, Mana Gem,
// Goblin Sapper Charge) return `1`.
//
// Modern equivalent: none — modern WoW exposes charges only through
// tooltip text scanning. `C_Item.GetItemCount(includeBank, includeCharges)`
// totals charges across every matching stack; this is the per-slot
// reader.
#include "Game.h"
#include "item/Charges.h"
#include "item/Location.h"
namespace Container::Charges {
namespace {
int __fastcall Script_C_Container_GetContainerItemCharges(void *L) {
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L,
"Usage: C_Container.GetContainerItemCharges(containerIndex, slotIndex)");
return 0;
}
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
return Item::Charges::PushChargesForItem(
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemCharges",
&Script_C_Container_GetContainerItemCharges);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Container::Charges
+39
View File
@@ -0,0 +1,39 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Charges.h"
#include "../Game.h"
#include "../Offsets.h"
#include <cstdlib>
namespace Item::Charges {
int PushChargesForItem(void *L, const uint8_t *item) {
if (item == nullptr)
return 0;
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
item + Offsets::OFF_ITEM_DESCRIPTOR);
if (descriptor == nullptr)
return 0;
const int32_t stack = static_cast<int32_t>(*reinterpret_cast<const uint32_t *>(
descriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT));
const int32_t rawCharges = *reinterpret_cast<const int32_t *>(
descriptor + Offsets::OFF_DESCRIPTOR_SPELL_CHARGES_0);
const int32_t usesPerItem = (rawCharges < 0) ? -rawCharges : 1;
Game::Lua::PushNumber(L, static_cast<double>(stack * usesPerItem));
return 1;
}
} // namespace Item::Charges
+44
View File
@@ -0,0 +1,44 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
namespace Item::Charges {
// Pushes the total uses available in this single slot — the
// per-slot equivalent of `GetItemCount(_, _, includeCharges=true)`.
// Returns `1` (pushed one value) on success, `0` (pushes nothing,
// caller surfaces as `nil`) when the item is missing or the
// descriptor is null.
//
// Math is `stack * usesPerItem` where `usesPerItem` follows
// `Item::Count::GetUsesPerItem`:
// - `SPELL_CHARGES[0] >= 0` → `1` (no destroy-on-zero behavior;
// stack count alone reflects "how many uses").
// - `SPELL_CHARGES[0] < 0` → `abs(value)` (consume-on-use items;
// wand at 50 has `-50` so each item contributes 50).
//
// Worked examples:
// - Wand at 50 charges (stack=1, raw=-50): `1 * 50 = 50`
// - Hearthstone (stack=1, raw=0): `1 * 1 = 1` (cooldown-
// only, no charges concept; we still surface "1 use").
// - Water stack of 20 (stack=20, raw=-1): `20 * 1 = 20`
// - Equipped sword (stack=1, raw=0): `1 * 1 = 1`
//
// Caller can `return PushChargesForItem(L, item);` to forward the
// count directly.
int PushChargesForItem(void *L, const uint8_t *item);
} // namespace Item::Charges
+3 -3
View File
@@ -32,7 +32,7 @@
// - `NAME_PLATE_UNIT_ADDED` / `_REMOVED` — `arg1` is the
// `"nameplateN"` unit token (formatted from the plate's index in
// `g_orderedGUIDs` at fire time). The token resolves to the unit
// via the `nameplateN`-aware token resolver in `TokenResolver.cpp`
// via the `nameplateN`-aware token resolver in `unit/TokenExtensions.cpp`
// — addons can pass it straight to `UnitName`, `UnitGUID`, etc.,
// or to `GetNamePlateForUnit` for the frame.
@@ -86,7 +86,7 @@ std::unordered_set<const void *> g_seenPlates;
// Ordered list of currently-visible nameplate GUIDs, in
// creation-order. Append on UNIT_ADDED, erase on UNIT_REMOVED. Backs
// the `nameplateN` unit-token resolver in `TokenResolver.cpp`. Order
// the `nameplateN` unit-token resolver in `unit/TokenExtensions.cpp`. Order
// matches modern WoW semantics: stable for the lifetime of each
// plate, gaps when middle plates vanish (until the next REMOVED
// shifts later entries down).
@@ -239,7 +239,7 @@ void PrepareForReload() {
}
// Exposed via `nameplate/Walk.h` so the `nameplateN` token resolver
// in `TokenResolver.cpp` can map an index to a GUID without seeing
// in `unit/TokenExtensions.cpp` can map an index to a GUID without seeing
// the internal vector.
uint64_t GetGUIDByIndex(int oneBased) {
if (oneBased <= 0)
+127
View File
@@ -0,0 +1,127 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `FocusUnit(unit)` / `ClearFocus()` / `focus` unit token. Polyfills
// the post-vanilla focus-target system on top of the engine's
// existing token resolver via the hook in
// `unit/TokenExtensions.cpp`.
//
// Storage: a single 64-bit GUID held in-process. The 3.3.5 engine
// keeps the same shape (two globals at `DAT_00BD07D0`/`D4`); we
// don't share those because vanilla never reads them. The resolver
// hook reads our `Unit::Focus::Get()` directly.
//
// Setter mirrors 3.3.5's `FUN_0051FF20` minus the post-vanilla
// `UnitFlag bit 0x2000` ("focus glow" rendering hint, introduced in
// TBC for the default focus frame). Vanilla addons (pfUI) render
// their own focus indicator and don't need the hint.
//
// Auto-clear on despawn: modern WoW fires `PLAYER_FOCUS_CHANGED`
// when the focused unit leaves the client's object table (out of
// rendering range, despawn) and does NOT auto-refocus when they
// come back. We mirror that by probing `ObjectByGUID(g_focusGUID)`
// every world tick — when it returns null, we `Set(0)` which fires
// the event. Cost is one hash-table lookup per tick while focus is
// set; zero when no focus is active.
#include "Focus.h"
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "tick/WorldTick.h"
#include <cstdint>
namespace Unit::Focus {
namespace {
constexpr const char *kEventName = "PLAYER_FOCUS_CHANGED";
const Event::Custom::AutoReserve _reserve{kEventName};
uint64_t g_focusGUID = 0;
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName,
uint32_t guidLo, uint32_t guidHi,
int priority);
uint64_t ResolveTokenGUID(const char *token) {
if (token == nullptr || *token == '\0')
return 0;
auto fn = reinterpret_cast<TokenToGUID_t>(
static_cast<uintptr_t>(Offsets::FUN_TOKEN_TO_GUID));
return fn(token);
}
// Per-tick despawn watcher. Probes the engine's object table for
// `g_focusGUID`; when it disappears (out of range, fully despawned),
// clear focus and fire PLAYER_FOCUS_CHANGED — matches modern's
// "leaves render distance → focus drops" behavior. Won't refocus
// when the unit comes back, also matching modern.
void OnWorldTick() {
if (g_focusGUID == 0)
return;
auto resolve = reinterpret_cast<ResolveByGUID_t>(
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
if (resolve(Offsets::OBJ_TYPE_UNIT, "Focus",
static_cast<uint32_t>(g_focusGUID),
static_cast<uint32_t>(g_focusGUID >> 32),
0x172) == nullptr)
Set(0);
}
// `FocusUnit(unit)` — sets focus to whatever GUID `unit` currently
// resolves to. Modern signature: `FocusUnit(unit)` with the token
// argument. Modern also allows `FocusUnit()` (no arg) to mean
// `"target"`, matching the `/focus` slash command's default.
int __fastcall Script_FocusUnit(void *L) {
if (Game::Lua::IsString(L, 1)) {
const char *token = Game::Lua::ToString(L, 1);
Set(ResolveTokenGUID(token));
} else {
Set(ResolveTokenGUID("target"));
}
return 0;
}
// `ClearFocus()` — drops the focus, fires PLAYER_FOCUS_CHANGED.
int __fastcall Script_ClearFocus(void *L) {
(void)L;
Set(0);
return 0;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("FocusUnit", &Script_FocusUnit);
Game::Lua::RegisterGlobalFunction("ClearFocus", &Script_ClearFocus);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
} // namespace
uint64_t Get() { return g_focusGUID; }
void Set(uint64_t guid) {
if (guid == g_focusGUID)
return; // no-op: same target → no event fire (matches 3.3.5)
g_focusGUID = guid;
const int slot = Event::Custom::Lookup(kEventName);
if (slot >= 0)
Event::Custom::Fire(slot, "");
}
} // namespace Unit::Focus
+31
View File
@@ -0,0 +1,31 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
namespace Unit::Focus {
// Current focus GUID, or `0` when nothing is focused. Read by the
// `focus` / `focustarget*` branch of the token resolver in
// `unit/TokenExtensions.cpp`.
uint64_t Get();
// Sets focus to `guid` and fires `PLAYER_FOCUS_CHANGED` (no args).
// Identity-checks first — assigning the same GUID is a no-op,
// matching 3.3.5's `FUN_0051FF20` behavior so the event only fires
// on a real transition. Pass `0` to clear focus.
void Set(uint64_t guid);
} // namespace Unit::Focus
+9 -6
View File
@@ -107,14 +107,16 @@ static int __fastcall Script_UnitGUID(void *L) {
//
// The search walks the modern retail token order with post-vanilla
// tokens dropped (no `vehicle`, `arenaN`, `arenapetN`, `bossN`,
// `focus`, `softenemy`, `softfriend`, `softinteract` — those all
// post-date 1.12 and `FUN_TOKEN_TO_GUID` would raise "Unknown unit
// name" for them). `nameplateN` is included — we hook the token
// resolver so the engine recognizes it (see `nameplate/TokenResolver.cpp`).
// Order:
// `softenemy`, `softfriend`, `softinteract` — those all post-date
// 1.12 and `FUN_TOKEN_TO_GUID` would raise "Unknown unit name" for
// them). `nameplateN` and `focus` are included — we hook the token
// resolver so the engine recognizes them (see
// `unit/TokenExtensions.cpp` and `unit/Focus.cpp`). Order matches
// retail with the dropped tokens skipped in place:
//
// player → pet → party1..4 → partypet1..4 → raid1..40
// → raidpet1..40 → nameplate1..N → target → npc → mouseover
// → raidpet1..40 → nameplate1..N → target → focus
// → npc → mouseover
//
// The result is inherently unstable — multiple tokens can map to the
// same GUID at once (your target IS your mouseover IS your party1), and
@@ -203,6 +205,7 @@ static int __fastcall Script_UnitTokenFromGUID(void *L) {
}
if (check("target")) return 1;
if (check("focus")) return 1;
if (check("npc")) return 1;
if (check("mouseover")) return 1;
@@ -11,45 +11,41 @@
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Extend `FUN_TOKEN_TO_GUID` (`0x00515970`) with modern WoW's
// `nameplateN` unit-token family. Hooking the central token→GUID
// resolver means every `Script_Unit*` (UnitName, UnitGUID, UnitHealth,
// UnitClass, UnitExists, …) gains nameplate-token support for free,
// Extend `FUN_TOKEN_TO_GUID` (`0x00515970`) with post-vanilla unit
// tokens. Hooking the central token→GUID resolver means every
// `Script_Unit*` (UnitName, UnitGUID, UnitHealth, UnitClass,
// UnitExists, …) gains support for the new token families for free,
// since they all funnel here.
//
// Resolution:
// Currently extended families:
//
// 1. SStrCmpI the input against `"nameplate"` (9 chars). Non-match →
// fall through to the original resolver and behave exactly as
// before (engine handles `player` / `target` / `partyN` / etc.,
// or raises "Unknown unit name").
// 2. Parse trailing digits — `nameplate1`, `nameplate12`, ... — into
// a 1-based index `N`.
// 3. Look up `N` in `NamePlate::Events`' ordered GUID list. Out of
// range returns 0 quietly, which the engine treats as "no unit"
// and Lua callers see as `nil` — matches modern behavior, no
// "Unknown unit name" error.
// 4. If the token continues past the digits (`nameplate1target`,
// `nameplate1targettarget`, …) mirror the engine's own suffix
// walker at `LAB_005159d3`: each `"target"` chunk advances the
// GUID by reading `m_objectFields + OFF_UNIT_FIELD_TARGET`
// (UNIT_FIELD_TARGET) via `ObjectByGUID`.
// - `nameplate1`..`nameplateN` — index into the ordered list of
// visible nameplates maintained in `nameplate/Events.cpp`. See
// `Unit tokens (nameplateN)` in docs/API.md.
//
// The engine's walker is **inside** the function we hook, so a hook
// that returns early at the prefix-parse step would skip it
// entirely. Replicating the walker here keeps `nameplate1target`
// working without rewriting the resolver. The walker's logic is
// short (one `SStrCmpI` + one `ObjectByGUID` + one field-read per
// iteration) and matches the engine instruction-for-instruction —
// verified against the disassembly of `0x005159D3..0x00515A2C`.
// - `focus` — single-slot sticky target backed by `Unit::Focus`.
// See `unit/Focus.cpp` for the storage + `PLAYER_FOCUS_CHANGED`
// event firing.
//
// Both families compose with the engine's own `target`-suffix walker
// (`focustarget`, `nameplate1targettarget`) via the shared
// `WalkSuffix` helper, which mirrors the engine's
// `LAB_005159d3..0x00515A2C` walker instruction-for-instruction
// (SStrCmpI "target" + ObjectByGUID + read UNIT_FIELD_TARGET).
//
// Resolution order in the hook: focus check first (literal-equal,
// short token), then nameplate prefix (longer, requires digit).
// Both branches short-circuit; on no-match the original engine
// resolver runs unchanged.
#include "Game.h"
#include "Offsets.h"
#include "nameplate/Walk.h"
#include "unit/Focus.h"
#include <cstdint>
namespace NamePlate::TokenResolver {
namespace Unit::TokenExtensions {
namespace {
@@ -64,8 +60,10 @@ using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName,
uint32_t guidLo, uint32_t guidHi,
int priority);
constexpr const char kPrefix[] = "nameplate";
constexpr int kPrefixLen = static_cast<int>(sizeof(kPrefix) - 1);
constexpr const char kNamePlatePrefix[] = "nameplate";
constexpr int kNamePlatePrefixLen = static_cast<int>(sizeof(kNamePlatePrefix) - 1);
constexpr const char kFocusPrefix[] = "focus";
constexpr int kFocusPrefixLen = static_cast<int>(sizeof(kFocusPrefix) - 1);
constexpr const char kSuffixTarget[] = "target";
constexpr int kSuffixTargetLen = static_cast<int>(sizeof(kSuffixTarget) - 1);
@@ -87,7 +85,7 @@ uint64_t WalkSuffix(uint64_t guid, const char *suffix) {
return 0; // unknown suffix component — modern returns nil
suffix += kSuffixTargetLen;
auto *obj = static_cast<uint8_t *>(
resolve(Offsets::OBJ_TYPE_UNIT, "nameplate",
resolve(Offsets::OBJ_TYPE_UNIT, "ClassicAPI",
static_cast<uint32_t>(guid),
static_cast<uint32_t>(guid >> 32),
kResolvePriority));
@@ -113,27 +111,36 @@ uint64_t __fastcall Hook_h(const char *token) {
return Original_o(token);
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
if (sstrcmpi(token, kPrefix, kPrefixLen) != 0)
return Original_o(token);
// Must have at least one digit after `"nameplate"`; otherwise
// it's `"nameplate"` (no number) or `"nameplateX"` — fall
// through to the original so the engine raises its
// "Unknown unit name" error consistently.
const char *p = token + kPrefixLen;
if (*p < '0' || *p > '9')
return Original_o(token);
int index = 0;
while (*p >= '0' && *p <= '9') {
index = index * 10 + (*p - '0');
++p;
// `focus` / `focustarget*`. The 5-char prefix is unique among
// unit tokens (no `focuscast` / `focused*` etc. live in vanilla
// or modern resolvers), so any match continues as our token.
if (sstrcmpi(token, kFocusPrefix, kFocusPrefixLen) == 0) {
const uint64_t guid = Unit::Focus::Get();
if (guid == 0)
return 0;
return WalkSuffix(guid, token + kFocusPrefixLen);
}
const uint64_t guid = NamePlate::Events::GetGUIDByIndex(index);
if (guid == 0)
return 0; // OOB / no plate at index — modern returns nil
return WalkSuffix(guid, p);
// `nameplateN` / `nameplate1target*`. Requires a digit after the
// prefix; anything else (`nameplate`, `nameplateX`) falls through
// to the engine for its standard "Unknown unit name" error.
if (sstrcmpi(token, kNamePlatePrefix, kNamePlatePrefixLen) == 0) {
const char *p = token + kNamePlatePrefixLen;
if (*p >= '0' && *p <= '9') {
int index = 0;
while (*p >= '0' && *p <= '9') {
index = index * 10 + (*p - '0');
++p;
}
const uint64_t guid = NamePlate::Events::GetGUIDByIndex(index);
if (guid == 0)
return 0;
return WalkSuffix(guid, p);
}
}
return Original_o(token);
}
} // namespace
@@ -143,4 +150,4 @@ static const Game::HookAutoRegister _hook{
reinterpret_cast<void *>(&Hook_h),
reinterpret_cast<void **>(&Original_o)};
} // namespace NamePlate::TokenResolver
} // namespace Unit::TokenExtensions