mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 12:27:35 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8995e82627 | ||
|
|
94b521881b | ||
|
|
634a440def | ||
|
|
4dce58a457 | ||
|
|
99e9621590 | ||
|
|
e8fc583f19 | ||
|
|
03f63d391a | ||
|
|
6df403444c | ||
|
|
cb7953b503 | ||
|
|
774df8d0d4 | ||
|
|
36e1aa87e4 | ||
|
|
40532aca8e | ||
|
|
8592254b2d | ||
|
|
289f82a7e7 |
@@ -1,7 +1,7 @@
|
||||
## Interface: 11200
|
||||
## Title: !!!ClassicAPI
|
||||
## Notes: Library to emulate aspects of the modern WoW API on 1.12.1 / Lua 5.0.
|
||||
## Version: 1.3
|
||||
## Version: DEV
|
||||
## Author: Brues
|
||||
|
||||
GlobalStrings.lua
|
||||
@@ -29,3 +29,4 @@ Util\PlayerUtil.lua
|
||||
Util\SlashCommandsRegistry.lua
|
||||
Util\LinkUtil.lua
|
||||
Util\IconDataProvider.lua
|
||||
Util\GameTooltip.lua
|
||||
|
||||
@@ -83,12 +83,11 @@ function EventUtil.TriggerOnVariablesLoaded()
|
||||
end
|
||||
|
||||
function EventUtil.ContinueOnAddOnLoaded(addOnName, callback)
|
||||
-- 1.12's IsAddOnLoaded returns a single bool. 3.3.5 split that into
|
||||
-- (isLoadedOrLoading, isLoaded); treat truthy as "ready to use".
|
||||
if IsAddOnLoaded(addOnName) then
|
||||
callback()
|
||||
return
|
||||
end
|
||||
local isLoadedOrLoading, isLoaded = C_AddOns.IsAddOnLoaded(addOnName)
|
||||
if isLoaded then
|
||||
callback();
|
||||
return;
|
||||
end
|
||||
|
||||
EventUtil.RegisterOnceFrameEventAndCallback("ADDON_LOADED", callback, addOnName)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
function GameTooltip_Hide()
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
@@ -18,7 +18,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| Namespace | Calls |
|
||||
|-----------|-------|
|
||||
| [Action](docs/API.md#action) | `GetActionInfo` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
|
||||
| [Combat](docs/API.md#combat) | `InCombatLockdown` |
|
||||
@@ -33,7 +33,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [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` |
|
||||
| [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:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura` |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
|
||||
@@ -43,10 +43,10 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [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` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [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.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` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [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` |
|
||||
| [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` |
|
||||
@@ -71,6 +71,7 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
| [CharacterList](docs/API.md#characterlist) | `GetSavedCharacterOrder`, `SetSavedCharacterOrder` (persist to `WTF\Account\...\ClassicAPI.txt`) |
|
||||
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool` (storage is process-global — writes from glue are visible in-world and vice versa) |
|
||||
| Script | `RunScript` (compile and run a Lua chunk in the glue state's globals — useful for slash-command-style helpers in GlueXML) |
|
||||
| State | `IsLoggedIn` |
|
||||
|
||||
### Macros
|
||||
|
||||
|
||||
+134
-61
@@ -17,6 +17,7 @@ build instructions.
|
||||
- [`C_AddOns.GetAddOnTitle(indexOrName)`](#c_addonsgetaddontitleindexorname)
|
||||
- [`C_AddOns.GetAddOnNotes(indexOrName)`](#c_addonsgetaddonnotesindexorname)
|
||||
- [`C_AddOns.IsAddOnLoadable(indexOrName)`](#c_addonsisaddonloadableindexorname)
|
||||
- [`C_AddOns.IsAddOnLoaded(indexOrName)`](#c_addonsisaddonloadedindexorname)
|
||||
- [`C_AddOns.GetAddOnSecurity(indexOrName)`](#c_addonsgetaddonsecurityindexorname)
|
||||
- [`C_AddOns.DoesAddOnExist(indexOrName)`](#c_addonsdoesaddonexistindexorname)
|
||||
|
||||
@@ -115,6 +116,7 @@ build instructions.
|
||||
- [`GameTooltip:SetSpellByID(spellID)`](#gametooltipsetspellbyidspellid)
|
||||
- [`GameTooltip:SetTalentByID(talentID)`](#gametooltipsettalentbyidtalentid)
|
||||
- [`GameTooltip:SetInventoryItemByID(itemID)`](#gametooltipsetinventoryitembyiditemid)
|
||||
- [`GameTooltip:SetItemByGUID(itemGUID)`](#gametooltipsetitembyguiditemguid)
|
||||
- [`GameTooltip:SetEquipmentSet(name)`](#gametooltipsetequipmentsetname)
|
||||
- [`GameTooltip:GetItem()`](#gametooltipgetitem)
|
||||
- [`GameTooltip:GetSpell()`](#gametooltipgetspell)
|
||||
@@ -200,7 +202,6 @@ build instructions.
|
||||
- [`CastSpellNoToggle` as a macro cast line](#castspellnotoggle-as-a-macro-cast-line)
|
||||
- [`GetMacroSpell(macroSlot)`](#getmacrospellmacroslot)
|
||||
- [`GetMacroIcons` / `GetMacroItemIcons` / `GetLooseMacroIcons` / `GetLooseMacroItemIcons`](#getmacroicons--getmacroitemicons--getloosemacroicons--getloosemacroitemicons)
|
||||
- [`IconDataProviderMixin`](#icondataprovidermixin)
|
||||
|
||||
- [Map](#map)
|
||||
- [`C_Map.GetBestMapForUnit(unitToken)`](#c_mapgetbestmapforunitunittoken)
|
||||
@@ -237,6 +238,7 @@ build instructions.
|
||||
- [`C_QuestLog.IsOnQuest(questID)`](#c_questlogisonquestquestid)
|
||||
- [`C_QuestLog.IsUnitOnQuest(unit, questID)`](#c_questlogisunitonquestunit-questid)
|
||||
- [`C_QuestLog.GetTitleForQuestID(questID)`](#c_questloggettitleforquestidquestid)
|
||||
- [`C_QuestLog.GetQuestDetails(questID)`](#c_questloggetquestdetailsquestid)
|
||||
- [`C_QuestLog.IsQuestDataCachedByID(questID)`](#c_questlogisquestdatacachedbyidquestid)
|
||||
- [`GetQuestLogLeaderBoardID(objectiveIndex [, questIndex])`](#getquestlogleaderboardidobjectiveindex--questindex)
|
||||
|
||||
@@ -566,6 +568,29 @@ C_AddOns.IsAddOnLoadable("HardcoreTooltips") -- false, "DISABLED"
|
||||
C_AddOns.IsAddOnLoadable("garbage") -- false, nil
|
||||
```
|
||||
|
||||
### `C_AddOns.IsAddOnLoaded(indexOrName)`
|
||||
|
||||
Returns `loadedOrLoading, loaded` — two booleans.
|
||||
|
||||
```lua
|
||||
C_AddOns.IsAddOnLoaded("DebugTools") -- true, true
|
||||
C_AddOns.IsAddOnLoaded("garbage") -- false, false
|
||||
C_AddOns.IsAddOnLoaded(1) -- true, true (first addon by index)
|
||||
```
|
||||
|
||||
Modern WoW splits the two returns to distinguish "load-in-progress"
|
||||
from "fully loaded" — the difference matters for `LoadOnDemand`
|
||||
addons whose load is split across multiple `LoadAddOn` callbacks.
|
||||
Vanilla 1.12's addon loader (`FUN_0051F240`) is fully synchronous:
|
||||
the `loaded` byte flips inside a single call, so the in-flight
|
||||
state is never observable from Lua. Both returns are always the
|
||||
same boolean here. We surface the two-return shape so consumer
|
||||
code written against modern API doesn't need to special-case
|
||||
vanilla.
|
||||
|
||||
Unknown addons (numeric index past `GetNumAddOns()`, or
|
||||
string name not in the registry) return `false, false`.
|
||||
|
||||
### `C_AddOns.GetAddOnSecurity(indexOrName)`
|
||||
|
||||
Returns an `Enum.AddOnSecurityStatus` integer (not a string — modern
|
||||
@@ -1390,7 +1415,7 @@ Modern's signatures take a numeric `iconFileID`; we accept icon path
|
||||
strings (e.g. `"INV_Shield_06"`) since vanilla has no fileDataID
|
||||
system. Same string-or-default fallback semantic as 4.3.4 native.
|
||||
|
||||
Cap is **10 sets per character**, matching 4.3.4. The full list re-
|
||||
No cap on the number of sets per character. The full list re-
|
||||
serializes on every mutation; a corrupted file is harmless (parse
|
||||
errors leave the in-memory list empty and the next save rewrites the
|
||||
file from scratch).
|
||||
@@ -1495,9 +1520,8 @@ returns its setID. Honors `IgnoreSlotForSave` — slots flagged ignored
|
||||
at call time get the ignored marker instead of the equipped item's
|
||||
GUID.
|
||||
|
||||
Returns the new setID on success. Returns nothing if:
|
||||
- the name is empty or already in use
|
||||
- the cap of 10 sets is reached
|
||||
Returns the new setID on success. Returns nothing if the name is
|
||||
empty or already in use.
|
||||
|
||||
`icon` defaults to `"INV_Misc_QuestionMark"` if omitted.
|
||||
|
||||
@@ -2574,6 +2598,37 @@ existing `Script_GameTooltip_SetHyperlink` (registry slot 12).
|
||||
> caveat, just with `C_Item.RequestLoadItemData(itemLocation)` /
|
||||
> `Item:OnItemLoad`-style continuation.
|
||||
|
||||
### `GameTooltip:SetItemByGUID(itemGUID)`
|
||||
|
||||
Renders the tooltip for a specific item *instance* identified by
|
||||
GUID — same string `C_Item.GetItemGUID` returns
|
||||
(`"0xHHHHHHHHLLLLLLLL"`). Distinct from `SetItemByID`: this path
|
||||
goes through the live CGItem, so the tooltip includes enchant
|
||||
lines, random-suffix-decorated name + bonuses, and locked/broken
|
||||
state — the same depth `SetBagItem` / `SetInventoryItem` would
|
||||
produce for that specific copy.
|
||||
|
||||
```lua
|
||||
local guid = C_Item.GetItemGUID({ equipmentSlotIndex = 10 }) -- legs
|
||||
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
|
||||
GameTooltip:SetItemByGUID(guid)
|
||||
GameTooltip:Show()
|
||||
```
|
||||
|
||||
Resolves through the engine's own `FUN_OBJECT_RESOLVE_BY_GUID`
|
||||
(same path `C_Item.GetItemLocation` uses), builds the full
|
||||
`item:N:enchant:gem:gem:gem:gem:suffix:unique`-style hyperlink via
|
||||
the engine's link builder, then dispatches to
|
||||
`Script_GameTooltip_SetHyperlink`. The link round-trip is what
|
||||
gives `GameTooltip:GetItem()` the dressed link back when called
|
||||
after — same shape `GetInventoryItemLink` / `GetContainerItemLink`
|
||||
emit for the same slot.
|
||||
|
||||
Silent no-op when the GUID is malformed, the item isn't currently
|
||||
loaded in the client (e.g. items only the server knows about), or
|
||||
the GUID resolves to a non-item object (creature/gameobject GUIDs
|
||||
go to a different resolver and won't match).
|
||||
|
||||
### `GameTooltip:SetUnitAura(unit, index, [filter])`
|
||||
|
||||
Modern unified-aura method. 1.12 splits this into `SetUnitBuff` and
|
||||
@@ -4726,62 +4781,6 @@ MPQ will appear in both the loose and mpq lists).
|
||||
The vanilla legacy globals `GetNumMacroIcons` / `GetMacroIconInfo`
|
||||
remain available unchanged.
|
||||
|
||||
### `IconDataProviderMixin`
|
||||
|
||||
Lua-side wrapper (ported from
|
||||
`Blizzard_FrameXMLBase/Classic/IconDataProvider.lua`) around the four
|
||||
mutators above. Bundles `Spell` and `Item` buckets into one stateful
|
||||
provider with an optional "extras" prepend section, and exposes the
|
||||
modern `GetIconByIndex(i)` / `GetNumIcons()` / `GetIndexOfIcon(icon)`
|
||||
/ `Release()` / `GetIconForSaving(i)` API the modern Macro UI and
|
||||
Equipment Manager icon-pickers consume.
|
||||
|
||||
```lua
|
||||
-- Equipment Manager icon picker (matches modern Cata code as-written):
|
||||
local provider = CreateAndInitFromMixin(IconDataProviderMixin,
|
||||
IconDataProviderExtraType.Equipment)
|
||||
provider:GetNumIcons() -- total count (1 + extras + base)
|
||||
provider:GetIconByIndex(1) -- always the `?` sentinel
|
||||
provider:GetIconByIndex(2) -- first equipped-item icon
|
||||
-- ...
|
||||
provider:Release() -- decref + collectgarbage when last ref drops
|
||||
```
|
||||
|
||||
`IconDataProviderExtraType`:
|
||||
|
||||
- `Spellbook` — pre-walks the spellbook tabs
|
||||
(`GetNumSpellTabs` / `GetSpellTabInfo` / `GetSpellTexture`) **and**
|
||||
the three vanilla talent trees
|
||||
(`GetNumTalentTabs` / `GetNumTalents` / `GetTalentInfo`), gathering
|
||||
every talent's icon — including talents the player hasn't put points
|
||||
into. Dedups via a set, prepends to the picker. Vanilla doesn't have
|
||||
spec groups or PvP talents, so dual-spec / PvP-talent walks present
|
||||
in modern's `IconDataProvider.lua` are omitted. Spells not surfaced
|
||||
in the spellbook UI (racials kept off the bar, profession recipes
|
||||
the engine doesn't list as spells) are still missed — that's a
|
||||
vanilla data limitation, not a port omission.
|
||||
- `Equipment` — pre-walks `INVSLOT_FIRST_EQUIPPED..INVSLOT_LAST_EQUIPPED`
|
||||
via `GetInventoryItemTexture("player", i)`.
|
||||
- `None` — no prepend; base DB only.
|
||||
|
||||
`IconDataProviderIconType`:
|
||||
|
||||
- `Spell` (`= 1`) — `Ability_*` / `Spell_*` set.
|
||||
- `Item` (`= 2`) — `INV_*` set.
|
||||
|
||||
The mixin doesn't cross-dedup between extras and the base DB — the
|
||||
player's currently-equipped weapon icon may appear twice in the
|
||||
final picker (once at the head as an "extra", once again later in
|
||||
its alphabetical spot inside the `INV_*` base list). That's
|
||||
deliberate; the modern engine has the same behavior. Pickers
|
||||
typically tolerate it because the extras section is short.
|
||||
|
||||
Memory model matches modern: `BaseIconFilenames` is module-level
|
||||
state shared across all active providers. The first `Init` triggers
|
||||
the four engine calls (which collectively walk ~30k icon paths); on
|
||||
the last `Release`, `BaseIconFilenames` is nil'd and `collectgarbage()`
|
||||
runs explicitly.
|
||||
|
||||
## Map
|
||||
|
||||
### `C_Map.GetBestMapForUnit(unitToken)`
|
||||
@@ -5622,6 +5621,80 @@ end)
|
||||
C_QuestLog.RequestLoadQuestByID(215)
|
||||
```
|
||||
|
||||
### `C_QuestLog.GetQuestDetails(questID)`
|
||||
|
||||
Returns a table with every field we know how to read from the engine's
|
||||
quest static-info cache for `questID`, or `nil` if the data isn't
|
||||
loaded. One round-trip alternative to calling
|
||||
`GetTitleForQuestID` / `GetQuestLogRewardMoney` / etc. across each of
|
||||
its individual accessors. Quest-helper-style addons that want
|
||||
structured data per quest are the primary consumers.
|
||||
|
||||
Like `GetTitleForQuestID`, this is a pure cache probe — pair it with
|
||||
`C_QuestLog.RequestLoadQuestByID` and listen for
|
||||
`QUEST_DATA_LOAD_RESULT` if the quest isn't already cached.
|
||||
|
||||
#### Returned table
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `questID` | number | Echo of the input. |
|
||||
| `title` | string | Locale-applied. |
|
||||
| `level` | number | Quest level from the static record. |
|
||||
| `questType` | string \| absent | Localized tag string — `"Elite"`, `"Group"`, `"PvP"`, `"Dungeon"`, `"Raid"` — sourced from `QuestInfo.dbc`. Field is omitted for plain quests with no tag. |
|
||||
| `rewardMoney` | number | Reward copper. `0` if the quest *requires* money instead. |
|
||||
| `requiredMoney` | number | Copper the player must hand in to complete (e.g. quartermaster contributions). `0` for quests with no money requirement. |
|
||||
| `rewardMoneyAtMaxLevel` | number | Vanilla's level-60 reward bonus. Added to `rewardMoney` when the player is level 60; populated even on low-level quests. |
|
||||
| `rewardSpellID` | number | `spellID` of a spell *taught* on completion (e.g. profession recipes), or `0` for no learned spell reward. |
|
||||
| `srcItemID` | number | `itemID` the questgiver hands the player on accept (e.g. the sigil in *"Verdant Sigil"* — given on accept, read by the player, then turned back in). `0` = no source item. Same as the quest's `requirements[].id` for "give-then-return" quests. |
|
||||
| `questFlags` | number | Raw `QUEST_FLAGS_*` bitfield — only bit `0x08` (sharable) is positively confirmed-tested by the vanilla engine; other bits are presumed-stored but unverified. |
|
||||
| `isSharable` | boolean | Convenience extraction of bit `0x08` from `questFlags`. |
|
||||
| `description` | string | The questgiver's narrative text. Raw — printf-style `$N` (player name), `$C` (class), `$R` (race) tokens are **not** substituted; use `string.gsub` if you need runtime values. |
|
||||
| `objectives` | string | The "what you must do" summary text, same raw / un-substituted format as `description`. |
|
||||
| `completionText` | string | The "now turn it in" text shown on the reward UI panel after objectives are met. Often empty for simple quests; populated for narrative-heavy ones. Modern API name: `GetQuestLogCompletionText`. |
|
||||
| `poi` | table \| absent | Point-of-interest marker (`{mapID, x, y, opt}`). Set on quests with a server-supplied "go here" location; omitted when `mapID == 0`. |
|
||||
| `rewardItems` | array of `{id, count}` | Items the quest gives unconditionally on turn-in. Empty when there are no fixed rewards. |
|
||||
| `choiceItems` | array of `{id, count}` | "Pick one" reward items. Empty when none. |
|
||||
| `requirements` | array of `{kind, id, count, text}` | Objectives. `kind` is `"monster"` (creature), `"object"` (gameobject), or `"item"` (collect / interact). `text` is the questgiver's per-objective override (e.g. `"Investigate the cave"` instead of the auto-generated `"Mor'shan Bear: 0/8"`); empty string means use the auto-format. Order: NPC/GO objectives first, then item objectives, both 1-indexed. Only non-empty slots included. |
|
||||
|
||||
> **Not included** — race, class, skill, time limit, and suggested-player count. Vanilla 1.12's `SMSG_QUEST_QUERY_RESPONSE` doesn't ship those fields; the server enforces them and filters quests *before* broadcasting, so the client only ever sees quests it could accept and never receives the static restriction values. Verified empirically: a race-restricted starter (`3120` Verdant Sigil — Night Elf Druid only), a dungeon quest (`914`), and a timed delivery (`3364` Scalding Mornbrew — 5-min authored timer) all have those cache slots zero-filled in memory. **Addons that need that data must source it from an external scraped database** like pfQuest's.
|
||||
>
|
||||
> **Field reliability:** everything currently returned is either confirmed-correct via the engine's own `Script_GetQuestLog*` accessors or empirically verified against in-game quest semantics. The `poi` field is the only remaining hypothesis — its offsets are confirmed but no test quest has yet exercised it.
|
||||
|
||||
XP rewards aren't exposed — vanilla 1.12 has no `GetQuestLogRewardXP`,
|
||||
and per emulator-decoded packet structure, the server doesn't include
|
||||
an XP field in `SMSG_QUEST_QUERY_RESPONSE`. XP is computed
|
||||
server-side at turn-in from level-scaling tables.
|
||||
|
||||
#### Example
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("QUEST_DATA_LOAD_RESULT")
|
||||
f:SetScript("OnEvent", function()
|
||||
if event == "QUEST_DATA_LOAD_RESULT" and arg2 == 1 then
|
||||
local d = C_QuestLog.GetQuestDetails(arg1)
|
||||
if d then
|
||||
print(d.title, "level", d.level, d.questType or "(no tag)")
|
||||
for _, r in ipairs(d.requirements) do
|
||||
print(" -", r.kind, r.id, "x", r.count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
C_QuestLog.RequestLoadQuestByID(1467)
|
||||
```
|
||||
|
||||
Sample output for quest 1467 (`"Reagents for Reclaimers Inc."`):
|
||||
|
||||
```
|
||||
Reagents for Reclaimers Inc. level 40 (no tag)
|
||||
- item 6253 x 1
|
||||
```
|
||||
|
||||
with `d.choiceItems = {{id=6793, count=1}, {id=6794, count=1}}`,
|
||||
`d.rewardMoney = 3500`, `d.rewardMoneyAtMaxLevel = 1920`, etc.
|
||||
|
||||
### `C_QuestLog.IsQuestDataCachedByID(questID)`
|
||||
|
||||
Returns `true` if the quest's static data is currently in the
|
||||
|
||||
+33
-5
@@ -186,17 +186,45 @@ void PushLocalizedString(void *L, const char *globalName, const char *fallback)
|
||||
}
|
||||
}
|
||||
|
||||
// In-place `string.gsub(stack[-1], pattern, replacement)` — replaces
|
||||
// the string at the top of the stack with the substitution result.
|
||||
// Discards gsub's second return (the substitution count) by
|
||||
// requesting only one return from `Call`.
|
||||
static void GsubInPlace(void *L, const char *pattern, const char *replacement) {
|
||||
PushString(L, "string");
|
||||
GetTable(L, GLOBALS_INDEX);
|
||||
PushString(L, "gsub");
|
||||
GetTable(L, -2); // [..., str, string, gsub]
|
||||
Remove(L, -2); // [..., str, gsub]
|
||||
Insert(L, -2); // [..., gsub, str]
|
||||
PushString(L, pattern);
|
||||
PushString(L, replacement);
|
||||
Call(L, 3, 1); // [..., result]
|
||||
}
|
||||
|
||||
void PushLocalizedFormatInt(void *L, const char *globalName,
|
||||
const char *fallback, int n) {
|
||||
// Resolve `string.format` from _G. The engine ships it as both a
|
||||
// global and as `string.format`; the global is faster (one
|
||||
// gettable instead of two) and equally available across builds.
|
||||
// Resolve `string.format` from _G.
|
||||
PushString(L, "format");
|
||||
GetTable(L, GLOBALS_INDEX);
|
||||
|
||||
PushLocalizedString(L, globalName, fallback);
|
||||
|
||||
// Expand WoW's `|4singular:plural;` (and 3-form
|
||||
// `|4nom:gen_sing:plural;`) pluralization template by piggybacking
|
||||
// on Lua's `string.gsub`. The 2- vs 3-form distinction is locale
|
||||
// grammar (Slavic counting has a separate genitive-singular form
|
||||
// for 2..4); we collapse to nominative/plural since vanilla has
|
||||
// no proper locale-aware counting rules. 3-form pattern runs
|
||||
// first because it's more specific. Vanilla's `string.format`
|
||||
// doesn't process `|4` natively, so without this the template
|
||||
// would render literally.
|
||||
const char *picker3 = (n == 1) ? "%1" : "%3";
|
||||
const char *picker2 = (n == 1) ? "%1" : "%2";
|
||||
GsubInPlace(L, "|4([^:;]+):([^:;]+):([^;]+);", picker3);
|
||||
GsubInPlace(L, "|4([^:;]+):([^;]+);", picker2);
|
||||
|
||||
PushNumber(L, static_cast<double>(n));
|
||||
Call(L, 2, 1); // format(fmt, n) → string
|
||||
Call(L, 2, 1); // format(expandedFmt, n) → string
|
||||
}
|
||||
} // namespace Lua
|
||||
|
||||
|
||||
+49
-4
@@ -2147,6 +2147,7 @@ enum Offsets {
|
||||
// `Script_IsInInstance`).
|
||||
VAR_CURRENT_MAP_ID = 0x00B4E378,
|
||||
|
||||
|
||||
// Local player's current AreaTable.dbc area ID (u32 storage; only
|
||||
// the low 16 bits are used — the value is broadcast over the wire
|
||||
// as a u16 in `SMSG_PARTY_MEMBER_STATS`). Written by the zone-change
|
||||
@@ -2433,6 +2434,48 @@ enum Offsets {
|
||||
// registry before the load pass.
|
||||
FUN_ADDON_INIT = 0x0051C740,
|
||||
|
||||
// Generic intrusive-list insert/move helper used by `FUN_TOC_PARSER`
|
||||
// to thread new addon entries into the registry's doubly-linked
|
||||
// list. Signature (`__thiscall`):
|
||||
// `(ListCtrl *this, void *entry, int position, int anchor)`
|
||||
// Where:
|
||||
// - `this` — list-control struct (for the addon registry,
|
||||
// this is `&VAR_ADDON_LIST_CTRL`).
|
||||
// - `entry` — entry to insert. If already linked, the
|
||||
// function removes it from its current
|
||||
// position first.
|
||||
// - `position` — `1` = insert at head, anything else (engine
|
||||
// passes `2`) = insert at tail.
|
||||
// - `anchor` — `0` = anchor against the list sentinel
|
||||
// (default), nonzero = anchor relative to
|
||||
// another entry's link field at that offset.
|
||||
//
|
||||
// Used by `Addons::Embedded` to move our injected `!!!ClassicAPI`
|
||||
// entry from the tail (where `FUN_TOC_PARSER` puts it) to the head
|
||||
// — vanilla's addon load pass at `FUN_0051F600` walks the linked
|
||||
// list in insertion order, so without this fix our addon would
|
||||
// load *last*, breaking dependent addons that expect our globals
|
||||
// to be ready in their main chunk.
|
||||
FUN_INTRUSIVE_LIST_INSERT = 0x00521AD0,
|
||||
|
||||
// Addon-registry intrusive-list control. Layout
|
||||
// (`__thiscall`-friendly, generic across all intrusive lists in the
|
||||
// binary):
|
||||
// +0x00 (= `0x00BE1B64`) — `int` next-pointer offset within each
|
||||
// entry (= `0xC` at runtime; entries
|
||||
// store their next-link at
|
||||
// `entry+0xC..0x10`).
|
||||
// +0x04 (= `0x00BE1B68`) — sentinel `next` field
|
||||
// (points back into &sentinel+1 when
|
||||
// the list is empty).
|
||||
// +0x08 (= `0x00BE1B6C`) — list head (alias for sentinel `prev`
|
||||
// when treated as circular).
|
||||
//
|
||||
// `VAR_ADDON_LIST_CTRL` is just the address of the control struct
|
||||
// (i.e., `0x00BE1B64`). Pass `&VAR_ADDON_LIST_CTRL` as the `this`
|
||||
// arg to `FUN_INTRUSIVE_LIST_INSERT`.
|
||||
VAR_ADDON_LIST_CTRL = 0x00BE1B64,
|
||||
|
||||
// `SStrDup(const char *src, const char *file, int line)`. `__stdcall`.
|
||||
// Storm's string-copy wrapper around `SMemAlloc` — used by the engine
|
||||
// itself to populate event entry names. We use it from `Event::Custom`
|
||||
@@ -2567,9 +2610,11 @@ enum Offsets {
|
||||
// We dispatch to these directly so the wrappers don't need to
|
||||
// bounce through `Script_GetAddOnInfo` and unwind 7 returns.
|
||||
//
|
||||
// IS_LOAD_ON_DEMAND `__fastcall(entry) → byte`
|
||||
// Hash-table lookup in the addon registry. Non-zero ⇒
|
||||
// `## LoadOnDemand: 1` in the .toc.
|
||||
// IS_LOADED `__fastcall(entry) → byte`
|
||||
// Hash-table lookup in the addon registry. Returns the byte
|
||||
// at `entry+0x18`, which the addon loader (`FUN_0051F240`)
|
||||
// writes `1` to when the addon has finished loading. Non-zero
|
||||
// ⇒ the addon is loaded.
|
||||
//
|
||||
// CAN_LOAD `__fastcall(entry, char fullCheck=1, int *outStatus,
|
||||
// int *outDepHandle, const char *accountName) → byte`
|
||||
@@ -2590,7 +2635,7 @@ enum Offsets {
|
||||
// Hash-table lookup that returns the security category index
|
||||
// (0=SECURE, 1=INSECURE, 2=BANNED). Defaults to `1` (INSECURE)
|
||||
// when the entry isn't in the override table.
|
||||
FUN_ADDON_IS_LOAD_ON_DEMAND = 0x0051E6F0,
|
||||
FUN_ADDON_IS_LOADED = 0x0051E6F0,
|
||||
FUN_ADDON_CAN_LOAD = 0x0051E780,
|
||||
FUN_ADDON_GET_SECURITY_INDEX = 0x0051E990,
|
||||
|
||||
|
||||
@@ -301,11 +301,58 @@ using AddonInit_t = void(__fastcall *)(const char *accountName);
|
||||
AddonInit_t AddonInit_o = nullptr;
|
||||
|
||||
using TocParser_t = void(__fastcall *)(const char *name);
|
||||
using ListInsert_t = void(__thiscall *)(void *listCtrl, void *entry,
|
||||
int position, int anchor);
|
||||
|
||||
// `FUN_TOC_PARSER` appends new entries to the *tail* of the addon
|
||||
// registry's linked list, and the engine's addon-load pass at
|
||||
// `FUN_0051F600` walks that list in *insertion order* (NOT
|
||||
// alphabetical order — the alphabetical sort only applies to the
|
||||
// flat array `GetAddOnInfo(i)` reads from). So our post-hook
|
||||
// injection lands at the very end of the load order, after every
|
||||
// disk-scanned addon. Addons that consume our globals from their
|
||||
// main chunk (idTip's `FrameWatcher`, etc.) would see `nil` and
|
||||
// hard-error during their boot.
|
||||
//
|
||||
// Fix: walk the linked list, find our entry by name, and re-link
|
||||
// it to the head via `FUN_INTRUSIVE_LIST_INSERT(position=1)`. The
|
||||
// engine's own insert helper handles remove-then-insert atomically,
|
||||
// so it's safe to call on an entry that's already in the list.
|
||||
//
|
||||
// Entries are pointed to by `[VAR_ADDON_LIST_HEAD]` (`0x00BE1B6C`),
|
||||
// with the next-pointer field at `entry + 0x10` (computed as
|
||||
// `*(value-at-VAR_ADDON_LIST_CTRL) + entry + 4` = `0xC + entry + 4`
|
||||
// = `entry + 0x10`). Low-bit-1 means "sentinel" (end of list).
|
||||
// Name pointer lives at `entry + 0x14`.
|
||||
constexpr uintptr_t VAR_ADDON_LIST_HEAD = 0x00BE1B6C;
|
||||
constexpr int OFF_ADDON_ENTRY_NAME_PTR = 0x14;
|
||||
|
||||
void MoveEmbeddedToListHead() {
|
||||
uintptr_t entry = *reinterpret_cast<uintptr_t *>(VAR_ADDON_LIST_HEAD);
|
||||
const int linkOffset = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_CTRL));
|
||||
while ((entry & 1) == 0 && entry != 0) {
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
entry + OFF_ADDON_ENTRY_NAME_PTR);
|
||||
if (name != nullptr && std::strcmp(name, kAddonName) == 0) {
|
||||
auto fn = reinterpret_cast<ListInsert_t>(
|
||||
Offsets::FUN_INTRUSIVE_LIST_INSERT);
|
||||
fn(reinterpret_cast<void *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_CTRL)),
|
||||
reinterpret_cast<void *>(entry),
|
||||
/*position=*/ 1, /*anchor=*/ 0);
|
||||
return;
|
||||
}
|
||||
entry = *reinterpret_cast<const uintptr_t *>(
|
||||
entry + linkOffset + 4);
|
||||
}
|
||||
}
|
||||
|
||||
void __fastcall AddonInit_h(const char *accountName) {
|
||||
AddonInit_o(accountName);
|
||||
auto TocParser = reinterpret_cast<TocParser_t>(Offsets::FUN_TOC_PARSER);
|
||||
TocParser(kAddonName);
|
||||
MoveEmbeddedToListHead();
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookFileRead{
|
||||
|
||||
+39
-3
@@ -163,9 +163,13 @@ int __fastcall Script_IsAddOnLoadable(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
auto isLoD = reinterpret_cast<EntryByteFn_t>(
|
||||
Offsets::FUN_ADDON_IS_LOAD_ON_DEMAND);
|
||||
if (isLoD(entry) != 0) {
|
||||
// Already-loaded addons are trivially loadable — skip the
|
||||
// recursive dep walk and return (true, nil) directly. `IS_LOADED`
|
||||
// reads the byte at `entry+0x18` (the loader sets that to 1 on
|
||||
// successful load).
|
||||
auto isLoaded = reinterpret_cast<EntryByteFn_t>(
|
||||
Offsets::FUN_ADDON_IS_LOADED);
|
||||
if (isLoaded(entry) != 0) {
|
||||
Game::Lua::PushBoolean(L, 1);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
@@ -229,6 +233,36 @@ int __fastcall Script_GetAddOnSecurity(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_AddOns.IsAddOnLoaded(indexOrName)` → `(loadedOrLoading, loaded)`.
|
||||
//
|
||||
// Modern WoW distinguishes "load-in-progress" from "fully loaded" —
|
||||
// matters for LoD addons whose load is split across multiple
|
||||
// `LoadAddOn` callbacks. Vanilla 1.12's `FUN_0051F240` is fully
|
||||
// synchronous: the loaded byte at `entry+0x18` flips from 0 → 1
|
||||
// inside a single call, so the "loading" state is never observable
|
||||
// from Lua. We return the same boolean twice — `loadedOrLoading`
|
||||
// and `loaded` are identical here.
|
||||
//
|
||||
// Resolves both numeric indices (via `FUN_ADDON_GET_BY_INDEX`) and
|
||||
// string names. `IS_LOADED` accepts an entry pointer as its name arg
|
||||
// because the entry's first 12 bytes are the inline NUL-terminated
|
||||
// directory name (same as the other per-field accessors), so we can
|
||||
// feed it either an entry or a Lua-side string transparently.
|
||||
int __fastcall Script_IsAddOnLoaded(void *L) {
|
||||
const uint8_t *entry = ResolveAddOnName(L);
|
||||
if (entry == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
}
|
||||
auto isLoaded = reinterpret_cast<EntryByteFn_t>(
|
||||
Offsets::FUN_ADDON_IS_LOADED);
|
||||
const bool loaded = isLoaded(entry) != 0;
|
||||
Game::Lua::PushBool(L, loaded);
|
||||
Game::Lua::PushBool(L, loaded);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `C_AddOns.DoesAddOnExist(indexOrName)` — true iff the engine's
|
||||
// addon registry has a matching entry. For numeric input we trust
|
||||
// `GetByIndex`'s OOR-NULL; for string input we walk the flat array,
|
||||
@@ -276,6 +310,8 @@ static void RegisterLuaFunctions() {
|
||||
&Script_GetAddOnSecurity);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "DoesAddOnExist",
|
||||
&Script_DoesAddOnExist);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "IsAddOnLoaded",
|
||||
&Script_IsAddOnLoaded);
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AddOnSecurityStatus",
|
||||
kAddOnSecurityStatusEntries,
|
||||
|
||||
+136
-16
@@ -35,6 +35,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "Set.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Swap.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -401,39 +402,158 @@ int __fastcall Script_UseEquipmentSet(void *L) {
|
||||
uint64_t guids[SLOT_COUNT];
|
||||
std::memcpy(guids, s->items, sizeof(guids));
|
||||
|
||||
// Snapshot empty bag slots up front. The CGItem state doesn't
|
||||
// update between packet sends (server has to acknowledge each
|
||||
// swap), so iterating the bag walk fresh per unequip would
|
||||
// repeatedly pick the same "first empty" slot — second packet
|
||||
// collides with what packet 1 just put there, and the server
|
||||
// tries to swap that item back onto the wrong paperdoll slot
|
||||
// → "This item cannot be equipped".
|
||||
struct FreeSlot {
|
||||
int bag;
|
||||
int slot;
|
||||
};
|
||||
FreeSlot freeSlots[64];
|
||||
int freeSlotCount = 0;
|
||||
for (int bag = 0; bag <= 4 && freeSlotCount < 64; ++bag) {
|
||||
const int slots = Item::Location::GetBagSlotCount(bag);
|
||||
for (int slot = 1; slot <= slots && freeSlotCount < 64; ++slot) {
|
||||
if (Item::Location::ResolveBag(L, bag, slot) == nullptr)
|
||||
freeSlots[freeSlotCount++] = {bag, slot};
|
||||
}
|
||||
}
|
||||
int nextFreeSlot = 0;
|
||||
|
||||
// Pre-snapshot each set item's location against the ORIGINAL
|
||||
// state, before any swap packets fly. This mirrors how 4.3.4's
|
||||
// `Script_UseEquipmentSet` (`0x009A15D0`) builds its action list:
|
||||
// resolve all 19 source locations up front, then execute. The
|
||||
// engine's CGItem / InvMgr state lags behind our packet sends
|
||||
// (waits for server ACK), so any `FindGUID` mid-execution would
|
||||
// return stale answers and we'd re-resolve items into the wrong
|
||||
// place.
|
||||
int sourceLoc[SLOT_COUNT];
|
||||
for (int i = 0; i < SLOT_COUNT; ++i) {
|
||||
const uint64_t g = guids[i];
|
||||
if (g == GUID_EMPTY || g == GUID_IGNORED) {
|
||||
sourceLoc[i] = 0;
|
||||
continue;
|
||||
}
|
||||
sourceLoc[i] = Locations::FindGUID(g);
|
||||
}
|
||||
|
||||
// Virtual paperdoll occupant model — seeded from the engine's
|
||||
// initial paperdoll GUID array, then updated in-place after each
|
||||
// swap so phase 1 can chain rotations and phase 3 can spot
|
||||
// already-empty slots.
|
||||
uint64_t virtualPaperdoll[SLOT_COUNT];
|
||||
Locations::SnapshotPaperdoll(virtualPaperdoll);
|
||||
|
||||
// Locates the current virtual slot of a GUID on the paperdoll,
|
||||
// or 0 if it's not currently there. Used during phase 1 so
|
||||
// chained paperdoll-to-paperdoll moves operate on the *current*
|
||||
// virtual position, not the stale pre-snapshot.
|
||||
auto currentPaperdollSlot = [&](uint64_t g) -> int {
|
||||
if (g == 0)
|
||||
return 0;
|
||||
for (int slot = 1; slot <= SLOT_COUNT; ++slot) {
|
||||
if (virtualPaperdoll[slot - 1] == g)
|
||||
return slot;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// ── Phase 1: paperdoll → paperdoll ─────────────────────────────
|
||||
// Move items already on the paperdoll into their target slots.
|
||||
// Source and destination are both paperdoll, so atomic swaps
|
||||
// never touch bag state — phase 2's bag lookups stay accurate.
|
||||
for (int i = 0; i < SLOT_COUNT; ++i) {
|
||||
const int targetSlot = i + 1;
|
||||
const uint64_t g = guids[i];
|
||||
if (g == GUID_EMPTY || g == GUID_IGNORED)
|
||||
continue;
|
||||
|
||||
const int loc = Locations::FindGUID(g);
|
||||
const int loc = sourceLoc[i];
|
||||
if (loc == 0)
|
||||
continue; // missing — nothing to do
|
||||
|
||||
// Already in its target slot? Skip.
|
||||
if ((loc & LOC_BAGS) == 0 && (loc & LOC_BANK) == 0 &&
|
||||
(loc & LOC_SLOT_MASK) == targetSlot)
|
||||
continue;
|
||||
|
||||
// In bank (main or bank-bag) — server rejects equip-from-bank.
|
||||
if (loc & LOC_BANK)
|
||||
continue;
|
||||
continue; // server rejects equip-from-bank
|
||||
if (loc & LOC_BAGS)
|
||||
continue; // phase 2
|
||||
|
||||
const int srcSlot = currentPaperdollSlot(g);
|
||||
if (srcSlot == 0 || srcSlot == targetSlot)
|
||||
continue; // missing or already there
|
||||
|
||||
const uint8_t *item = Locations::ResolveItemByGUID(g);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
|
||||
if (loc & LOC_BAGS) {
|
||||
const int srcBag = (loc >> LOC_BAG_SHIFT) & 0xFF;
|
||||
const int srcSlot = loc & LOC_SLOT_MASK;
|
||||
Item::Swap::FromBag(item, srcBag, srcSlot, targetSlot);
|
||||
} else {
|
||||
const int srcSlot = loc & LOC_SLOT_MASK;
|
||||
Item::Swap::FromPaperdoll(item, srcSlot, targetSlot);
|
||||
const uint64_t displaced = virtualPaperdoll[targetSlot - 1];
|
||||
if (Item::Swap::FromPaperdoll(item, srcSlot, targetSlot)) {
|
||||
virtualPaperdoll[targetSlot - 1] = g;
|
||||
virtualPaperdoll[srcSlot - 1] = displaced;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 2: bag → paperdoll ───────────────────────────────────
|
||||
// Move items from bag slots onto the paperdoll. Each atomic swap
|
||||
// displaces the current paperdoll occupant into the bag slot the
|
||||
// incoming item vacated. Anything we still wanted on the paperdoll
|
||||
// was already placed by phase 1, so phase 2 displacements can't
|
||||
// strand a future set item in a bag.
|
||||
for (int i = 0; i < SLOT_COUNT; ++i) {
|
||||
const int targetSlot = i + 1;
|
||||
const uint64_t g = guids[i];
|
||||
if (g == GUID_EMPTY || g == GUID_IGNORED)
|
||||
continue;
|
||||
if (virtualPaperdoll[targetSlot - 1] == g)
|
||||
continue; // already at target (probably by phase 1)
|
||||
|
||||
const int loc = sourceLoc[i];
|
||||
if (loc == 0)
|
||||
continue;
|
||||
if (loc & LOC_BANK)
|
||||
continue;
|
||||
if ((loc & LOC_BAGS) == 0)
|
||||
continue; // not from a bag — phase 1 handled, or item missing
|
||||
|
||||
const uint8_t *item = Locations::ResolveItemByGUID(g);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
|
||||
const int srcBag = (loc >> LOC_BAG_SHIFT) & 0xFF;
|
||||
const int srcSlot = loc & LOC_SLOT_MASK;
|
||||
if (Item::Swap::FromBag(item, srcBag, srcSlot, targetSlot)) {
|
||||
virtualPaperdoll[targetSlot - 1] = g;
|
||||
// The displaced item lands in (srcBag, srcSlot). We
|
||||
// don't track bag-side virtual state — by phase 3 we
|
||||
// only care about the paperdoll occupant.
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 3: unequip slots the set wants empty ─────────────────
|
||||
// For each `GUID_EMPTY` slot, send the current paperdoll occupant
|
||||
// (if any) to a pre-snapshotted free bag slot. Runs last so any
|
||||
// displaced items from phases 1/2 that landed in the slot get
|
||||
// unequipped here too.
|
||||
for (int i = 0; i < SLOT_COUNT; ++i) {
|
||||
const int targetSlot = i + 1;
|
||||
if (guids[i] != GUID_EMPTY)
|
||||
continue;
|
||||
const uint64_t occupant = virtualPaperdoll[targetSlot - 1];
|
||||
if (occupant == 0)
|
||||
continue;
|
||||
if (nextFreeSlot >= freeSlotCount)
|
||||
continue;
|
||||
const uint8_t *equipped = Locations::ResolveItemByGUID(occupant);
|
||||
if (equipped == nullptr)
|
||||
continue;
|
||||
const FreeSlot &fs = freeSlots[nextFreeSlot++];
|
||||
if (Item::Swap::ToBag(equipped, targetSlot, fs.bag, fs.slot))
|
||||
virtualPaperdoll[targetSlot - 1] = 0;
|
||||
}
|
||||
|
||||
const int evt = Event::Custom::Lookup(kSwapFinishedEvent);
|
||||
if (evt >= 0)
|
||||
Event::Custom::Fire(evt, "%d%d", 1, static_cast<int>(setID));
|
||||
|
||||
@@ -140,8 +140,6 @@ uint32_t Create(const char *name, const char *icon) {
|
||||
EnsureLoaded();
|
||||
if (g_path.empty() || name == nullptr || name[0] == '\0')
|
||||
return 0;
|
||||
if (g_sets.size() >= MAX_SETS)
|
||||
return 0;
|
||||
if (FindByName(name) != nullptr)
|
||||
return 0;
|
||||
Set s;
|
||||
|
||||
@@ -177,4 +177,15 @@ const uint8_t *ResolveItemByGUID(uint64_t guid) {
|
||||
return Item::Location::ResolveByGUID(guid);
|
||||
}
|
||||
|
||||
void SnapshotPaperdoll(uint64_t *out) {
|
||||
for (int i = 0; i < SLOT_COUNT; ++i)
|
||||
out[i] = 0;
|
||||
auto *invMgr = ResolvePlayerInvMgr();
|
||||
auto *guids = InvMgrGuidArray(invMgr);
|
||||
if (guids == nullptr)
|
||||
return;
|
||||
for (int i = 0; i < SLOT_COUNT; ++i)
|
||||
out[i] = guids[i];
|
||||
}
|
||||
|
||||
} // namespace EquipmentSet::Locations
|
||||
|
||||
@@ -42,4 +42,16 @@ int FindGUID(uint64_t targetGuid);
|
||||
// isn't known to the client. Side-effect free.
|
||||
const uint8_t *ResolveItemByGUID(uint64_t guid);
|
||||
|
||||
// Copies the current paperdoll GUIDs into `out[0..SLOT_COUNT-1]`,
|
||||
// where `out[i]` is the GUID at 1-based equipment slot `i+1`.
|
||||
// Empty / unequipped slots write 0. Reads from the player invMgr's
|
||||
// flat GUID array directly — no Lua stack, no engine state changes.
|
||||
//
|
||||
// Used by `UseEquipmentSet` to seed a "virtual paperdoll" model that
|
||||
// stays current across in-flight swaps. The engine's CGItem state
|
||||
// lags behind packet sends (server has to ACK each swap), so any
|
||||
// subsequent `FindGUID` after the first packet would return stale
|
||||
// pre-swap locations. Tracking moves locally bypasses that lag.
|
||||
void SnapshotPaperdoll(uint64_t *out);
|
||||
|
||||
} // namespace EquipmentSet::Locations
|
||||
|
||||
@@ -56,11 +56,6 @@ inline const char *DefaultIcon() { return "INV_Misc_QuestionMark"; }
|
||||
// (1.15.x is also 32). 4.3.4 used the same buffer size.
|
||||
constexpr int MAX_NAME_LEN = 31;
|
||||
|
||||
// Hard cap matching 4.3.4 behavior. Modern Classic Era (1.15) raised
|
||||
// this to 30; we follow the 4.3.4 number so legacy addons that show
|
||||
// "X / 10" don't surprise users.
|
||||
constexpr int MAX_SETS = 10;
|
||||
|
||||
// `GetItemLocations` encoding — matches Blizzard FrameXML's
|
||||
// `EquipmentManager_UnpackLocation` (constants are in
|
||||
// `Constants.lua`: `ITEM_INVENTORY_LOCATION_*` and
|
||||
|
||||
@@ -199,6 +199,38 @@ bool FromPaperdoll(const void *cgItem, int srcPaperdollSlot, int dstPaperdollSlo
|
||||
dstPaperdollSlot);
|
||||
}
|
||||
|
||||
bool ToBag(const void *cgItem, int srcPaperdollSlot, int dstBagID, int dstSlotInBag) {
|
||||
if (srcPaperdollSlot < 1 || srcPaperdollSlot > 19)
|
||||
return false;
|
||||
if (cgItem == nullptr)
|
||||
return false;
|
||||
|
||||
void *player = ResolvePlayer();
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
uint32_t playerLo = 0, playerHi = 0;
|
||||
if (!ReadGuid(player, &playerLo, &playerHi))
|
||||
return false;
|
||||
|
||||
uint32_t dstContainerLo = 0, dstContainerHi = 0, dstLinear = 0;
|
||||
if (!EncodeBagSlot(dstBagID, dstSlotInBag,
|
||||
&dstContainerLo, &dstContainerHi, &dstLinear))
|
||||
return false;
|
||||
|
||||
uint32_t itemLo = 0, itemHi = 0;
|
||||
if (!ReadGuid(cgItem, &itemLo, &itemHi))
|
||||
return false;
|
||||
|
||||
auto fn = reinterpret_cast<SwapFn_t>(Offsets::FUN_INVENTORY_SWAP);
|
||||
fn(player,
|
||||
itemLo, itemHi,
|
||||
playerLo, playerHi,
|
||||
static_cast<uint32_t>(srcPaperdollSlot - 1),
|
||||
dstContainerLo, dstContainerHi, dstLinear,
|
||||
0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MoveCount(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot, int count) {
|
||||
// Vanilla protocol writes count as a single byte. Clamp at 255;
|
||||
// anything larger would either truncate silently (255 sent) or
|
||||
|
||||
@@ -32,6 +32,12 @@ namespace Item::Swap {
|
||||
// paperdoll slot `srcSlot` (1-based). Used for paperdoll-to-
|
||||
// paperdoll swaps like rings 11/12, trinkets 13/14.
|
||||
//
|
||||
// - `ToBag(item, srcPaperdollSlot, dstBag, dstSlot)` — item currently
|
||||
// in paperdoll slot, moving to a bag slot. The "unequip" direction
|
||||
// `UseEquipmentSet` needs for slots the set wants empty. Uses the
|
||||
// same atomic swap packet — server-side accept rules are identical
|
||||
// (bagID 0..4, dst slot must be valid for that bag).
|
||||
//
|
||||
// All slot args are 1-based Lua-facing; helpers do the
|
||||
// linear-slot conversion. Returns `false` on bad args (slot out of
|
||||
// range, no player object, missing instance block on the item or
|
||||
@@ -44,6 +50,8 @@ bool FromBag(const void *cgItem, int bagID, int slotInBag, int dstPaperdollSlot)
|
||||
|
||||
bool FromPaperdoll(const void *cgItem, int srcPaperdollSlot, int dstPaperdollSlot);
|
||||
|
||||
bool ToBag(const void *cgItem, int srcPaperdollSlot, int dstBagID, int dstSlotInBag);
|
||||
|
||||
// General bag-to-bag swap — neither side is constrained to the
|
||||
// paperdoll. Same engine helper (`FUN_INVENTORY_SWAP`), which routes
|
||||
// to opcode 0x10D (same-container slot swap, e.g. backpack ↔ backpack)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace Item::Tooltip {
|
||||
|
||||
@@ -290,8 +291,82 @@ static int __fastcall Script_GameTooltipHasItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `GameTooltip:SetItemByGUID(guidString)` — modern method that renders
|
||||
// the tooltip for the specific item instance identified by GUID
|
||||
// (`"0xHHHHHHHHLLLLLLLL"` per `C_Item.GetItemGUID`). Distinct from
|
||||
// `SetItemByID`, which shows the base `ItemSparse` data with no
|
||||
// per-instance state: this path uses the live CGItem so the tooltip
|
||||
// includes enchant lines, random-suffix-decorated name + bonuses,
|
||||
// locked/broken state, etc.
|
||||
//
|
||||
// Implementation: parse the GUID string, resolve via the engine's
|
||||
// `FUN_OBJECT_RESOLVE_BY_GUID` (same path `C_Item.GetItemLocation`
|
||||
// and `GameTooltip:GetItem` use), build the fully-decorated
|
||||
// hyperlink via `Item::Link::FromCGItem` (engine helper at
|
||||
// `FUN_GAMETOOLTIP_BUILD_ITEM_LINK`), then dispatch to the engine's
|
||||
// existing `Script_GameTooltip_SetHyperlink`. SetHyperlink parses
|
||||
// the link's `item:N:enchant:gem:gem:gem:gem:suffix:unique` payload
|
||||
// back into instance-specific tooltip data — closing the loop
|
||||
// without us needing a separate engine entry point for "tooltip
|
||||
// from CGItem*".
|
||||
//
|
||||
// Silent no-op when the GUID is malformed, not loaded in the
|
||||
// client, or doesn't resolve to an item (creature/object GUIDs go
|
||||
// to a different resolver).
|
||||
static int __fastcall Script_GameTooltipSetItemByGUID(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::Error(L, "Usage: GameTooltip:SetItemByGUID(itemGUID)");
|
||||
return 0;
|
||||
}
|
||||
if (Game::Lua::Type(L, 2) != Game::Lua::TYPE_STRING) {
|
||||
Game::Lua::Error(L, "Usage: GameTooltip:SetItemByGUID(itemGUID)");
|
||||
return 0;
|
||||
}
|
||||
const char *guidStr = Game::Lua::ToString(L, 2);
|
||||
uint64_t guid = 0;
|
||||
if (!Item::Location::ParseGUIDString(guidStr, &guid))
|
||||
return 0;
|
||||
const uint8_t *cgItem = Item::Location::ResolveByGUID(guid);
|
||||
if (cgItem == nullptr)
|
||||
return 0;
|
||||
const char *link = Item::Link::FromCGItem(cgItem);
|
||||
if (link == nullptr || *link == '\0')
|
||||
return 0;
|
||||
|
||||
// `Item::Link::FromCGItem` returns the full
|
||||
// `|cffXXXXXXXX|Hitem:N:E:S:S:S:S:R:U|h[Name]|h|r` decorated link
|
||||
// (the public-link form addons paste into chat). `SetHyperlink`
|
||||
// wants just the bare `item:N:E:S:S:S:S:R:U` payload — passing the
|
||||
// decorated form trips its "Unknown link type" Lua error because
|
||||
// the parser sees `|c` first and gives up. Extract the payload by
|
||||
// finding `|Hitem:` and copying through to the closing `|h`.
|
||||
const char *open = std::strstr(link, "|Hitem:");
|
||||
if (open == nullptr)
|
||||
return 0;
|
||||
open += 2; // skip past `|H` so we start at `item:`
|
||||
const char *close = std::strstr(open, "|h");
|
||||
if (close == nullptr)
|
||||
return 0;
|
||||
const size_t payloadLen = static_cast<size_t>(close - open);
|
||||
char payload[128];
|
||||
if (payloadLen + 1 > sizeof(payload))
|
||||
return 0;
|
||||
std::memcpy(payload, open, payloadLen);
|
||||
payload[payloadLen] = '\0';
|
||||
|
||||
// Replace stack[2] (the GUID) with the bare item link and
|
||||
// dispatch to SetHyperlink with (self, "item:...").
|
||||
Game::Lua::SetTop(L, 1);
|
||||
Game::Lua::PushString(L, payload);
|
||||
|
||||
using Script_t = int(__fastcall *)(void *L);
|
||||
auto fn = reinterpret_cast<Script_t>(Offsets::FUN_SCRIPT_GAMETOOLTIP_SET_HYPERLINK);
|
||||
return fn(L);
|
||||
}
|
||||
|
||||
static const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"SetItemByID", &Script_GameTooltipSetItemByID},
|
||||
{"SetItemByGUID", &Script_GameTooltipSetItemByGUID},
|
||||
{"SetInventoryItemByID", &Script_GameTooltipSetInventoryItemByID},
|
||||
{"GetItem", &Script_GameTooltipGetItem},
|
||||
{"HasItem", &Script_GameTooltipHasItem},
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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/>.
|
||||
|
||||
// `IsLoggedIn()` — exposed on both Lua states (glue and in-game).
|
||||
// Returns `true` once a character has been loaded *and identified*
|
||||
// (GUID populated), `false` on glue, pre-load, and the brief window
|
||||
// between struct allocation and character-data unpack.
|
||||
//
|
||||
// Check is two-step:
|
||||
// 1. `[VAR_LOCAL_PLAYER_PTR]` non-NULL — the player aggregator
|
||||
// struct has been allocated
|
||||
// 2. `*(uint64*)(player + OFF_LOCAL_PLAYER_GUID)` non-zero — the
|
||||
// character's GUID has been populated into the struct
|
||||
//
|
||||
// The pointer alone is too lax: the engine allocates the struct
|
||||
// shortly before the character data lands, so there's a window where
|
||||
// the pointer is non-NULL but the GUID is still zero. Most addons
|
||||
// that check `IsLoggedIn` go straight to `UnitGUID("player")` next,
|
||||
// and we don't want them to see GUID==nil right after IsLoggedIn
|
||||
// reported true.
|
||||
//
|
||||
// 3.3.5 uses a single byte flag at `[0x00BD0793]` cleared on world
|
||||
// entry — we couldn't pin down the analogous 1.12 flag (the engine
|
||||
// path that signals event 253 wires through CVar-init machinery and
|
||||
// doesn't map cleanly across the binary). The pointer+GUID check is
|
||||
// the same effective signal: both flip together at the moment the
|
||||
// player object is ready, and stay stable across zone transitions.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Player::LoginStatus {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_IsLoggedIn(void *L) {
|
||||
auto *player = *reinterpret_cast<const uint8_t *const *>(
|
||||
Offsets::VAR_LOCAL_PLAYER_PTR);
|
||||
if (player == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const uint64_t guid = *reinterpret_cast<const uint64_t *>(
|
||||
player + Offsets::OFF_LOCAL_PLAYER_GUID);
|
||||
Game::Lua::PushBool(L, guid != 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("IsLoggedIn", &Script_IsLoggedIn);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("IsLoggedIn", &Script_IsLoggedIn);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
static const Game::GlueModuleAutoRegister _gluereg{&RegisterGlue};
|
||||
|
||||
} // namespace Player::LoginStatus
|
||||
+155
-1
@@ -40,15 +40,156 @@ inline const uint8_t *Peek(uint32_t questID) {
|
||||
}
|
||||
|
||||
// Field offsets within the data block returned by `Lookup`.
|
||||
constexpr int OFF_TITLE = 0x9C;
|
||||
//
|
||||
// Derived from the engine's own `Script_GetQuestLog*` accessors at
|
||||
// `0x004DF930..0x004E12B0`. Each accessor calls `_GetRecord` then
|
||||
// reads from one of these offsets; the existing comment block on each
|
||||
// constant identifies the source function. See CLAUDE.md's
|
||||
// "Quest static-info cache" section for the full trace.
|
||||
|
||||
// Numeric metadata fields, in wire order from `SMSG_QUEST_QUERY_RESPONSE`.
|
||||
//
|
||||
// The cache layout mirrors the packet's field order — the deserializer
|
||||
// at the SMSG handler unpacks 15 uint32 fields at +0x00..+0x38 before
|
||||
// the reward/choice item arrays. Names and semantics cross-referenced
|
||||
// from the in-binary serializer (`FUN_006DBCEC`) plus the canonical
|
||||
// vanilla 1.12 packet structure documented by vmangos/cmangos emulator
|
||||
// projects. Fields without a direct binary-side accessor (so just
|
||||
// known from wire-format alignment) are marked **(hypothesis)** —
|
||||
// they should be verified empirically before being relied on for
|
||||
// gameplay logic.
|
||||
//
|
||||
// QuestId — uint32, echo of the questID this entry caches.
|
||||
constexpr int OFF_QUEST_ID = 0x00;
|
||||
// QuestMethod — uint32. Server-side classification (typically `2` for
|
||||
// active-and-available, `0` for unavailable). Vanilla doesn't use
|
||||
// this for gameplay; it's stored but no engine accessor reads it.
|
||||
// **(hypothesis)**
|
||||
constexpr int OFF_QUEST_METHOD = 0x04;
|
||||
// Quest level — int32. Source: `FUN_004df340` (called by
|
||||
// `Script_GetQuestLogTitle`) does `mov eax, [block + 0x08]` and
|
||||
// pushes it as `level`.
|
||||
constexpr int OFF_LEVEL = 0x08;
|
||||
// ZoneOrSort — int32. The packet's `ZoneOrSort` field, used by the
|
||||
// quest log UI to group entries under headers:
|
||||
// - Positive = `AreaTable.dbc` zone areaID (e.g. 132 = Coldridge
|
||||
// Valley, 12 = Elwynn Forest)
|
||||
// - Negative = `-QuestSort.dbc` row (e.g. -263 = "Druid" sort)
|
||||
// `FUN_004dee60` reads this directly and matches it against
|
||||
// `VAR_QUEST_LOG_CATEGORY_TABLE` to find the collapsible header index.
|
||||
constexpr int OFF_ZONE_OR_SORT = 0x0C;
|
||||
// QuestInfo.dbc row — uint32 row index into the small DBC at
|
||||
// `0x00C0D9CC` (records) / `0x00C0D9D0` (count). Each record is
|
||||
// `{id@+0, char *name[locale]@+4}`; the localized name is the
|
||||
// quest-type tag string ("Dungeon", "Raid", "Group", "PvP", "Elite"
|
||||
// — modern WoW's `frequency`/`questTag`). Source: `FUN_004df2a0`.
|
||||
constexpr int OFF_QUEST_INFO_ROW = 0x10;
|
||||
// +0x14, +0x18, +0x1C, +0x20, +0x24 — five uint32 cache slots that
|
||||
// SMSG_QUEST_QUERY_RESPONSE in vanilla 1.12 doesn't populate. The
|
||||
// struct has them (the serializer at `0x006DBCEC` writes them as part
|
||||
// of the WDB save / network shape), but the response handler leaves
|
||||
// them zero-filled. Verified empirically across three diverse quests
|
||||
// (race-restricted starter, dungeon, authored-timer delivery) — all
|
||||
// five slots zero in every case.
|
||||
//
|
||||
// Wire-format docs (vmangos / cmangos emulator quest_template) name
|
||||
// them SuggestedPlayers, LimitTime, RequiredRaces, RequiredSkill,
|
||||
// RequiredSkillValue respectively, but the vanilla server *enforces*
|
||||
// these constraints server-side and filters quests before broadcasting
|
||||
// them, so the client never needs the data. Addons that want race /
|
||||
// class / skill / timer info must source from an external scraped DB
|
||||
// (pfQuest-style). Constants intentionally omitted — no use case.
|
||||
//
|
||||
// Signed money delta — int32. Positive = reward copper, negative =
|
||||
// `-required` copper. The engine has two accessors that share this
|
||||
// field with complementary sign:
|
||||
// - `Script_GetQuestLogRewardMoney` returns max(0, +money)
|
||||
// - `Script_GetQuestLogRequiredMoney` returns max(0, -money)
|
||||
constexpr int OFF_MONEY_DELTA = 0x28;
|
||||
// Level-cap reward money bonus — int32. Added to the reward when
|
||||
// the player's level is ≥ 60. Source: `Script_GetQuestLogRewardMoney`
|
||||
// does `cmp [player.unit + 0x70], 0x3C` (level >= 60) then
|
||||
// `add edx, [block + 0x2C]`.
|
||||
constexpr int OFF_LEVEL_CAP_BONUS_MONEY = 0x2C;
|
||||
// Reward spellID — int32. 0 = no spell reward. Source:
|
||||
// `Script_GetQuestLogRewardSpell` does `mov eax, [block + 0x30]` then
|
||||
// resolves the SpellIcon path and locale-applied name via `Spell.dbc`.
|
||||
// This is a spell *learned by the player* on completion (recipes,
|
||||
// taught spells); not to be confused with `OFF_SRC_ITEM_ID` which
|
||||
// is the item the questgiver hands over at accept-time.
|
||||
constexpr int OFF_REWARD_SPELL_ID = 0x30;
|
||||
// SrcItemId — itemID given to the player when the quest is accepted.
|
||||
// 0 = no source item. The questgiver hands this item over so the
|
||||
// player can complete the quest objective (e.g. "Read the Verdant
|
||||
// Sigil" gives item 9580; player uses it, then turns the same item
|
||||
// back in).
|
||||
//
|
||||
// Empirically derived (the offset was originally interpreted as
|
||||
// `RewardSpellCast` from one emulator wire-order variant, but a
|
||||
// real-world dump of quest 3120 stored `9580` here while the quest's
|
||||
// own item objective also references item 9580 — consistent with
|
||||
// SrcItemId semantics, not with a spell-cast-at-turn-in).
|
||||
constexpr int OFF_SRC_ITEM_ID = 0x34;
|
||||
// Quest flags — uint32. Bit definitions match modern WoW's
|
||||
// `QUEST_FLAGS_*` enum, of which only bit 3 (`0x08` SHARABLE) is
|
||||
// confirmed-tested by the engine (`Script_GetQuestLogPushable`'s
|
||||
// `test byte ptr [block + 0x38], 0x08`). Other bits are presumed
|
||||
// stored but unverified.
|
||||
constexpr int OFF_QUEST_FLAGS = 0x38;
|
||||
// Reward and choice item arrays — up to 4 reward and 6 choice
|
||||
// itemIDs stored inline (4 bytes each). 0 in any slot means
|
||||
// "no item in this slot". Used by `GetQuestLogItemLink` /
|
||||
// `GetQuestLogItemID`.
|
||||
//
|
||||
// Each itemID array is paired with a parallel uint32 count array
|
||||
// (the "how many of this item" reward). Layout in cache memory:
|
||||
// +0x3C RewardItems[4]
|
||||
// +0x4C RewardItemsCount[4]
|
||||
// +0x5C ChoiceItems[6]
|
||||
// +0x74 ChoiceItemsCount[6]
|
||||
// Verified from `Script_GetQuestLogRewardInfo` (`fild [block + slot*4 + 0x4C]`
|
||||
// for numItems) and `Script_GetQuestLogChoiceInfo` (`fild [block + slot*4 + 0x74]`).
|
||||
constexpr int OFF_REWARD_ITEMS = 0x3C;
|
||||
constexpr int OFF_REWARD_ITEMS_COUNT = 0x4C;
|
||||
constexpr int OFF_CHOICE_ITEMS = 0x5C;
|
||||
constexpr int OFF_CHOICE_ITEMS_COUNT = 0x74;
|
||||
constexpr int REWARD_COUNT = 4;
|
||||
constexpr int CHOICE_COUNT = 6;
|
||||
// Point-of-interest marker — server-supplied map coordinate hint
|
||||
// used by the questgiver objective panel to display "go here" arrows
|
||||
// in modern WoW. Set on quests that have a designated location;
|
||||
// 0/0/0 = no POI. Read sequentially by the serializer at
|
||||
// `0x006DBE5B..0x006DBE8E` using mixed integer + float wire methods.
|
||||
// **(hypothesis)**
|
||||
constexpr int OFF_POI_MAP_ID = 0x8C; // uint32
|
||||
constexpr int OFF_POI_X = 0x90; // float
|
||||
constexpr int OFF_POI_Y = 0x94; // float
|
||||
constexpr int OFF_POI_OPT = 0x98; // uint32 — display option, or doubles as title length
|
||||
constexpr int OFF_TITLE = 0x9C;
|
||||
// Inline narrative text — both locale-applied and printf-style
|
||||
// format-expanded by the engine's variable substituter at
|
||||
// `0x00506F70` (handles tokens like `$N` for player name, `$C` for
|
||||
// class, etc.). Source: `Script_GetQuestLogQuestText` reads both,
|
||||
// pushes `description` first then `objectives`.
|
||||
//
|
||||
// We deliberately expose the *raw* inline bytes, not the format-
|
||||
// expanded version, because the substituter wants a 1024-byte
|
||||
// scratch buffer and the substitutions (player name, class) belong
|
||||
// in the addon layer anyway. Addons can run `string.gsub` if they
|
||||
// need the runtime values, otherwise raw text is more useful for
|
||||
// guides / databases.
|
||||
constexpr int OFF_OBJECTIVES = 0x29C;
|
||||
constexpr int OFF_DESCRIPTION = 0xA9C;
|
||||
// Completion text — the "talk to me now" body shown on the
|
||||
// quest-reward UI panel after objectives are met. 0x200 (512) bytes
|
||||
// inline. Modern API exposes this as `GetQuestLogCompletionText()`.
|
||||
// Confirmed by the cache copy at `0x00562E1F..0x00562E36` (a 0x200-byte
|
||||
// memcpy from src+0x129C to dst+0x129C) and by the serializer at
|
||||
// `0x006DBEBE` (`lea eax, [edi+0x129C]` followed by the string-writer
|
||||
// call). Vanilla LeaderBoard reads from this offset for some override
|
||||
// cases but the engine never directly exposes the text to Lua —
|
||||
// `Script_GetQuestLogQuestText` returns description + objectives only.
|
||||
constexpr int OFF_COMPLETION_TEXT = 0x129C;
|
||||
// Objective arrays — four uint32[4] parallel slabs carrying
|
||||
// SMSG_QUEST_QUERY_RESPONSE's RequiredNPCOrGo / Count / Item / Count
|
||||
// fields verbatim. NPC-or-GO is **signed**: positive = creature
|
||||
@@ -66,5 +207,18 @@ constexpr int OFF_REQUIRED_NPC_OR_GO_COUNT = 0x14AC; // uint32[4]
|
||||
constexpr int OFF_REQUIRED_ITEM = 0x14BC; // uint32[4]
|
||||
constexpr int OFF_REQUIRED_ITEM_COUNT = 0x14CC; // uint32[4]
|
||||
constexpr int OBJECTIVE_COUNT = 4;
|
||||
// Per-objective override text — array of 4 inline char buffers,
|
||||
// 256 (0x100) bytes per entry, indexed in objective declaration
|
||||
// order (NPC/GO slots fill first, then item slots — same order the
|
||||
// LeaderBoard accessor enumerates). Empty string = no override
|
||||
// (engine auto-formats as `"<NPC name>: X/Y"`); otherwise the
|
||||
// engine displays the buffer verbatim. Confirmed by the
|
||||
// `shl ecx, 8; mov dl, [block + ecx + 0x14DC]` access pattern in
|
||||
// `Script_GetQuestLogLeaderBoard` and by the four sequential reads
|
||||
// in the packet serializer at `0x006DBF17..0x006DBF41` (0x14DC,
|
||||
// 0x15DC, 0x16DC, 0x17DC).
|
||||
constexpr int OFF_OBJECTIVE_TEXT = 0x14DC;
|
||||
constexpr int OBJECTIVE_TEXT_STRIDE = 0x100;
|
||||
constexpr int OBJECTIVE_TEXT_COUNT = 4;
|
||||
|
||||
} // namespace Quest::Cache
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
// 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_QuestLog.GetQuestDetails(questID)` — single accessor that returns
|
||||
// everything we know how to read from the quest static-info cache as a
|
||||
// Lua table. Powers quest-helper-style addons that want one round-trip
|
||||
// instead of N separate `C_QuestLog.GetTitleForQuestID` / etc. calls.
|
||||
//
|
||||
// Returns `nil` if the quest isn't cached. Caller should pair with
|
||||
// `C_QuestLog.RequestLoadQuestByID` + listen for `QUEST_DATA_LOAD_RESULT`.
|
||||
// All offsets read here come from the engine's own `Script_GetQuestLog*`
|
||||
// accessors — see `Cache.h` for the per-field derivation.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "quest/Cache.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Quest::Details {
|
||||
|
||||
namespace {
|
||||
|
||||
// QuestInfo.dbc — 12-row table holding the localized quest-type tags
|
||||
// ("Dungeon" / "Raid" / "Group" / "PvP" / "Elite"). Address pair from
|
||||
// `docs/DBCs.md`.
|
||||
constexpr uintptr_t VAR_QUESTINFO_DBC_RECORDS = 0x00C0D9CC;
|
||||
constexpr uintptr_t VAR_QUESTINFO_DBC_COUNT = 0x00C0D9D0;
|
||||
|
||||
const char *LookupQuestType(uint32_t row) {
|
||||
if (row == 0)
|
||||
return nullptr;
|
||||
const int count = *reinterpret_cast<const int *>(VAR_QUESTINFO_DBC_COUNT);
|
||||
if (count <= 0 || row > static_cast<uint32_t>(count))
|
||||
return nullptr;
|
||||
const uint8_t *const *records =
|
||||
*reinterpret_cast<const uint8_t *const *const *>(
|
||||
VAR_QUESTINFO_DBC_RECORDS);
|
||||
if (!records)
|
||||
return nullptr;
|
||||
const uint8_t *record = records[row];
|
||||
if (!record)
|
||||
return nullptr;
|
||||
const int localeIdx =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
if (localeIdx < 0 || localeIdx > 8)
|
||||
return nullptr;
|
||||
auto *names = reinterpret_cast<const char *const *>(record + 4);
|
||||
return names[localeIdx];
|
||||
}
|
||||
|
||||
// Pushes an array of `{id=itemID, count=N}` tables for each non-zero
|
||||
// reward slot. `ids[count]` and `counts[count]` are parallel arrays
|
||||
// in the cache block. Leaves exactly one table on top of the stack.
|
||||
void PushItemArray(void *L, const uint32_t *ids, const uint32_t *counts,
|
||||
int count) {
|
||||
Game::Lua::NewTable(L);
|
||||
int next = 1;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (ids[i] == 0)
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(next++));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "id", static_cast<double>(ids[i]));
|
||||
Game::Lua::SetFieldNumber(L, "count", static_cast<double>(counts[i]));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
}
|
||||
|
||||
// Per-objective override text — the 4-slot array at `+0x14DC` with
|
||||
// 256-byte stride. Index in objective-declaration order (NPC/GO first,
|
||||
// then item, same enumeration the LeaderBoard accessor walks).
|
||||
// Returns empty string for slots past the cache's 4-entry capacity
|
||||
// or for slots with an empty buffer (engine auto-formats those).
|
||||
const char *OverrideTextFor(const uint8_t *data, int objectiveSlot) {
|
||||
if (objectiveSlot < 0 || objectiveSlot >= Quest::Cache::OBJECTIVE_TEXT_COUNT)
|
||||
return "";
|
||||
return reinterpret_cast<const char *>(
|
||||
data + Quest::Cache::OFF_OBJECTIVE_TEXT +
|
||||
objectiveSlot * Quest::Cache::OBJECTIVE_TEXT_STRIDE);
|
||||
}
|
||||
|
||||
void PushRequirements(void *L, const uint8_t *data) {
|
||||
Game::Lua::NewTable(L);
|
||||
int next = 1;
|
||||
int overrideSlot = 0;
|
||||
|
||||
auto *npcOrGo = reinterpret_cast<const int32_t *>(
|
||||
data + Quest::Cache::OFF_REQUIRED_NPC_OR_GO);
|
||||
auto *npcOrGoCount = reinterpret_cast<const uint32_t *>(
|
||||
data + Quest::Cache::OFF_REQUIRED_NPC_OR_GO_COUNT);
|
||||
for (int i = 0; i < Quest::Cache::OBJECTIVE_COUNT; ++i) {
|
||||
const int32_t v = npcOrGo[i];
|
||||
if (v == 0)
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(next++));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "kind", v > 0 ? "monster" : "object");
|
||||
Game::Lua::SetFieldNumber(L, "id",
|
||||
static_cast<double>(v > 0 ? v : -v));
|
||||
Game::Lua::SetFieldNumber(L, "count",
|
||||
static_cast<double>(npcOrGoCount[i]));
|
||||
Game::Lua::SetFieldString(L, "text",
|
||||
OverrideTextFor(data, overrideSlot++));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
auto *items = reinterpret_cast<const uint32_t *>(
|
||||
data + Quest::Cache::OFF_REQUIRED_ITEM);
|
||||
auto *itemCounts = reinterpret_cast<const uint32_t *>(
|
||||
data + Quest::Cache::OFF_REQUIRED_ITEM_COUNT);
|
||||
for (int i = 0; i < Quest::Cache::OBJECTIVE_COUNT; ++i) {
|
||||
if (items[i] == 0)
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(next++));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "kind", "item");
|
||||
Game::Lua::SetFieldNumber(L, "id", static_cast<double>(items[i]));
|
||||
Game::Lua::SetFieldNumber(L, "count",
|
||||
static_cast<double>(itemCounts[i]));
|
||||
Game::Lua::SetFieldString(L, "text",
|
||||
OverrideTextFor(data, overrideSlot++));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
}
|
||||
|
||||
int __fastcall Script_GetQuestDetails(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_QuestLog.GetQuestDetails(questID)");
|
||||
return 0;
|
||||
}
|
||||
const int questID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (questID <= 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *data = Quest::Cache::Peek(static_cast<uint32_t>(questID));
|
||||
if (data == nullptr)
|
||||
return 0;
|
||||
|
||||
const auto u32 = [data](int off) {
|
||||
return *reinterpret_cast<const uint32_t *>(data + off);
|
||||
};
|
||||
const auto i32 = [data](int off) {
|
||||
return *reinterpret_cast<const int32_t *>(data + off);
|
||||
};
|
||||
const auto str = [data](int off) {
|
||||
return reinterpret_cast<const char *>(data + off);
|
||||
};
|
||||
|
||||
const auto f32 = [data](int off) {
|
||||
return *reinterpret_cast<const float *>(data + off);
|
||||
};
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
Game::Lua::SetFieldNumber(L, "questID", static_cast<double>(questID));
|
||||
Game::Lua::SetFieldString(L, "title", str(Quest::Cache::OFF_TITLE));
|
||||
Game::Lua::SetFieldNumber(L, "level",
|
||||
static_cast<double>(i32(Quest::Cache::OFF_LEVEL)));
|
||||
|
||||
if (const char *tag =
|
||||
LookupQuestType(u32(Quest::Cache::OFF_QUEST_INFO_ROW))) {
|
||||
if (*tag != '\0')
|
||||
Game::Lua::SetFieldString(L, "questType", tag);
|
||||
}
|
||||
|
||||
// Race / class / skill / timer / suggested-players gates aren't
|
||||
// here intentionally — vanilla 1.12's quest-query response leaves
|
||||
// those cache slots (+0x14..+0x24) zero-filled; the server enforces
|
||||
// those constraints and only ships quests to clients who could
|
||||
// accept them. Addons that need that data must source it from an
|
||||
// external scraped DB (pfQuest-style).
|
||||
const int32_t money = i32(Quest::Cache::OFF_MONEY_DELTA);
|
||||
Game::Lua::SetFieldNumber(L, "rewardMoney",
|
||||
money > 0 ? static_cast<double>(money) : 0.0);
|
||||
Game::Lua::SetFieldNumber(L, "requiredMoney",
|
||||
money < 0 ? static_cast<double>(-money) : 0.0);
|
||||
Game::Lua::SetFieldNumber(
|
||||
L, "rewardMoneyAtMaxLevel",
|
||||
static_cast<double>(i32(Quest::Cache::OFF_LEVEL_CAP_BONUS_MONEY)));
|
||||
Game::Lua::SetFieldNumber(
|
||||
L, "rewardSpellID",
|
||||
static_cast<double>(i32(Quest::Cache::OFF_REWARD_SPELL_ID)));
|
||||
Game::Lua::SetFieldNumber(
|
||||
L, "srcItemID",
|
||||
static_cast<double>(u32(Quest::Cache::OFF_SRC_ITEM_ID)));
|
||||
|
||||
const uint32_t flags = u32(Quest::Cache::OFF_QUEST_FLAGS);
|
||||
Game::Lua::SetFieldNumber(L, "questFlags", static_cast<double>(flags));
|
||||
Game::Lua::SetFieldBool(L, "isSharable", (flags & 0x08) != 0);
|
||||
|
||||
Game::Lua::SetFieldString(L, "description",
|
||||
str(Quest::Cache::OFF_DESCRIPTION));
|
||||
Game::Lua::SetFieldString(L, "objectives",
|
||||
str(Quest::Cache::OFF_OBJECTIVES));
|
||||
Game::Lua::SetFieldString(L, "completionText",
|
||||
str(Quest::Cache::OFF_COMPLETION_TEXT));
|
||||
|
||||
if (const uint32_t mapID = u32(Quest::Cache::OFF_POI_MAP_ID)) {
|
||||
Game::Lua::PushString(L, "poi");
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "mapID", static_cast<double>(mapID));
|
||||
Game::Lua::SetFieldNumber(L, "x", static_cast<double>(f32(Quest::Cache::OFF_POI_X)));
|
||||
Game::Lua::SetFieldNumber(L, "y", static_cast<double>(f32(Quest::Cache::OFF_POI_Y)));
|
||||
Game::Lua::SetFieldNumber(L, "opt",
|
||||
static_cast<double>(u32(Quest::Cache::OFF_POI_OPT)));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
Game::Lua::PushString(L, "rewardItems");
|
||||
PushItemArray(L,
|
||||
reinterpret_cast<const uint32_t *>(
|
||||
data + Quest::Cache::OFF_REWARD_ITEMS),
|
||||
reinterpret_cast<const uint32_t *>(
|
||||
data + Quest::Cache::OFF_REWARD_ITEMS_COUNT),
|
||||
Quest::Cache::REWARD_COUNT);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
|
||||
Game::Lua::PushString(L, "choiceItems");
|
||||
PushItemArray(L,
|
||||
reinterpret_cast<const uint32_t *>(
|
||||
data + Quest::Cache::OFF_CHOICE_ITEMS),
|
||||
reinterpret_cast<const uint32_t *>(
|
||||
data + Quest::Cache::OFF_CHOICE_ITEMS_COUNT),
|
||||
Quest::Cache::CHOICE_COUNT);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
|
||||
Game::Lua::PushString(L, "requirements");
|
||||
PushRequirements(L, data);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_QuestLog", "GetQuestDetails",
|
||||
&Script_GetQuestDetails);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Quest::Details
|
||||
Reference in New Issue
Block a user