mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-13 22:50:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee2b4d22ac | ||
|
|
59a603f9b2 | ||
|
|
0332e49ac2 | ||
|
|
b1b564a533 | ||
|
|
444875ae38 |
@@ -1,3 +1,4 @@
|
||||
AOELOOT = "AoE Loot"
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
|
||||
@@ -45,7 +45,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit` |
|
||||
| [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_CreatureInfo.GetCreatureID`, `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` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `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` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellName`, `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`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `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` |
|
||||
@@ -54,7 +54,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [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` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` (backports `GlobalColor.dbc`; companion addon wraps each row with `ColorMixin` and assigns `_G[baseTag]` + `_G[baseTag.."_CODE"]`) |
|
||||
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitGUID`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitTokenFromGUID` |
|
||||
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitGUID`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `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` |
|
||||
|
||||
### GlueXML calls
|
||||
|
||||
+58
@@ -249,6 +249,7 @@ build instructions.
|
||||
|
||||
- [NameCache](#namecache)
|
||||
- [`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_CreatureInfo.GetCreatureID(guid)`](#c_creatureinfogetcreatureidguid)
|
||||
@@ -346,6 +347,7 @@ build instructions.
|
||||
- [Unit](#unit)
|
||||
- [`UnitGUID(unit)`](#unitguidunit)
|
||||
- [`UnitTokenFromGUID(guid)`](#unittokenfromguidguid)
|
||||
- [`UnitSubName(unit)`](#unitsubnameunit)
|
||||
- [`GetUnitSpeed(unit)`](#getunitspeedunit)
|
||||
- [`UnitClassBase(unit)`](#unitclassbaseunit)
|
||||
- [`UnitRaceBase(unit)`](#unitracebaseunit)
|
||||
@@ -6003,6 +6005,35 @@ instance lives at `0x00C0E228`; entry layout (name, realm, race,
|
||||
sex, class) was reverse-engineered from the
|
||||
`SMSG_NAME_QUERY_RESPONSE` write path at `0x0055F310`.
|
||||
|
||||
### `UnitNameFromGUID(guid)`
|
||||
|
||||
Returns `name, realm` for the player identified by `guid`, or `nil`
|
||||
if no player with that GUID has been encountered yet. Same lookup
|
||||
chain as [`GetPlayerInfoByGUID`](#getplayerinfobyguidguid) (engine
|
||||
NameCache, persistent on-disk fallback when enabled) — just narrower
|
||||
return shape for callers that only need the name.
|
||||
|
||||
```lua
|
||||
local name, realm = UnitNameFromGUID(UnitGUID("target"))
|
||||
if name then
|
||||
ChatFrame1:AddMessage("Target: " .. name)
|
||||
end
|
||||
|
||||
-- Resolving a chat link
|
||||
local name, realm = UnitNameFromGUID("0x0000000000000777")
|
||||
```
|
||||
|
||||
`realm` is always `""` in vanilla — the engine doesn't populate
|
||||
per-player realm names and 1.12 has no cross-realm interaction.
|
||||
We push the empty string rather than `nil` to match the convention
|
||||
of the other player-info accessors; addons can gate on `realm == ""`.
|
||||
|
||||
Doesn't trigger a network query on miss. To populate an unknown
|
||||
GUID, either let the engine receive a `SMSG_NAME_QUERY_RESPONSE`
|
||||
through normal chat/target interaction, or call
|
||||
[`C_PlayerCache.RememberPlayer`](#c_playercacherememberplayerguid-name-classtoken)
|
||||
explicitly.
|
||||
|
||||
### `C_PlayerCache.GetPlayerInfoByName(name)`
|
||||
|
||||
Returns
|
||||
@@ -8251,6 +8282,33 @@ end
|
||||
> ex-targets, ex-mouseover units, distant players seen in the chat
|
||||
> log, etc. The engine simply doesn't address those by token.
|
||||
|
||||
### `UnitSubName(unit)`
|
||||
|
||||
Returns the NPC's subtitle / title string (the small italic text
|
||||
under their name in the tooltip): `"Innkeeper"`, `"Stable Master"`,
|
||||
`"<Master Engineer>"`, etc. Returns `nil` for players, unsynced
|
||||
NPCs, and NPCs with no subtitle.
|
||||
|
||||
```lua
|
||||
/dump UnitSubName("target")
|
||||
-- "Innkeeper" (Stormwind innkeep)
|
||||
-- "<Auction House>" (auctioneer)
|
||||
-- nil (a player, or a wolf in the woods)
|
||||
```
|
||||
|
||||
Modern WoW returns the subtitle as `UnitName`'s second return;
|
||||
vanilla 1.12's `UnitName` returns only one value, leaving no
|
||||
direct route to the subtitle. Addons that wanted it had to scrape
|
||||
`GameTooltip` text, which is fragile and required the unit to be
|
||||
hovered. `UnitSubName` reads it straight off the engine's creature
|
||||
cache (`[unit + 0xB30] → [+0x10]`), no tooltip round-trip.
|
||||
|
||||
Coverage caveat: the creature cache row is populated when an NPC
|
||||
becomes visible to the client AND the server has sent its
|
||||
`CMSG_CREATURE_QUERY_RESPONSE`. The first time a fresh NPC appears
|
||||
the row may be briefly NULL — usually only one or two frames.
|
||||
Subsequent calls succeed once the response lands.
|
||||
|
||||
### `GetUnitSpeed(unit)`
|
||||
|
||||
Returns `currentSpeed, runSpeed, flightSpeed, swimSpeed` — all four
|
||||
|
||||
@@ -405,6 +405,23 @@ enum Offsets {
|
||||
// descriptor at +0x114 — these are sibling classes under CGObject
|
||||
// with class-specific descriptor offsets.
|
||||
OFF_UNIT_DESCRIPTOR = 0x110,
|
||||
|
||||
// CGCreature client-side creature-data cache row. Populated by the
|
||||
// engine when an NPC GUID becomes visible (the same row the
|
||||
// `CreatureCache` SMSG fills with the templated NPC's name,
|
||||
// subname, etc.). NULL for unsynced units or for player CGUnits
|
||||
// (players don't have a row in this cache — their name comes from
|
||||
// the SMSG_NAME_QUERY_RESPONSE-fed name cache at `0x00C0E228`).
|
||||
//
|
||||
// Layout of the cache row:
|
||||
// +0x00 ?
|
||||
// +0x0C name C-string ptr (alias of UNIT_NAME for creatures)
|
||||
// +0x10 subName C-string ptr ("Innkeeper", "<Master Trainer>", etc.)
|
||||
//
|
||||
// Used by `Script_UnitName`'s creature path and by VanillaMinimapTracking's
|
||||
// subname-based blip filter (`src/Blips.cpp::ExtractUnitSubName`).
|
||||
OFF_UNIT_CREATURE_CACHE_ROW = 0xB30,
|
||||
OFF_CREATURE_CACHE_SUB_NAME = 0x10,
|
||||
// Pointer to the CGUnit's 8-byte GUID at `*(CGUnit + 0x08)`. Verified
|
||||
// in `Script_GetInventoryItemLink` at `0x004C8CB0`-`0x004C8CB5`:
|
||||
// `mov eax, [esi+8]; mov edi, [eax]; mov ecx, [eax+4]` reads the
|
||||
|
||||
@@ -12,4 +12,9 @@
|
||||
<Binding name="TARGETFOCUS">
|
||||
TargetUnit("focus");
|
||||
</Binding>
|
||||
<Binding name="AOELOOT">
|
||||
if not C_Loot.IsScanInProgress() then
|
||||
C_Loot.ScanNearbyLoot()
|
||||
end
|
||||
</Binding>
|
||||
</Bindings>
|
||||
|
||||
+163
-56
@@ -147,41 +147,54 @@ static int __fastcall Script_IsItemDataCached(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Pending explicit-request tracking. We deliberately do NOT eagerly
|
||||
// create cache entries when an addon calls `C_Item.RequestLoadItemData`
|
||||
// at addon-load / PLAYER_ENTERING_WORLD time — empirically (pfQuest),
|
||||
// pre-creating the entry blocks the engine's natural prefetch path
|
||||
// from issuing a fresh `SMSG_ITEM_QUERY_SINGLE`. The entry stays in a
|
||||
// permanently-pending state because nothing ever sends the query.
|
||||
// Pending-request tracking. We deliberately do NOT eagerly create cache
|
||||
// entries during the startup window — empirically (pfQuest), pre-creating
|
||||
// the entry at addon-load / PLAYER_ENTERING_WORLD time, before the
|
||||
// engine's network/opcode dispatch has settled, blocks the engine's
|
||||
// natural prefetch from issuing a fresh `SMSG_ITEM_QUERY_SINGLE`. The
|
||||
// entry stays permanently pending because nothing ever sends the query.
|
||||
// This applies to BOTH paths into the cache:
|
||||
// - explicit `C_Item.RequestLoadItemData(ByID)`
|
||||
// - implicit `WarmCache` (the `GetItemInfo` hook + every `GetItem*`
|
||||
// getter's cache-miss warmup) — see `WarmCache`.
|
||||
//
|
||||
// Instead we hook the engine's item-cache response handler at
|
||||
// `FUN_ITEMSTATS_CACHE_RESPONSE` (the function that fills cache
|
||||
// entries on `SMSG_ITEM_QUERY_SINGLE_RESPONSE`). After the engine
|
||||
// finishes filling any entries and walking its own callback lists,
|
||||
// we sweep our pending set and fire `ITEM_DATA_LOAD_RESULT(itemID, 1)`
|
||||
// for any tracked items that now resolve. The engine's natural
|
||||
// prefetch (for player-inventory items) takes care of the actual
|
||||
// `SMSG_ITEM_QUERY_SINGLE` for us.
|
||||
// Both route through `Track`, which records the item without touching
|
||||
// the cache while `g_settled` is false. We hook the engine's item-cache
|
||||
// response handler at `FUN_ITEMSTATS_CACHE_RESPONSE` (the function that
|
||||
// fills cache entries on `SMSG_ITEM_QUERY_SINGLE_RESPONSE`); after the
|
||||
// engine finishes filling entries and walking its own callback lists, we
|
||||
// sweep our pending set and fire the per-item completion event for any
|
||||
// tracked item that now resolves. The engine's natural prefetch (for
|
||||
// player-inventory items) takes care of the actual query for us.
|
||||
//
|
||||
// For items the engine *doesn't* naturally prefetch (e.g. quest
|
||||
// reward IDs the player has never owned), we escalate after
|
||||
// `kEscalateTicks` by issuing our own `CacheFetch` with a no-op
|
||||
// For items the engine *doesn't* naturally prefetch (e.g. quest reward
|
||||
// IDs the player has never owned), `OnWorldTick` escalates once the
|
||||
// startup gate has settled by issuing our own `CacheFetch` with a no-op
|
||||
// callback — that creates the entry and queues the query, and the
|
||||
// response handler hook still fires our event when the data lands.
|
||||
// `kMaxWaitTicks` is the hard timeout that gives up and fires
|
||||
// `ITEM_DATA_LOAD_RESULT(itemID, nil)`.
|
||||
// `kMaxWaitTicks` is the hard timeout that gives up and fires the
|
||||
// completion event with failure.
|
||||
struct Pending {
|
||||
uint32_t itemID;
|
||||
int ticksWaiting;
|
||||
bool requestIssued;
|
||||
bool implicit; // true: fire GET_ITEM_INFO_RECEIVED; false: ITEM_DATA_LOAD_RESULT
|
||||
bool failed; // set by the escalation callback on a server "no such item" reply
|
||||
};
|
||||
|
||||
constexpr int kMaxPending = 64;
|
||||
constexpr int kEscalateTicks = 60;
|
||||
constexpr int kMaxPending = 512;
|
||||
constexpr int kMaxWaitTicks = 1200;
|
||||
// World ticks after which we assume the engine's startup
|
||||
// network/opcode-dispatch state has settled and eager queries are safe.
|
||||
// Also latched the moment the engine's own response handler fires (a
|
||||
// response proves dispatch is live) — see `ItemStatsCacheResponse_h`.
|
||||
// One-way latch; see the relog note in `WarmCache`.
|
||||
constexpr int kSettleTicks = 60;
|
||||
|
||||
static Pending g_pending[kMaxPending];
|
||||
static int g_pendingCount = 0;
|
||||
static bool g_settled = false;
|
||||
static int g_worldTicks = 0;
|
||||
|
||||
static int FindPending(uint32_t itemID) {
|
||||
for (int i = 0; i < g_pendingCount; ++i) {
|
||||
@@ -197,27 +210,92 @@ static void RemovePendingAt(int idx) {
|
||||
g_pending[idx] = g_pending[g_pendingCount];
|
||||
}
|
||||
|
||||
// No-op callback for the escalation path — we just need a non-null
|
||||
// callback pointer so the engine's `_GetRecord` queues the query.
|
||||
// The response handler hook will detect the fill and fire the event;
|
||||
// this callback doesn't need to do anything.
|
||||
static void __stdcall ItemLoadCallback_NoOp(void * /*userData*/, int /*success*/) {}
|
||||
// Callback for the escalation path. The engine queues the query when we
|
||||
// pass a non-null callback to `_GetRecord`; it then invokes this with the
|
||||
// itemID smuggled through `userData`. On a successful fill the engine has
|
||||
// already set the entry's loaded flag, so the response-handler sweep
|
||||
// reports success — we do nothing here. On a server failure (high-bit
|
||||
// itemID in `SMSG_ITEM_QUERY_SINGLE_RESPONSE`) the engine does NOT set the
|
||||
// loaded flag (verified in `FUN_0055BDB0`), so the sweep can't detect it;
|
||||
// we mark the pending entry failed so the sweep / tick fires failure
|
||||
// immediately instead of waiting out `kMaxWaitTicks`.
|
||||
static void __stdcall ItemLoadCallback_Escalated(void *userData, int success) {
|
||||
if (success)
|
||||
return;
|
||||
const auto itemID = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(userData));
|
||||
const int idx = FindPending(itemID);
|
||||
if (idx >= 0)
|
||||
g_pending[idx].failed = true;
|
||||
}
|
||||
|
||||
static void RequestAndMaybeNotify(uint32_t itemID) {
|
||||
// Already loaded — fire success synchronously.
|
||||
static void FireByTag(uint32_t itemID, bool success, bool implicit) {
|
||||
if (implicit)
|
||||
FireGetItemInfoReceived(static_cast<int>(itemID), success);
|
||||
else
|
||||
FireItemDataLoadResult(static_cast<int>(itemID), success);
|
||||
}
|
||||
|
||||
// Records an item we owe a completion event for, WITHOUT eagerly
|
||||
// creating the cache entry. During the startup window (before
|
||||
// `g_settled`) this is the only safe way to "request" an item: calling
|
||||
// `CacheFetch` with a callback now would create an orphan entry the
|
||||
// engine's prefetch then skips, leaving it permanently pending (see the
|
||||
// block comment above). `OnWorldTick` issues the actual query once
|
||||
// settled, and the response-handler sweep fires the event — tagged
|
||||
// implicit (GET_ITEM_INFO_RECEIVED) or explicit (ITEM_DATA_LOAD_RESULT).
|
||||
// Returns false only when the request was dropped because the pending
|
||||
// set is full (the caller is still tracking nothing). All other outcomes
|
||||
// — already-loaded, already-tracking, newly-tracked — return true.
|
||||
static bool Track(uint32_t itemID, bool implicit) {
|
||||
if (CacheFetch(itemID, nullptr) != nullptr) {
|
||||
FireItemDataLoadResult(static_cast<int>(itemID), true);
|
||||
return;
|
||||
// Already loaded. Explicit RequestLoadItemData fires its
|
||||
// completion event synchronously (modern API contract). Implicit
|
||||
// warmup mirrors the engine's already-loaded shortcut, which does
|
||||
// not re-invoke the callback — so a cache hit fires nothing.
|
||||
if (!implicit)
|
||||
FireItemDataLoadResult(static_cast<int>(itemID), true);
|
||||
return true;
|
||||
}
|
||||
const int idx = FindPending(itemID);
|
||||
if (idx >= 0) {
|
||||
// Already tracking. An explicit request supersedes a pending
|
||||
// implicit one so the explicit caller's ITEM_DATA_LOAD_RESULT is
|
||||
// honored (the two events are mutually exclusive per fill).
|
||||
if (!implicit)
|
||||
g_pending[idx].implicit = false;
|
||||
return true;
|
||||
}
|
||||
// Already tracking — second request just dedups.
|
||||
if (FindPending(itemID) >= 0)
|
||||
return;
|
||||
if (g_pendingCount >= kMaxPending)
|
||||
return;
|
||||
return false;
|
||||
g_pending[g_pendingCount].itemID = itemID;
|
||||
g_pending[g_pendingCount].ticksWaiting = 0;
|
||||
g_pending[g_pendingCount].requestIssued = false;
|
||||
g_pending[g_pendingCount].implicit = implicit;
|
||||
g_pending[g_pendingCount].failed = false;
|
||||
++g_pendingCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool RequestAndMaybeNotify(uint32_t itemID) { return Track(itemID, /*implicit=*/false); }
|
||||
|
||||
// Fires + removes a pending entry if it has resolved either way: loaded
|
||||
// (engine set the flag) → success, or `failed` (escalation callback saw a
|
||||
// server rejection) → failure. Returns true if it fired and removed the
|
||||
// entry at `i`, so callers must NOT advance their index when it does.
|
||||
static bool TryCompletePending(int i) {
|
||||
const uint32_t itemID = g_pending[i].itemID;
|
||||
const bool implicit = g_pending[i].implicit;
|
||||
if (CacheFetch(itemID, nullptr) != nullptr) {
|
||||
RemovePendingAt(i);
|
||||
FireByTag(itemID, true, implicit);
|
||||
return true;
|
||||
}
|
||||
if (g_pending[i].failed) {
|
||||
RemovePendingAt(i);
|
||||
FireByTag(itemID, false, implicit);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Response handler hook. After the engine fills cache entries from
|
||||
@@ -237,16 +315,17 @@ static void __fastcall ItemStatsCacheResponse_h(void *cache, void * /*edx_unused
|
||||
void *packetReader, int flag) {
|
||||
using Trampoline_t = void(__fastcall *)(void *cache, void *edx, void *packetReader, int flag);
|
||||
reinterpret_cast<Trampoline_t>(ItemStatsCacheResponse_o)(cache, nullptr, packetReader, flag);
|
||||
// The engine just processed an item-query response, which proves its
|
||||
// network/opcode dispatch is live — eager queries are safe from here
|
||||
// on. Latch the startup gate so `WarmCache` stops deferring.
|
||||
g_settled = true;
|
||||
// Sweep pending — the engine just finished filling any matching
|
||||
// entries. Peek (no callback) returns non-null iff the entry's
|
||||
// loaded flag is set.
|
||||
// entries (and, on the failure branch, invoked our escalation
|
||||
// callback which marked any rejected items `failed`). Fire whatever
|
||||
// resolved this response, success or failure.
|
||||
for (int i = 0; i < g_pendingCount;) {
|
||||
const uint32_t itemID = g_pending[i].itemID;
|
||||
if (CacheFetch(itemID, nullptr) != nullptr) {
|
||||
RemovePendingAt(i);
|
||||
FireItemDataLoadResult(static_cast<int>(itemID), true);
|
||||
if (TryCompletePending(i))
|
||||
continue;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
@@ -258,23 +337,34 @@ static const Game::HookAutoRegister _hookCacheResponse{
|
||||
|
||||
// Escalation + timeout. The hook covers the happy path (engine fills
|
||||
// cache → we fire). This tick is only responsible for:
|
||||
// 1. Issuing a query for items the engine doesn't naturally prefetch
|
||||
// (e.g. quest rewards) — wait `kEscalateTicks` so we don't race
|
||||
// the engine's startup-time state machine, then call CacheFetch
|
||||
// with a no-op callback. The engine sends the SMSG, the response
|
||||
// handler hook fires the event when the response lands.
|
||||
// 2. Giving up after `kMaxWaitTicks` for items that never resolve
|
||||
// (bad itemID, server doesn't respond, etc.) — fire failure.
|
||||
// 1. Latching the startup gate via a tick-count fallback, in case no
|
||||
// engine response arrives early enough to latch it first.
|
||||
// 2. Issuing a query for items the engine doesn't naturally prefetch
|
||||
// (e.g. quest rewards) — but only once `g_settled`, so we don't
|
||||
// race the engine's startup-time state machine. The engine sends
|
||||
// the SMSG, the response handler hook fires the event when the
|
||||
// response lands.
|
||||
// 3. Firing failure immediately for items the server rejected (the
|
||||
// escalation callback marked them `failed`), and as a last resort
|
||||
// giving up after `kMaxWaitTicks` for items that never resolve at
|
||||
// all (lost query, server silent) — fire failure.
|
||||
static void OnWorldTick() {
|
||||
if (!g_settled && ++g_worldTicks >= kSettleTicks)
|
||||
g_settled = true;
|
||||
for (int i = 0; i < g_pendingCount;) {
|
||||
if (!g_pending[i].requestIssued && g_pending[i].ticksWaiting >= kEscalateTicks) {
|
||||
CacheFetch(g_pending[i].itemID, &ItemLoadCallback_NoOp);
|
||||
if (!g_pending[i].requestIssued && g_settled) {
|
||||
CacheFetch(g_pending[i].itemID, &ItemLoadCallback_Escalated);
|
||||
g_pending[i].requestIssued = true;
|
||||
}
|
||||
// A server rejection (marked by the escalation callback) resolves
|
||||
// the item as failure without waiting out the timeout.
|
||||
if (TryCompletePending(i))
|
||||
continue;
|
||||
if (g_pending[i].ticksWaiting >= kMaxWaitTicks) {
|
||||
const uint32_t itemID = g_pending[i].itemID;
|
||||
const bool implicit = g_pending[i].implicit;
|
||||
RemovePendingAt(i);
|
||||
FireItemDataLoadResult(static_cast<int>(itemID), false);
|
||||
FireByTag(itemID, false, implicit);
|
||||
continue;
|
||||
}
|
||||
++g_pending[i].ticksWaiting;
|
||||
@@ -291,7 +381,24 @@ static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
// `ITEM_DATA_LOAD_RESULT`), matching modern semantics for ambient
|
||||
// cache fills.
|
||||
void WarmCache(uint32_t itemID) {
|
||||
CacheFetch(itemID, &ItemLoadCallback_Implicit);
|
||||
// Post-settle (steady state): query immediately for lowest latency.
|
||||
// The engine invokes our implicit callback when the data lands.
|
||||
if (g_settled) {
|
||||
CacheFetch(itemID, &ItemLoadCallback_Implicit);
|
||||
return;
|
||||
}
|
||||
// Startup window: do NOT eagerly create the cache entry — that
|
||||
// orphans it (the engine's prefetch skips already-existing entries,
|
||||
// and our SMSG goes into a hole while dispatch isn't ready), leaving
|
||||
// the item permanently nil for the session. This is the exact race
|
||||
// that broke pfQuest/Questie scanning at PLAYER_ENTERING_WORLD. Defer
|
||||
// through the pending set instead; OnWorldTick issues the query once
|
||||
// settled and the response-handler sweep fires GET_ITEM_INFO_RECEIVED.
|
||||
//
|
||||
// NOTE: g_settled is a one-way latch per process. The race this
|
||||
// guards is the initial-login cold-WDB window. On relog the WDB is
|
||||
// already warm so cold-fetch pressure is minimal; we don't re-arm it.
|
||||
Track(itemID, /*implicit=*/true);
|
||||
}
|
||||
|
||||
// Hook for `Script_GetItemInfo`. Pre-warms the cache from arg 1, then
|
||||
@@ -314,8 +421,8 @@ static int __fastcall Script_RequestLoadItemDataByID(void *L) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
RequestAndMaybeNotify(static_cast<uint32_t>(itemID));
|
||||
Game::Lua::PushBoolean(L, 1);
|
||||
const bool accepted = RequestAndMaybeNotify(static_cast<uint32_t>(itemID));
|
||||
Game::Lua::PushBoolean(L, accepted ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -329,8 +436,8 @@ static int __fastcall Script_RequestLoadItemData(void *L) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
RequestAndMaybeNotify(static_cast<uint32_t>(itemID));
|
||||
Game::Lua::PushBoolean(L, 1);
|
||||
const bool accepted = RequestAndMaybeNotify(static_cast<uint32_t>(itemID));
|
||||
Game::Lua::PushBoolean(L, accepted ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "guid/Guid.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Player::Info {
|
||||
|
||||
@@ -182,6 +183,92 @@ int PushFromNameCacheEntry(void *L, const char *name,
|
||||
return 7;
|
||||
}
|
||||
|
||||
// `UnitNameFromGUID(guid)` → `(name, realm)`. Modern WoW backport;
|
||||
// vanilla 1.12 doesn't expose any GUID → name accessor at the Lua
|
||||
// surface, so addons that need to resolve a chat link or combat-log
|
||||
// GUID can't get a name without round-tripping through a unit token.
|
||||
//
|
||||
// Resolution order (matches vanilla's own `Script_UnitName` at
|
||||
// `0x00517020`):
|
||||
// 1. Resolve the GUID through the object manager. Any currently-
|
||||
// synced unit — player you can see, NPC in interact range,
|
||||
// target / mouseover / party / raid member — gets a `CGObject *`
|
||||
// back. Call the engine's canonical polymorphic name getter
|
||||
// `FUN_OBJECT_GET_NAME` on it; this routes through the player
|
||||
// NameCache for players AND the creature cache for NPCs, so it
|
||||
// works for both with a single call.
|
||||
// 2. If the unit isn't currently synced (ex-target who logged off,
|
||||
// player encountered earlier in chat but no longer visible),
|
||||
// fall back to the persistent NameCache (when enabled).
|
||||
//
|
||||
// `realm` is always `""` in vanilla — the engine doesn't populate
|
||||
// per-player realm names and 1.12 has no cross-realm interaction.
|
||||
// We push the empty string rather than nil so callers don't have to
|
||||
// special-case the second return; modern WoW returns nil, but
|
||||
// `realm == ""` is just as easy to gate on.
|
||||
//
|
||||
// Returns nothing on:
|
||||
// - Missing or non-string arg (raises an error rather than return).
|
||||
// - Unparseable GUID string.
|
||||
// - Zero / NULL GUID.
|
||||
// - GUID not resolvable in the object manager AND not in persistent
|
||||
// NameCache (i.e., we've never seen this unit).
|
||||
//
|
||||
// Doesn't trigger a network query on miss — passive read, same as
|
||||
// `GetPlayerInfoByGUID`. Use `C_PlayerCache.RememberPlayer` or let
|
||||
// the engine populate via normal interaction.
|
||||
int __fastcall Script_UnitNameFromGUID(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitNameFromGUID(\"0x...\")");
|
||||
return 0;
|
||||
}
|
||||
const char *guidStr = Game::Lua::ToString(L, 1);
|
||||
uint32_t hi, lo;
|
||||
if (!ParseGUID(guidStr, hi, lo))
|
||||
return 0;
|
||||
if (hi == 0 && lo == 0)
|
||||
return 0;
|
||||
|
||||
// Object-manager path — TYPEMASK_OBJECT (0x01) is permissive; the
|
||||
// name getter at `FUN_OBJECT_GET_NAME` does its own type check and
|
||||
// safely returns the "UNKNOWNOBJECT" sentinel for non-unit
|
||||
// objects (gameobjects etc.), so we just gate on the sentinel
|
||||
// rather than pre-filtering by typemask.
|
||||
using ObjectPtr_t = void *(__fastcall *)(uint32_t typeMask,
|
||||
const char *debugMsg,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int debugCode);
|
||||
using GetName_t = const char *(__thiscall *)(void *obj, int *outFlags);
|
||||
|
||||
auto objectPtr = reinterpret_cast<ObjectPtr_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *obj = objectPtr(Offsets::TYPEMASK_OBJECT, nullptr, lo, hi, 0);
|
||||
if (obj != nullptr) {
|
||||
auto getName = reinterpret_cast<GetName_t>(Offsets::FUN_OBJECT_GET_NAME);
|
||||
const char *name = getName(obj, nullptr);
|
||||
if (name != nullptr && *name != '\0' &&
|
||||
std::strcmp(name, "UNKNOWNOBJECT") != 0 &&
|
||||
std::strcmp(name, "Unknown Being") != 0) {
|
||||
Game::Lua::PushString(L, name);
|
||||
Game::Lua::PushString(L, "");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Object-manager miss / sentinel — fall back to the persistent
|
||||
// NameCache (when enabled). Covers ex-units no longer in the
|
||||
// engine's sync window.
|
||||
const std::string *cachedName = nullptr;
|
||||
const NameCache::Entry *cached = NameCache::Lookup(
|
||||
(static_cast<uint64_t>(hi) << 32) | lo, &cachedName);
|
||||
if (cached == nullptr || cachedName == nullptr || cachedName->empty())
|
||||
return 0;
|
||||
Game::Lua::PushString(L, cachedName->c_str());
|
||||
Game::Lua::PushString(L, "");
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `C_PlayerCache.GetPlayerInfoByName(name)` — name-keyed lookup over
|
||||
// the persistent NameCache. Returns `(localizedClass, englishClass,
|
||||
// localizedRace, englishRace, sex, name, realm, guid)` or nothing if
|
||||
@@ -212,6 +299,8 @@ int __fastcall Script_C_PlayerCache_GetPlayerInfoByName(void *L) {
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetPlayerInfoByGUID",
|
||||
&Script_GetPlayerInfoByGUID);
|
||||
Game::Lua::RegisterGlobalFunction("UnitNameFromGUID",
|
||||
&Script_UnitNameFromGUID);
|
||||
Game::Lua::RegisterTableFunction("C_PlayerCache", "GetPlayerInfoByName",
|
||||
&Script_C_PlayerCache_GetPlayerInfoByName);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// 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/>.
|
||||
|
||||
// `UnitSubName(unit)` — returns the NPC's subname / title (the small
|
||||
// italic text under their name in the tooltip): "Innkeeper",
|
||||
// "Stable Master", "<Master Engineer>", etc. Modern WoW exposes this
|
||||
// via `UnitName`'s second return; vanilla 1.12's `UnitName` returns
|
||||
// only one value, so addons that want the subtitle have to roll their
|
||||
// own — until now.
|
||||
//
|
||||
// Returns `nil` for:
|
||||
// - Unresolvable unit token (`target` with nothing targeted, empty
|
||||
// `partyN` slot, etc.).
|
||||
// - Player units — they don't have a row in the creature cache.
|
||||
// - NPCs whose creature data hasn't synced yet (rare; happens when
|
||||
// the unit comes into view but the SMSG_CREATURE_QUERY_RESPONSE
|
||||
// hasn't arrived).
|
||||
// - NPCs with no subname (most non-vendor / non-trainer creatures).
|
||||
//
|
||||
// Raises a Lua error on missing / non-string `unit` argument — same
|
||||
// semantics as the engine's `UnitName`.
|
||||
//
|
||||
// Implementation: walks the creature cache row at `[unit + 0xB30]` →
|
||||
// `[+0x10]` (subname C string). Verified path; same one
|
||||
// VanillaMinimapTracking's blip subname filter uses.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::SubName {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
int __fastcall Script_UnitSubName(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitSubName(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
if (token == nullptr)
|
||||
return 0;
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
|
||||
auto *cacheRow = *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_UNIT_CREATURE_CACHE_ROW);
|
||||
if (cacheRow == nullptr)
|
||||
return 0;
|
||||
|
||||
auto *subName = *reinterpret_cast<const char *const *>(
|
||||
cacheRow + Offsets::OFF_CREATURE_CACHE_SUB_NAME);
|
||||
if (subName == nullptr || *subName == '\0')
|
||||
return 0;
|
||||
|
||||
Game::Lua::PushString(L, subName);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitSubName", &Script_UnitSubName);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Unit::SubName
|
||||
Reference in New Issue
Block a user