mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 20:37:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7ffe1c704 | ||
|
|
9f75737882 | ||
|
|
10d5790975 | ||
|
|
b571483c5f | ||
|
|
0a79016b0f | ||
|
|
3c4f24192a | ||
|
|
93b4dc5db8 | ||
|
|
d786c607b8 | ||
|
|
598c66894e | ||
|
|
75b4ea3863 | ||
|
|
5f66e381d2 | ||
|
|
4dd892297d | ||
|
|
51cb7a5674 | ||
|
|
cc0bf769c4 | ||
|
|
ad13440743 | ||
|
|
999a5846c4 |
@@ -1,9 +1,9 @@
|
||||
name: Release
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
branches: ['**']
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -16,12 +16,8 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# Stamp the addon's TOC version from the tag (drops the leading `v`)
|
||||
# BEFORE CMake configure, so the embed step picks up the stamped TOC.
|
||||
# The DLL bundles AddOns/!!!ClassicAPI/ at build time — see the
|
||||
# `embed_addon` custom command in CMakeLists.txt and the file-read
|
||||
# hook in src/addons/Embedded.cpp.
|
||||
- name: Stamp embedded TOC version
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = '${{ github.ref_name }}' -replace '^v',''
|
||||
@@ -29,13 +25,26 @@ jobs:
|
||||
(Get-Content $toc) -replace '^## Version:.*', "## Version: $version" |
|
||||
Set-Content $toc
|
||||
|
||||
- name: Configure
|
||||
- name: Configure (tag)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: cmake -B build -A Win32 "-DCLASSICAPI_TAG=${{ github.ref_name }}"
|
||||
|
||||
- name: Configure (branch)
|
||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||
run: cmake -B build -A Win32
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build --config Release
|
||||
|
||||
- name: Upload artifact
|
||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ClassicAPI-${{ github.sha }}
|
||||
path: build/Release/ClassicAPI.dll
|
||||
|
||||
- name: Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: build/Release/ClassicAPI.dll
|
||||
@@ -1,10 +0,0 @@
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
|
||||
ITEMS_EQUIPPED = "%d |4item:items; equipped";
|
||||
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
|
||||
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
|
||||
ITEM_MISSING = "%s missing";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
@@ -1,5 +1,3 @@
|
||||
if GetLocale() ~= "enUS" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/..\FrameXML\UI.xsd">
|
||||
<Script file="deDE.lua"/>
|
||||
<Script file="enUS.lua"/>
|
||||
<Script file="enUS.lua"/>
|
||||
<Script file="deDE.lua"/>
|
||||
<Script file="esES.lua"/>
|
||||
<Script file="frFR.lua"/>
|
||||
<Script file="ruRU.lua"/>
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
-- AoE Loot Test
|
||||
--
|
||||
-- /aoeloot opens a picker window listing every item in every lootable
|
||||
-- corpse within click range. Clicking a row hands the (guid, itemID)
|
||||
-- pair to C_Loot.LootUnitItem and re-scans so the picker refreshes
|
||||
-- with whatever's left.
|
||||
--
|
||||
-- Known visual: each scan opens / closes loot windows in rapid
|
||||
-- succession, so the Blizzard LootFrame flickers during the scan. This
|
||||
-- addon intentionally doesn't suppress that — the goal is to exercise
|
||||
-- the C++ surface, not to ship polished UI.
|
||||
|
||||
local MAX_ROWS = 24
|
||||
local RESCAN_DELAY = 0.3 -- seconds; lets LootUnitItem complete before re-scan
|
||||
|
||||
-- ---------- UI scaffolding ----------
|
||||
|
||||
-- Frame geometry. Height is recomputed dynamically per scan so the
|
||||
-- window only takes up as much screen real estate as the result count
|
||||
-- needs — see `ResizeForRows`.
|
||||
local FRAME_WIDTH = 320
|
||||
local HEADER_HEIGHT = 64 -- title + status + insets; minimum frame height
|
||||
local ROW_HEIGHT = 22
|
||||
|
||||
local frame = CreateFrame("Frame", "AoELootTestFrame", UIParent)
|
||||
frame:SetWidth(FRAME_WIDTH)
|
||||
frame:SetHeight(HEADER_HEIGHT)
|
||||
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||
frame:SetBackdrop({
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tile = true, tileSize = 32, edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
})
|
||||
frame:SetMovable(true)
|
||||
frame:EnableMouse(true)
|
||||
frame:RegisterForDrag("LeftButton")
|
||||
-- Vanilla 1.12 fires script handlers with the frame set as the global
|
||||
-- `this`, not as a `self` arg to the function. Passing `frame.StartMoving`
|
||||
-- directly (the modern shortcut) breaks because the C method has no
|
||||
-- `self` to operate on. Wrap in closures so `this:method()` resolves
|
||||
-- correctly inside the handler.
|
||||
frame:SetScript("OnDragStart", function() this:StartMoving() end)
|
||||
frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
|
||||
frame:Hide()
|
||||
|
||||
local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||
title:SetPoint("TOP", frame, "TOP", 0, -16)
|
||||
title:SetText("AoE Loot")
|
||||
|
||||
local status = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
||||
status:SetPoint("TOPLEFT", frame, "TOPLEFT", 18, -36)
|
||||
status:SetText("")
|
||||
|
||||
local closeBtn = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
|
||||
closeBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -6)
|
||||
|
||||
-- ---------- Row pool ----------
|
||||
|
||||
local rows = {}
|
||||
|
||||
local function CreateRow(parent, idx)
|
||||
local row = CreateFrame("Button", nil, parent)
|
||||
row:SetWidth(290)
|
||||
row:SetHeight(20)
|
||||
row:SetPoint("TOPLEFT", parent, "TOPLEFT", 15,
|
||||
-54 - (idx - 1) * ROW_HEIGHT)
|
||||
|
||||
local icon = row:CreateTexture(nil, "ARTWORK")
|
||||
icon:SetWidth(18); icon:SetHeight(18)
|
||||
icon:SetPoint("LEFT", row, "LEFT", 0, 0)
|
||||
row.icon = icon
|
||||
|
||||
local text = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||
text:SetPoint("LEFT", icon, "RIGHT", 6, 0)
|
||||
text:SetPoint("RIGHT", row, "RIGHT", 0, 0)
|
||||
text:SetJustifyH("LEFT")
|
||||
row.text = text
|
||||
|
||||
local hl = row:CreateTexture(nil, "HIGHLIGHT")
|
||||
hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
|
||||
hl:SetBlendMode("ADD")
|
||||
hl:SetAllPoints(row)
|
||||
|
||||
-- Tooltip on hover. The engine's native `SetHyperlink` does a
|
||||
-- literal `SStrCmpI` of the input's first 5 chars against the
|
||||
-- string `"item:"` at `.rdata` VA `0x00842D60` — anything that
|
||||
-- starts with `|c...` (full link with color) or `|Hitem` (full
|
||||
-- link without color) fails the prefix check with "Unknown link
|
||||
-- type". Only the bare payload form survives.
|
||||
--
|
||||
-- We extract the payload from the engine's full link rather than
|
||||
-- composing `item:ID:0:0:0` ourselves, so per-instance fields
|
||||
-- (enchant / random-suffix / unique) survive round-trip and
|
||||
-- "Stringy Wolf Meat of the Bear" tooltips correctly instead of
|
||||
-- collapsing to the un-suffixed itemID.
|
||||
row:SetScript("OnEnter", function()
|
||||
if this.payload then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetHyperlink(this.payload)
|
||||
GameTooltip:Show()
|
||||
end
|
||||
end)
|
||||
row:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
|
||||
row:Hide()
|
||||
return row
|
||||
end
|
||||
|
||||
for i = 1, MAX_ROWS do
|
||||
rows[i] = CreateRow(frame, i)
|
||||
end
|
||||
|
||||
-- ---------- Render ----------
|
||||
|
||||
-- Tail of a GUID string — last 4 hex chars of the low dword. Lets the
|
||||
-- user tell apart two rows of "Stringy Wolf Meat x1" coming from
|
||||
-- different corpses, without occupying a whole column with the full
|
||||
-- 16-digit GUID.
|
||||
local function ShortGuidSuffix(guid)
|
||||
return "[.." .. string.sub(guid, -4) .. "]"
|
||||
end
|
||||
|
||||
-- Extract the `item:ID:enchant:suffix:unique` payload out of the
|
||||
-- engine's full link string (`|cff...|Hitem:...|h[Name]|h|r`).
|
||||
-- Vanilla `SetHyperlink` requires the input to start literally with
|
||||
-- `"item:"` — so we can't pass the full link, but we also can't drop
|
||||
-- the per-instance fields (random suffix is what distinguishes "of
|
||||
-- the Bear" from "of the Eagle" on otherwise identical itemIDs).
|
||||
-- Returns the payload, or nil if extraction fails.
|
||||
local function PayloadFromLink(link)
|
||||
if not link then return nil end
|
||||
return string.match(link, "|H(item:[^|]+)|h")
|
||||
end
|
||||
|
||||
local function ClearRows()
|
||||
for i = 1, MAX_ROWS do
|
||||
rows[i]:Hide()
|
||||
rows[i]:SetScript("OnClick", nil)
|
||||
rows[i].payload = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function PopulateRows(results)
|
||||
local idx = 0
|
||||
for _, corpse in ipairs(results) do
|
||||
for _, item in ipairs(corpse.items) do
|
||||
idx = idx + 1
|
||||
if idx > MAX_ROWS then break end
|
||||
local row = rows[idx]
|
||||
local name, _, _, _, _, _, _, _, icon = GetItemInfo(item.itemID)
|
||||
row.icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark")
|
||||
local label = name or ("Item #" .. item.itemID)
|
||||
if item.count and item.count > 1 then
|
||||
label = label .. " x" .. item.count
|
||||
end
|
||||
label = label .. " " .. ShortGuidSuffix(corpse.guid)
|
||||
row.text:SetText(label)
|
||||
row.payload = PayloadFromLink(item.link) -- for OnEnter tooltip
|
||||
local guid, itemID = corpse.guid, item.itemID
|
||||
row:SetScript("OnClick", function()
|
||||
C_Loot.LootUnitItem(guid, itemID)
|
||||
C_Timer.After(RESCAN_DELAY, function()
|
||||
if frame:IsShown() then C_Loot.ScanNearbyLoot() end
|
||||
end)
|
||||
end)
|
||||
row:Show()
|
||||
end
|
||||
if idx > MAX_ROWS then break end
|
||||
end
|
||||
for i = idx + 1, MAX_ROWS do rows[i]:Hide() end
|
||||
local shown
|
||||
if idx == 0 then
|
||||
status:SetText("Nothing lootable in range.")
|
||||
shown = 0
|
||||
elseif idx > MAX_ROWS then
|
||||
status:SetText(MAX_ROWS .. "+ items (showing first " .. MAX_ROWS .. ")")
|
||||
shown = MAX_ROWS
|
||||
else
|
||||
status:SetText(idx .. " item(s) available")
|
||||
shown = idx
|
||||
end
|
||||
frame:SetHeight(HEADER_HEIGHT + shown * ROW_HEIGHT)
|
||||
end
|
||||
|
||||
-- ---------- Scan trigger ----------
|
||||
|
||||
local function StartScan()
|
||||
status:SetText("Scanning...")
|
||||
ClearRows()
|
||||
frame:SetHeight(HEADER_HEIGHT) -- collapse while we wait for results
|
||||
if not C_Loot.ScanNearbyLoot() then
|
||||
status:SetText("Scan busy or not in world.")
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------- Event wiring ----------
|
||||
|
||||
local evt = CreateFrame("Frame")
|
||||
evt:RegisterEvent("LOOT_SCAN_COMPLETED")
|
||||
evt:SetScript("OnEvent", function()
|
||||
-- Race guard: a delayed scan may complete after the user closed
|
||||
-- the picker. Just drop the result rather than re-showing.
|
||||
if not frame:IsShown() then return end
|
||||
PopulateRows(C_Loot.GetLastScanResults())
|
||||
end)
|
||||
|
||||
-- ---------- Slash command ----------
|
||||
|
||||
RegisterNewSlashCommand(function()
|
||||
if frame:IsShown() then
|
||||
StartScan() -- already open → just refresh
|
||||
else
|
||||
frame:Show()
|
||||
StartScan()
|
||||
end
|
||||
end, "aoeloot", "aoe")
|
||||
@@ -0,0 +1,7 @@
|
||||
## Interface: 11200
|
||||
## Title: AoE Loot Test
|
||||
## Notes: Test bed for ClassicAPI's C_Loot.ScanNearbyLoot / LootUnitItem. /aoeloot
|
||||
## Dependencies: !!!ClassicAPI
|
||||
## Version: DEV
|
||||
|
||||
AoELootTest.lua
|
||||
@@ -25,8 +25,9 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
|
||||
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [Coroutine](docs/API.md#coroutine) | `coroutine.create`, `coroutine.resume`, `coroutine.yield`, `coroutine.status`, `coroutine.wrap` |
|
||||
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` (sets persist to `WTF\Account\...\ClassicAPI_EquipmentSets.txt`) |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
@@ -38,7 +39,9 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit` |
|
||||
| [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` |
|
||||
@@ -98,6 +101,7 @@ functions, just behavior the stock 1.12 engine didn't have. See the
|
||||
| `GLOBAL_MOUSE_UP` | `button` |
|
||||
| `HEARTHSTONE_BOUND` | *(none)* |
|
||||
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
|
||||
| `LOOT_SCAN_COMPLETED` | *(none)* |
|
||||
| `MODIFIER_STATE_CHANGED` | `keyName, down` |
|
||||
| `NAME_PLATE_CREATED` | `nameplateFrame` |
|
||||
| `NAME_PLATE_UNIT_ADDED` | `unitToken` ("nameplateN") |
|
||||
|
||||
+681
-11
@@ -46,6 +46,14 @@ build instructions.
|
||||
- [`C_Container.SwapItems(srcBag, srcSlot, dstBag, dstSlot)`](#c_containerswapitemssrcbag-srcslot-dstbag-dstslot)
|
||||
- [`C_Container.MoveItem(srcBag, srcSlot, dstBag, dstSlot, count)`](#c_containermoveitemsrcbag-srcslot-dstbag-dstslot-count)
|
||||
|
||||
- [Coroutine](#coroutine)
|
||||
- [`coroutine.create(fn)`](#coroutinecreatefn)
|
||||
- [`coroutine.resume(co, ...)`](#coroutineresumeco-)
|
||||
- [`coroutine.yield(...)`](#coroutineyield)
|
||||
- [`coroutine.status(co)`](#coroutinestatusco)
|
||||
- [`coroutine.wrap(fn)`](#coroutinewrapfn)
|
||||
- [Async pattern (RunAsync + C_Timer.After)](#async-pattern-runasync--c_timerafter)
|
||||
|
||||
- [CVar](#cvar)
|
||||
- [`C_CVar.GetCVarBool(cvar)`](#c_cvargetcvarboolcvar)
|
||||
|
||||
@@ -80,11 +88,13 @@ build instructions.
|
||||
- [`EQUIPMENT_SWAP_PENDING` event](#equipment_swap_pending-event)
|
||||
- [`EQUIPMENT_SWAP_FINISHED` event](#equipment_swap_finished-event)
|
||||
- [`FACTION_STANDING_CHANGED` event](#faction_standing_changed-event)
|
||||
- [`LOOT_SCAN_COMPLETED` event](#loot_scan_completed-event)
|
||||
- [`MODIFIER_STATE_CHANGED` event](#modifier_state_changed-event)
|
||||
- [`NAME_PLATE_CREATED` / `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` events](#name_plate_created--name_plate_unit_added--name_plate_unit_removed-events)
|
||||
- [`PLAYER_FOCUS_CHANGED` event](#player_focus_changed-event)
|
||||
- [`QUEST_ACCEPTED` event](#quest_accepted-event)
|
||||
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
|
||||
- [`UNIT_FACTION` event (fire-coverage fix)](#unit_faction-event-fire-coverage-fix)
|
||||
- [`UPDATE_SHAPESHIFT_FORM` event](#update_shapeshift_form-event)
|
||||
|
||||
- [Expansion](#expansion)
|
||||
@@ -162,6 +172,7 @@ build instructions.
|
||||
- [`C_Item.EquipItemByName(itemInfo [, dstSlot])`](#c_itemequipitembynameiteminfo--dstslot)
|
||||
- [`C_Item.GetCurrentItemLevel(itemLocation)` / `C_Item.GetDetailedItemLevelInfo(item)`](#c_itemgetcurrentitemlevelitemlocation--c_itemgetdetaileditemlevelinfoitem)
|
||||
- [`C_Item.GetItemCount(itemInfo, [includeBank], [includeUses])`](#c_itemgetitemcountiteminfo-includebank-includeuses)
|
||||
- [`C_Item.GetItemData(itemLocation)` / `C_Item.GetItemDataByID(item)`](#c_itemgetitemdataitemlocation--c_itemgetitemdatabyiditem)
|
||||
- [`C_Item.GetItemFamily(item)`](#c_itemgetitemfamilyitem)
|
||||
- [`C_Item.GetItemGUID(itemLocation)`](#c_itemgetitemguiditemlocation)
|
||||
- [`C_Item.GetItemID(itemLocation)`](#c_itemgetitemiditemlocation)
|
||||
@@ -200,12 +211,24 @@ build instructions.
|
||||
- [`GetItemIcon(itemID)` / `C_Item.GetItemIcon(itemLocation)` / `C_Item.GetItemIconByID(item)`](#getitemiconitemid--c_itemgetitemiconitemlocation--c_itemgetitemiconbyiditem)
|
||||
- [`OffhandHasWeapon()`](#offhandhasweapon)
|
||||
|
||||
- [Loot](#loot)
|
||||
- [`C_Loot.GetNearbyLootableUnits()`](#c_lootgetnearbylootableunits)
|
||||
- [`C_Loot.LootUnit(guid)`](#c_lootlootunitguid)
|
||||
- [`C_Loot.LootUnitItem(guid, itemID)`](#c_lootlootunititemguid-itemid)
|
||||
- [`C_Loot.ScanNearbyLoot()`](#c_lootscannearbyloot)
|
||||
- [`C_Loot.IsScanInProgress()`](#c_lootisscaninprogress)
|
||||
- [`C_Loot.GetLastScanResults()`](#c_lootgetlastscanresults)
|
||||
|
||||
- [Macros](#macros)
|
||||
- [Numeric spellIDs in `/cast` and `CastSpellByName`](#numeric-spellids-in-cast-and-castspellbyname)
|
||||
- [`CastSpellNoToggle` as a macro cast line](#castspellnotoggle-as-a-macro-cast-line)
|
||||
- [`GetMacroSpell(macroSlot)`](#getmacrospellmacroslot)
|
||||
- [`GetMacroIcons` / `GetMacroItemIcons` / `GetLooseMacroIcons` / `GetLooseMacroItemIcons`](#getmacroicons--getmacroitemicons--getloosemacroicons--getloosemacroitemicons)
|
||||
|
||||
- [Mail](#mail)
|
||||
- [`GetSendMailItemLink([attachmentIndex])`](#getsendmailitemlinkattachmentindex)
|
||||
- [`GetInboxItemLink(messageIndex[, attachmentIndex])`](#getinboxitemlinkmessageindex-attachmentindex)
|
||||
|
||||
- [Map](#map)
|
||||
- [`C_Map.GetBestMapForUnit(unitToken)`](#c_mapgetbestmapforunitunittoken)
|
||||
|
||||
@@ -1212,6 +1235,146 @@ constraint as `SwapItems`.
|
||||
|
||||
Send is fire-and-forget (same as `SwapItems`).
|
||||
|
||||
## Coroutine
|
||||
|
||||
Restores Lua 5.0's stripped `coroutine.*` library. The C-level
|
||||
coroutine machinery (`lua_resume`, `lua_yield`, `lua_newthread`,
|
||||
and the `thread` type at tag 8) is linked into the engine, but
|
||||
the script-facing library was never registered as a global table.
|
||||
ClassicAPI rewires the standard five entries on `coroutine`,
|
||||
matching Lua 5.0 semantics with two WoW-specific quirks called
|
||||
out below.
|
||||
|
||||
### `coroutine.create(fn)`
|
||||
|
||||
Creates a new coroutine with `fn` as its body and returns the
|
||||
resulting `thread`. `fn` must be a **Lua** function — passing a
|
||||
C function (any engine global like `GetTime` or `UnitName`)
|
||||
raises `bad argument #1 to 'create' (Lua function expected)`.
|
||||
|
||||
The new coroutine starts suspended; nothing runs until
|
||||
`coroutine.resume` is called on it. The args passed to the first
|
||||
resume become the body's arguments.
|
||||
|
||||
### `coroutine.resume(co, ...)`
|
||||
|
||||
Resumes a suspended coroutine, passing the trailing args to either
|
||||
the body (first resume) or back as the return values of the
|
||||
`coroutine.yield` that paused the coroutine.
|
||||
|
||||
Returns:
|
||||
|
||||
- `true, ...values` on success, where `values` are whatever the
|
||||
coroutine yielded (mid-execution) or returned (on completion).
|
||||
- `false, errMsg` on error. Two error sources:
|
||||
- Coroutine isn't suspended — e.g. resuming a dead one;
|
||||
`errMsg` is a fixed string like `"cannot resume dead coroutine"`.
|
||||
- The body hit a Lua VM error; `errMsg` is the formatted error
|
||||
string with file:line prefix.
|
||||
|
||||
**Caveat — `error()` doesn't propagate.** WoW replaces Lua's
|
||||
`error()` global with a soft handler that calls
|
||||
`geterrorhandler()(msg)` and returns normally instead of
|
||||
longjmp'ing. So `error("msg")` inside a coroutine body looks like
|
||||
a clean return to `resume` — the message gets printed to chat as
|
||||
a side effect and `resume` returns `true` with no values. Real VM
|
||||
errors (calling `nil`, indexing `nil`, etc.) still propagate
|
||||
through the standard `false, msg` path.
|
||||
|
||||
### `coroutine.yield(...)`
|
||||
|
||||
Suspends the current coroutine. Whatever's passed to `yield`
|
||||
becomes the return values of the matching `coroutine.resume`
|
||||
call. When the coroutine is next resumed, the args passed to
|
||||
`resume` become the return values of `yield`.
|
||||
|
||||
Raises an error if called outside a coroutine
|
||||
(`attempt to yield across metamethod/C-call boundary`) or from
|
||||
a C function (`cannot yield a C function`).
|
||||
|
||||
### `coroutine.status(co)`
|
||||
|
||||
Returns the coroutine's state as a string:
|
||||
|
||||
| Value | Meaning |
|
||||
|-------|---------|
|
||||
| `"running"` | `co` is the currently-running coroutine (i.e. the same thread that called `status`). |
|
||||
| `"suspended"` | `co` is paused — either initial (never resumed) or mid-yield. |
|
||||
| `"dead"` | `co`'s body has returned or errored. |
|
||||
|
||||
The standard Lua `"normal"` state — a coroutine that resumed a
|
||||
deeper coroutine and is now an ancestor on the call chain — is
|
||||
collapsed into `"suspended"`. Detecting it cleanly requires
|
||||
walking the global thread list, and the case is near-useless for
|
||||
addon-level logic.
|
||||
|
||||
### `coroutine.wrap(fn)`
|
||||
|
||||
Equivalent to `coroutine.create(fn)` followed by returning a
|
||||
closure that calls `coroutine.resume(co, ...)` on each invocation
|
||||
— but propagates errors as raised exceptions (via `lua_error`)
|
||||
instead of returning `false, msg`. Lets you use a coroutine as
|
||||
if it were an ordinary function:
|
||||
|
||||
```lua
|
||||
local gen = coroutine.wrap(function()
|
||||
for i = 1, 5 do coroutine.yield(i) end
|
||||
end)
|
||||
print(gen()) -- 1
|
||||
print(gen()) -- 2
|
||||
print(gen()) -- 3
|
||||
```
|
||||
|
||||
Calling the wrapped function after the body has completed raises
|
||||
`cannot resume dead coroutine`. The canonical "iterator that
|
||||
terminates cleanly through `for ... in`" idiom is to yield an
|
||||
explicit `nil` at the end so the for-loop's nil sentinel fires:
|
||||
|
||||
```lua
|
||||
local function range(from, to)
|
||||
return coroutine.wrap(function()
|
||||
for i = from, to do coroutine.yield(i) end
|
||||
coroutine.yield(nil)
|
||||
end)
|
||||
end
|
||||
|
||||
for n in range(1, 5) do print(n) end
|
||||
```
|
||||
|
||||
### Async pattern (`RunAsync` + `C_Timer.After`)
|
||||
|
||||
Coroutines pair naturally with `C_Timer.After` for chunked
|
||||
"do some work, wait a frame, do more" loops. The canonical
|
||||
scheduler:
|
||||
|
||||
```lua
|
||||
local function RunAsync(fn)
|
||||
local co = coroutine.create(fn)
|
||||
local function step()
|
||||
local ok, delay = coroutine.resume(co)
|
||||
if not ok or coroutine.status(co) == "dead" then return end
|
||||
C_Timer.After(delay or 0, step)
|
||||
end
|
||||
step()
|
||||
end
|
||||
|
||||
RunAsync(function()
|
||||
for i = 1, 100 do
|
||||
DoExpensiveWork(i)
|
||||
coroutine.yield(0) -- next frame
|
||||
end
|
||||
coroutine.yield(0.5) -- pause half a second
|
||||
DoFinalWork()
|
||||
end)
|
||||
```
|
||||
|
||||
`coroutine.yield(n)` returns `n` to the scheduler, which schedules
|
||||
the next `step` after that many seconds. `yield(0)` is the
|
||||
"resume on the next frame" form (since `C_Timer.After(0, ...)`
|
||||
fires on the next OnUpdate tick). `RunNextFrame(cb)` in
|
||||
[`AddOns/!!!ClassicAPI/Util/FunctionUtil.lua`](../AddOns/!!!ClassicAPI/Util/FunctionUtil.lua)
|
||||
wraps the same primitive for the non-coroutine case.
|
||||
|
||||
## CVar
|
||||
|
||||
### `C_CVar.GetCVarBool(cvar)`
|
||||
@@ -1821,6 +1984,29 @@ forwarding, which [`C_Reputation.GetLastStandingChange`](#c_reputationgetlaststa
|
||||
exposes — so addons can read the structured payload from inside the
|
||||
chat event without re-parsing the localized string.
|
||||
|
||||
### `LOOT_SCAN_COMPLETED` event
|
||||
|
||||
Fires (no payload) once a [`C_Loot.ScanNearbyLoot()`](#c_lootscannearbyloot)
|
||||
call has finished walking every reachable corpse. Handler should call
|
||||
[`C_Loot.GetLastScanResults()`](#c_lootgetlastscanresults) to read the
|
||||
collected `{ guid, coin, items }` array.
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("LOOT_SCAN_COMPLETED")
|
||||
f:SetScript("OnEvent", function()
|
||||
local results = C_Loot.GetLastScanResults()
|
||||
-- ... process results
|
||||
end)
|
||||
C_Loot.ScanNearbyLoot()
|
||||
```
|
||||
|
||||
Fires exactly once per `ScanNearbyLoot()` call regardless of
|
||||
outcome — even if every corpse timed out and the results table is
|
||||
empty. Doesn't fire spuriously for scans triggered by other addons
|
||||
(only one scan runs at a time; concurrent attempts return `false`
|
||||
from `ScanNearbyLoot`). See the Loot section for the full scan flow.
|
||||
|
||||
### `MODIFIER_STATE_CHANGED` event
|
||||
|
||||
Fires on every modifier-key press and release with `(key, down)`:
|
||||
@@ -2022,6 +2208,75 @@ quest invalidated, etc.). Only fires on a real successful turn-in.
|
||||
> done, quest removed from log). Hooking the packet handler gives
|
||||
> us the same signal modern WoW's event uses.
|
||||
|
||||
### `UNIT_FACTION` event (fire-coverage fix)
|
||||
|
||||
`UNIT_FACTION` already exists in the vanilla event table — addons can
|
||||
register for it — but the 1.12 engine **never fires it** on several
|
||||
paths where modern WoW (3.3.5+) does. We restore the modern firing
|
||||
semantics so addons that observe `UNIT_FACTION` stop missing state
|
||||
changes.
|
||||
|
||||
Payload is unchanged from modern: `arg1` is the unit token string,
|
||||
always `"player"` for these paths (you can only toggle / sync rep on
|
||||
the local player).
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("UNIT_FACTION")
|
||||
f:SetScript("OnEvent", function()
|
||||
if event == "UNIT_FACTION" and arg1 == "player" then
|
||||
RefreshFactionUI()
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
**Paths that now fire `UNIT_FACTION("player")`:**
|
||||
|
||||
| Path | Trigger | Detection |
|
||||
|------|---------|-----------|
|
||||
| `FactionToggleAtWar` | Lua call to toggle AT_WAR | Slot's flag byte changed (skips PEACE_FORCED no-ops, looting-blocked attempts, standing-blocked attempts) |
|
||||
| `SetFactionInactive` / `SetFactionActive` | Lua call to toggle INACTIVE | Slot's flag byte changed |
|
||||
| `SMSG_SET_FACTION_STANDINGS` | Server rep-change push (kills, quest turn-ins, etc.) | Any byte in the 64-entry flag array changed — catches AT_WAR side-effect flips when standing crosses the -3000 threshold |
|
||||
| `SMSG_SET_FACTION_ATWAR` | Server force-change of AT_WAR (faction reset, server-side state push) | Unconditional after byte write |
|
||||
| `SMSG_INITIALIZE_FACTIONS` | Server initial faction sync at login | Unconditional after table populated |
|
||||
|
||||
The last one fixes the case where addons relying on `UNIT_FACTION`
|
||||
(rather than `PLAYER_LOGIN` / `VARIABLES_LOADED`) miss the initial
|
||||
load entirely on vanilla.
|
||||
|
||||
**Not yet covered:**
|
||||
- Unit-faction-template changes for *non-player* units (mind control,
|
||||
charm, server-side faction scripts). Modern WoW fires
|
||||
`UNIT_FACTION` on the affected unit token; the vanilla broad helper
|
||||
isn't hooked here yet. Mostly affects nameplate / threat addons in
|
||||
rare encounter mechanics.
|
||||
|
||||
**Implementation notes**
|
||||
|
||||
`FactionToggleAtWar` / `SetFactionInactive` / `SetFactionActive`'s
|
||||
inner setters (`FUN_004D5FD0`, `FUN_004D60F0`) write to the rep slot's
|
||||
flag byte and send the corresponding CMSG, but never call the
|
||||
engine's "fire UNIT_FACTION on this unit" dispatcher. 3.3.5 added a
|
||||
`FUN_0071F8F0(player, 0)` call to both paths whose inner
|
||||
`FUN_0060BF10(playerGUID, UNIT_FACTION_id)` broadcasts the event for
|
||||
every unit token referencing the local player.
|
||||
|
||||
Since `FactionToggleAtWar` only ever runs for the player and the only
|
||||
unit token resolving to the player is `"player"`, we fire
|
||||
`UNIT_FACTION("player")` directly via the engine's printf-style
|
||||
dispatcher (`FUN_FIRE_EVENT`) — same observable result without
|
||||
re-deriving the vanilla broad helper. We resolve `UNIT_FACTION`'s
|
||||
event-table slot lazily by name (`Event::Custom::LookupByName`) so we
|
||||
stay correct against any DLL combination that reshuffles the table.
|
||||
|
||||
For the two SMSG-bound flag-flip paths (`SetFactionStandings`,
|
||||
`SetFactionInactive`-but-from-server), we snapshot the 64-entry rep
|
||||
slot flag-byte array before the original handler runs and compare
|
||||
after — single fire if any byte changed. The bulk snapshot is 64
|
||||
byte reads (cheap) and naturally handles the case where one packet
|
||||
carries multiple rep updates whose threshold crossings ripple AT_WAR
|
||||
state across unrelated slots.
|
||||
|
||||
### `UPDATE_SHAPESHIFT_FORM` event
|
||||
|
||||
Fires whenever the local player's shapeshift form changes — entering
|
||||
@@ -3568,6 +3823,143 @@ directly off the CGItem's m_objectFields descriptor at +0x20
|
||||
Equivalent to the legacy global `GetItemCount` (since 3.0) and the
|
||||
modern `C_Item.GetItemCount` introduced in 10.x.
|
||||
|
||||
### `C_Item.GetItemData(itemLocation)` / `C_Item.GetItemDataByID(item)`
|
||||
|
||||
ClassicAPI-only kitchen-sink reader: returns a single table with every
|
||||
field we can extract from the cached `ItemStats_C` record, so addons
|
||||
that need more than a couple of fields don't have to chain a dozen
|
||||
`C_Item.*` calls. Saves both stack churn and the cost of redoing the
|
||||
cache lookup for every accessor.
|
||||
|
||||
```
|
||||
data = C_Item.GetItemDataByID(item) -- numeric id / "item:NNN" / chat link
|
||||
data = C_Item.GetItemData(itemLocation) -- {bagID,slotIndex} or {equipmentSlotIndex}
|
||||
```
|
||||
|
||||
Returns `nil` if the input doesn't resolve to a valid itemID or the
|
||||
item isn't cached yet. Does **not** warm the cache — same passive-
|
||||
reader contract as
|
||||
[`C_Item.GetItemInfoInstant`](#c_itemgetiteminfoinstantitem).
|
||||
Callers that need to wait for cache fill should call
|
||||
[`C_Item.RequestLoadItemDataByID`](#c_itemrequestloaditemdatabyiditem--c_itemrequestloaditemdataitemlocation)
|
||||
and listen for `ITEM_DATA_LOAD_RESULT`.
|
||||
|
||||
Returned table:
|
||||
|
||||
| Key | Type | Source / notes |
|
||||
|-----|------|---------------|
|
||||
| `itemID` | number | Echoed from input. |
|
||||
| `name` | string | `m_name[0]` — localized display name. |
|
||||
| `description` | string \| nil | `m_description`. Omitted when empty. |
|
||||
| `icon` | string \| nil | `"Interface\\Icons\\<name>"`. Omitted when no icon. |
|
||||
| `displayInfoID` | number | `m_displayInfoID` → `ItemDisplayInfo.dbc` row. |
|
||||
| `quality` | number | 0..5 — see `LE_ITEM_QUALITY_*` enum. |
|
||||
| `classID` | number | `m_class` — `ItemClass.dbc` row. |
|
||||
| `subclassID` | number | `m_subClass` — `ItemSubClass.dbc` row. |
|
||||
| `className` | string | Localized class name (e.g. `"Weapon"`). |
|
||||
| `subclassName` | string | Localized subclass name (e.g. `"One-Handed Swords"`). |
|
||||
| `inventoryType` | number | `m_inventoryType` — raw integer. |
|
||||
| `equipLoc` | string | `"INVTYPE_*"` constant or `""` for non-equippable. |
|
||||
| `bindType` | number | `m_bonding` — 0=none, 1=BoP, 2=BoE, 3=BoU, 4=Quest. |
|
||||
| `flags` | number | Raw `m_flags` u32. |
|
||||
| `isConjured` | bool | Flag bit `0x2`. |
|
||||
| `isOpenable` | bool | Flag bit `0x4`. Same bit `C_Item.IsItemOpenable` reads. |
|
||||
| `isLootable` | bool | Flag bit `0x10` — has loot generators (right-click loot). |
|
||||
| `isWrapper` | bool | Flag bit `0x200` — gift-wrappable. |
|
||||
| `maxStackSize` | number | `m_stackable`. |
|
||||
| `maxCount` | number | `m_maxCount` — 0=unlimited, 1=unique, otherwise per-character cap. |
|
||||
| `containerSlots` | number | `m_containerSlots` — bag slot count, 0 for non-bags. |
|
||||
| `bagFamily` | number | `m_bagFamily` converted to modern bitmask (`1 << (id-1)`). |
|
||||
| `buyPrice` | number | Vendor buy price in copper. |
|
||||
| `sellPrice` | number | Vendor sell price in copper. |
|
||||
| `itemLevel` | number | Base ilvl from `ItemSparse`. |
|
||||
| `requiredLevel` | number | Minimum character level. |
|
||||
| `requiredSkill` | number | `SkillLine.dbc` row, 0=none. |
|
||||
| `requiredSkillRank` | number | Skill rank required. |
|
||||
| `requiredSpell` | number | `Spell.dbc` row required to learn / use, 0=none. |
|
||||
| `requiredHonorRank` | number | PvP honor rank required, 0=none. |
|
||||
| `requiredCityRank` | number | Reserved; 0 for vanilla. |
|
||||
| `requiredFaction` | number | `Faction.dbc` row, 0=none. |
|
||||
| `requiredFactionRank` | number | Reputation tier (Friendly/Honored/…). |
|
||||
| `allowableClass` | number | Class bitmask, `-1` = all classes. |
|
||||
| `allowableRace` | number | Race bitmask, `-1` = all races. |
|
||||
| `armor` | number | Armor value. |
|
||||
| `block` | number | Shield block value. |
|
||||
| `maxDurability` | number | 0 for items without durability. |
|
||||
| `stats` | table | Sparse `{ [statType] = statValue, … }`. Empty for items without stat allocations. |
|
||||
| `resistanceHoly` | number | Holy resistance. |
|
||||
| `resistanceFire` | number | Fire resistance. |
|
||||
| `resistanceNature` | number | Nature resistance. |
|
||||
| `resistanceFrost` | number | Frost resistance. |
|
||||
| `resistanceShadow` | number | Shadow resistance. |
|
||||
| `resistanceArcane` | number | Arcane resistance. |
|
||||
| `damageMin` | table | 5-element float array — weapon min damage per damage slot. |
|
||||
| `damageMax` | table | 5-element float array. |
|
||||
| `damageType` | table | 5-element integer array — damage school per slot. |
|
||||
| `delay` | number | Weapon swing time in ms. |
|
||||
| `ammoType` | number | Ammo subclass for ranged weapons. |
|
||||
| `rangedModRange` | number | Range modifier for bows/guns. |
|
||||
| `spells` | table | Sparse array of `{id, trigger, charges, cooldown, category, categoryCooldown}` records — one per non-empty spell slot (up to 5). |
|
||||
| `useSpellID` | number \| nil | Convenience — the spellID of the first slot with trigger=ON_USE (0). Omitted when the item has none. Same value [`C_Item.GetItemSpell`](#c_itemgetitemspellitem) returns. |
|
||||
| `lockID` | number | `Lock.dbc` row for the item's lock, 0=none. |
|
||||
| `itemSet` | number | `ItemSet.dbc` row, 0=not part of a set. |
|
||||
| `pageText` | number | `PageText.dbc` row for readable items. |
|
||||
| `pageMaterial` | number | Book page material. |
|
||||
| `languageID` | number | Language for in-game-readable books. |
|
||||
| `startQuest` | number | `Quest` row started by right-clicking this item. |
|
||||
| `material` | number | Material type (cloth/leather/metal etc. for hit sounds). |
|
||||
| `sheath` | number | Weapon sheath style. |
|
||||
| `randomProperty` | number | Random property template id. |
|
||||
| `area` | number | `AreaTable.dbc` — bound area for area-locked items, 0=none. |
|
||||
| `map` | number | `Map.dbc` — bound map for map-locked items, 0=none. |
|
||||
|
||||
```lua
|
||||
local data = C_Item.GetItemDataByID(6948) -- Hearthstone
|
||||
-- data.name == "Hearthstone"
|
||||
-- data.quality == 1, data.classID == 15, data.subclassID == 0
|
||||
-- data.bindType == 1 (BoP)
|
||||
-- data.useSpellID == 8690 (the recall spell)
|
||||
-- data.maxStackSize == 1, data.maxCount == 1
|
||||
|
||||
local sword = C_Item.GetItemDataByID(2092) -- Worn Shortsword
|
||||
-- sword.equipLoc == "INVTYPE_WEAPONMAINHAND"
|
||||
-- sword.damageMin[1] == 1.0, sword.damageMax[1] == 2.0, sword.delay == 1900
|
||||
-- sword.stats == {} (no stat allocations on the starter sword)
|
||||
```
|
||||
|
||||
**Cache state.** The function reads the same `DBCache_ItemStats`
|
||||
network cache every other `C_Item.*` reader consults. Items already in
|
||||
the player's inventory are typically populated by the engine's natural
|
||||
prefetch before `PLAYER_ENTERING_WORLD` returns; for other items
|
||||
(quest rewards, AH items, link hovers, etc.) the data lands once a
|
||||
`SMSG_ITEM_QUERY_SINGLE_RESPONSE` arrives. Use
|
||||
`C_Item.RequestLoadItemDataByID` if you specifically need to trigger
|
||||
the load.
|
||||
|
||||
**Stats encoding.** The `stats` table is keyed by the **ItemModType**
|
||||
enum (vanilla's encoding):
|
||||
|
||||
| Key | Meaning |
|
||||
|-----|---------|
|
||||
| 0 | Mana |
|
||||
| 1 | Health |
|
||||
| 3 | Agility |
|
||||
| 4 | Strength |
|
||||
| 5 | Intellect |
|
||||
| 6 | Spirit |
|
||||
| 7 | Stamina |
|
||||
|
||||
Higher-numbered slots (consumable per-second-regen, defense rating,
|
||||
hit/crit/dodge/parry chance, weapon/spell-power) only appear on items
|
||||
the server actually carries — vanilla 1.12 mostly uses 0..7. Empty
|
||||
slots (both type and value zero) are omitted, so iterating `stats`
|
||||
yields only the stats actually allocated.
|
||||
|
||||
No equivalent in modern WoW — `GetItemInfo` returns the 14-tuple
|
||||
piecemeal and `C_Item.GetItemInfo` adds a few more. ClassicAPI's
|
||||
`GetItemData` is a single-call superset useful for backporting tooltip
|
||||
addons and item-data caches.
|
||||
|
||||
### `C_Item.GetItemFamily(item)`
|
||||
|
||||
Returns the BagFamily bitmask for an item — i.e., what kind of
|
||||
@@ -3678,8 +4070,8 @@ end
|
||||
|
||||
Modern-style accessor for the always-available subset of item info — the
|
||||
fields that depend only on classification, not on player-specific state.
|
||||
Returns immediately from the client-side item cache; no server round-trip,
|
||||
no async polling. Returns `nil` if the item isn't in cache.
|
||||
Synchronous, side-effect-free: peeks the client-side item cache and
|
||||
returns whatever it has without warming or queueing.
|
||||
|
||||
Accepts a numeric `itemID` or a string containing `"item:NNN"` (matches both
|
||||
the bare `"item:1234"` shorthand and full chat links like
|
||||
@@ -3693,6 +4085,13 @@ Returns seven values:
|
||||
itemID, itemType, itemSubType, itemEquipLoc, icon, classID, subClassID
|
||||
```
|
||||
|
||||
The `itemID` return is always populated for any input that resolves to
|
||||
a positive integer (so it's safe to use the function as an ID extractor
|
||||
on a link without first warming the cache). The remaining six fields
|
||||
come from the cache record when present — when the item isn't cached
|
||||
yet they're all `nil`, but the call still returns a 7-tuple so the
|
||||
positional shape is preserved.
|
||||
|
||||
- `itemType` / `itemSubType` are the localized class / subclass names
|
||||
(e.g. `"Weapon"` / `"One-Handed Swords"`), read from `ItemClass.dbc` and
|
||||
`ItemSubClass.dbc`.
|
||||
@@ -3706,15 +4105,27 @@ itemID, itemType, itemSubType, itemEquipLoc, icon, classID, subClassID
|
||||
|
||||
```lua
|
||||
local id, type, subtype, equipLoc, icon, classID, subClassID
|
||||
= C_Item.GetItemInfoInstant(6948) -- Hearthstone
|
||||
= C_Item.GetItemInfoInstant(6948) -- Hearthstone, cached
|
||||
-- type="Miscellaneous", subtype="Junk", equipLoc="",
|
||||
-- icon="Interface\\Icons\\INV_Misc_Rune_01", classID=15, subClassID=0
|
||||
|
||||
local id = C_Item.GetItemInfoInstant("|cff...|Hitem:6948:0:0:0|h[Hearthstone]|h|r")
|
||||
-- id == 6948 even when the cache is cold — the call parses the link
|
||||
-- to extract the ID without consulting the cache.
|
||||
```
|
||||
|
||||
The actual class/subclass values reflect 1.12.1's data, which differs from
|
||||
modern WoW. For example, vanilla had no Cloth subclass under Trade Goods —
|
||||
Silk Cloth lives at `(7, 0)` in this client, not the modern `(7, 5)`.
|
||||
|
||||
> **No auto-warmup.** Unlike `GetItemInfo` or `C_Item.GetItemNameByID`,
|
||||
> `GetItemInfoInstant` does not trigger a network query on a cache
|
||||
> miss. The "Instant" name is contractual — callers that need the
|
||||
> cache populated should use
|
||||
> [`C_Item.RequestLoadItemDataByID`](#c_itemrequestloaditemdatabyiditem--c_itemrequestloaditemdataitemlocation)
|
||||
> explicitly, or just call `GetItemInfo` (which warms via the
|
||||
> `Script_GetItemInfo` hook).
|
||||
|
||||
### `C_Item.GetItemInventoryType(itemLocation)` / `C_Item.GetItemInventoryTypeByID(item)`
|
||||
|
||||
Returns the numeric `Enum.InventoryType` straight off the cache
|
||||
@@ -3802,7 +4213,7 @@ return nil; hits never need a fallback.
|
||||
|
||||
### `C_Item.GetItemMaxStackSize(itemLocation)` / `C_Item.GetItemMaxStackSizeByID(item)`
|
||||
|
||||
Returns the item type's max stack size — what you'd find as the 8th
|
||||
Returns the item type's max stack size — what you'd find as the 7th
|
||||
return of `GetItemInfo(item)`. `1` for non-stackable items; `5`, `20`,
|
||||
`200`, etc. for stackables. Different from `C_Item.GetStackCount` /
|
||||
the engine's `GetContainerItemInfo`, which return the **current**
|
||||
@@ -4241,16 +4652,19 @@ was already cached (so polling code paths still work), asynchronously
|
||||
fired when the engine's SMSG response handler completes a network
|
||||
fetch.
|
||||
|
||||
> **Vanilla quirk:** the engine has no native bool format code — `success`
|
||||
> arrives as `1`/`0` (number), not `true`/`false`. Idiomatic check is
|
||||
> `if success == 1 then ...`.
|
||||
> **`success` is `1` or `nil`.** The engine's printf-style event
|
||||
> dispatcher has no `%b` token, so we encode the boolean as `1` for
|
||||
> success / `nil` for failure (leaning on `lua_pushstring(NULL)` →
|
||||
> `lua_pushnil`). `if success then …` distinguishes them correctly —
|
||||
> `nil` is falsy, `1` is truthy. Same encoding as `GET_ITEM_INFO_RECEIVED`
|
||||
> and `QUEST_DATA_LOAD_RESULT`.
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("ITEM_DATA_LOAD_RESULT")
|
||||
f:SetScript("OnEvent", function()
|
||||
-- vanilla 1.12: event payload is in `arg1`, `arg2`, ... globals
|
||||
if event == "ITEM_DATA_LOAD_RESULT" and arg2 == 1 then
|
||||
if event == "ITEM_DATA_LOAD_RESULT" and arg2 then
|
||||
local _, type = C_Item.GetItemInfoInstant(arg1)
|
||||
-- ...
|
||||
end
|
||||
@@ -4258,6 +4672,21 @@ end)
|
||||
C_Item.RequestLoadItemDataByID(2589)
|
||||
```
|
||||
|
||||
> **Engine-aligned cache strategy.** `RequestLoadItemData` does NOT
|
||||
> eagerly create a cache entry. Empirically (e.g. pfQuest calling it
|
||||
> at addon-load / PLAYER_ENTERING_WORLD against a cleared WDB), pre-
|
||||
> creating the entry races the engine's natural inventory prefetch
|
||||
> and can leave the item permanently pending — the engine sees the
|
||||
> entry exists and skips its own `SMSG_ITEM_QUERY_SINGLE`, while our
|
||||
> early query gets dropped. Instead we track the itemID and hook the
|
||||
> engine's `SMSG_ITEM_QUERY_SINGLE_RESPONSE` handler at `0x0055BDB0`:
|
||||
> when the engine fills any entry, we sweep tracked items and fire
|
||||
> `ITEM_DATA_LOAD_RESULT` for matches. For items the engine doesn't
|
||||
> prefetch (quest rewards, AH browse targets, etc.) we escalate after
|
||||
> ~60 ticks by calling `CacheFetch` ourselves — by then engine state
|
||||
> has settled and our query goes through normally. Hard timeout at
|
||||
> ~1200 ticks (~20–40s) fires `ITEM_DATA_LOAD_RESULT(itemID, nil)`.
|
||||
|
||||
### `C_Item.UnlockAllItems()`
|
||||
|
||||
Sweep clear of every CGItem the engine knows about — same primitive
|
||||
@@ -4717,6 +5146,177 @@ if OffhandHasWeapon() then
|
||||
end
|
||||
```
|
||||
|
||||
## Loot
|
||||
|
||||
Programmatic loot operations that vanilla 1.12's Lua surface doesn't
|
||||
expose: enumerate nearby lootable corpses, open a loot session against
|
||||
one by GUID, take a specific item out of one without going through the
|
||||
visible `LootFrame`, and pre-scan every reachable corpse to build a
|
||||
picker table before any cursor work. Foundation for AoE-loot,
|
||||
auto-loot-by-filter, and similar addons that 1.12's stock
|
||||
`LootSlot` / `LootSlotInfo` flow can't drive.
|
||||
|
||||
All `guid` args are hex-string GUIDs in the same form `UnitGUID`
|
||||
returns (`"0xF130..."`). All `itemID` args are integers.
|
||||
|
||||
### `C_Loot.GetNearbyLootableUnits()`
|
||||
|
||||
Returns a 1-indexed array of `{ guid = "0x..." }` subtables, one per
|
||||
visible unit that the server has flagged lootable AND that's within
|
||||
the engine's right-click-loot interact range. Empty table pre-login,
|
||||
when no lootable units are in range, or when the local player has no
|
||||
loot rights on visible corpses (the server only sets the lootable
|
||||
flag for clients with rights).
|
||||
|
||||
```lua
|
||||
for _, unit in ipairs(C_Loot.GetNearbyLootableUnits()) do
|
||||
C_Loot.LootUnit(unit.guid)
|
||||
end
|
||||
```
|
||||
|
||||
The interact-range check matches what the engine uses for the
|
||||
hover-to-loot cursor — slightly tighter than melee range, looser than
|
||||
"on top of." Server still enforces real range on the resulting
|
||||
`CMSG_LOOT`; in-client OOR requests just silently get no response.
|
||||
|
||||
### `C_Loot.LootUnit(guid)`
|
||||
|
||||
Opens a loot session against the given corpse. The loot window
|
||||
appears asynchronously via the normal `LOOT_OPENED` event once
|
||||
`SMSG_LOOT_RESPONSE` arrives — same flow as right-clicking the
|
||||
corpse, just dispatched programmatically.
|
||||
|
||||
No return value. Silent no-op when:
|
||||
- The object manager is NULL (glue / character-select).
|
||||
- The GUID doesn't resolve to a visible unit (out of render range,
|
||||
despawned, wrong typemask).
|
||||
- The local player pointer can't be resolved.
|
||||
|
||||
Errors only on bad Lua usage (missing arg, unparseable GUID).
|
||||
|
||||
```lua
|
||||
-- Loot everything in range, one corpse at a time. With autoloot on,
|
||||
-- the engine drains each window automatically; without it, you
|
||||
-- still get the visible loot frame per corpse.
|
||||
for _, unit in ipairs(C_Loot.GetNearbyLootableUnits()) do
|
||||
C_Loot.LootUnit(unit.guid)
|
||||
end
|
||||
```
|
||||
|
||||
`useDistanceCheck` is hardcoded off — the engine won't try to
|
||||
auto-walk the player toward a corpse just out of range. That'd be a
|
||||
surprise side effect for an AoE-loot loop.
|
||||
|
||||
### `C_Loot.LootUnitItem(guid, itemID)`
|
||||
|
||||
Takes a specific item from a specific corpse without using the
|
||||
visible loot frame's slot list. Dispatches `CMSG_LOOT_ITEM` directly.
|
||||
|
||||
Two paths, picked automatically:
|
||||
|
||||
1. **Fast path** — the corpse's loot window is already open for the
|
||||
requested target. Synchronously finds the slot, sends the take
|
||||
packet, returns `true` if the slot was found / `false` if not.
|
||||
2. **Async path** — opens the corpse's window first (same wire as
|
||||
`C_Loot.LootUnit`), waits for the response, then sends the take
|
||||
packet automatically. Returns `true` once the request is queued.
|
||||
No "did it succeed" event; observe `BAG_UPDATE` / `ITEM_PUSH` to
|
||||
confirm.
|
||||
|
||||
`false` immediately when:
|
||||
- Args missing or wrong types (raises an error, not returns false).
|
||||
- Another `LootUnitItem` is mid-flight (one at a time).
|
||||
- The world isn't loaded.
|
||||
- The target GUID doesn't resolve to a visible unit.
|
||||
- (sync path only) No slot in the open window holds the itemID.
|
||||
|
||||
The send bypasses vanilla's BoP-confirmation dialog. Server still
|
||||
enforces all real permissions (loot rights, distance, master-loot
|
||||
rules); the BoP prompt is purely a client-side courtesy that callers
|
||||
of a programmatic API have already opted out of.
|
||||
|
||||
```lua
|
||||
-- Scan, then take just one specific item out of one specific corpse.
|
||||
C_Loot.ScanNearbyLoot() -- LOOT_SCAN_COMPLETED → results below
|
||||
local results = C_Loot.GetLastScanResults()
|
||||
local guid = results[1].guid
|
||||
local itemID = results[1].items[1].itemID
|
||||
C_Loot.LootUnitItem(guid, itemID)
|
||||
```
|
||||
|
||||
### `C_Loot.ScanNearbyLoot()`
|
||||
|
||||
Initiates a pre-scan: opens each lootable corpse in range one at a
|
||||
time, scrapes its slot contents (itemID, count, item link), closes
|
||||
the window, advances. The `LOOT_OPENED` / `LOOT_CLOSED` events are
|
||||
suppressed for the duration of the scan so `LootFrame` and other
|
||||
listeners don't react — the scan is invisible to addons that aren't
|
||||
opted in.
|
||||
|
||||
Returns `true` if the scan was queued, `false` if another scan is
|
||||
already in progress or the world isn't loaded. Completion is signaled
|
||||
via the [`LOOT_SCAN_COMPLETED`](#loot_scan_completed-event) event;
|
||||
results are read via [`C_Loot.GetLastScanResults()`](#c_lootgetlastscanresults).
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("LOOT_SCAN_COMPLETED")
|
||||
f:SetScript("OnEvent", function()
|
||||
for _, corpse in ipairs(C_Loot.GetLastScanResults()) do
|
||||
for _, item in ipairs(corpse.items) do
|
||||
ChatFrame1:AddMessage(item.link or ("item:"..item.itemID))
|
||||
end
|
||||
end
|
||||
end)
|
||||
C_Loot.ScanNearbyLoot()
|
||||
```
|
||||
|
||||
Per-corpse step timeout is ~3-6 seconds (180 WorldTick frames) so a
|
||||
single dropped server response doesn't hang the whole scan. Failed
|
||||
corpses simply don't appear in the results.
|
||||
|
||||
### `C_Loot.IsScanInProgress()`
|
||||
|
||||
Returns `true` while a `ScanNearbyLoot` call is mid-flight, `false`
|
||||
otherwise. Use to gate re-scan attempts so addons don't spam-call
|
||||
during the engine's per-corpse round-trip.
|
||||
|
||||
### `C_Loot.GetLastScanResults()`
|
||||
|
||||
Returns the most-recent completed-scan results as a 1-indexed array:
|
||||
|
||||
```lua
|
||||
{
|
||||
[1] = {
|
||||
guid = "0x...", -- corpse GUID
|
||||
coin = 1234, -- copper amount; 0 if no coin
|
||||
items = {
|
||||
[1] = {
|
||||
itemID = 2589,
|
||||
count = 4,
|
||||
link = "|cff...|Hitem:2589:0:0:0|h[Linen Cloth]|h|r",
|
||||
-- omitted if cache entry wasn't loaded
|
||||
},
|
||||
...
|
||||
},
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Empty table before the first scan completes. Replaced wholesale by
|
||||
each new scan — re-read after `LOOT_SCAN_COMPLETED` rather than
|
||||
caching the table.
|
||||
|
||||
The `link` field is the engine's full hyperlink (color + payload +
|
||||
display name + reset). Random-suffix items like "Stringy Wolf Meat
|
||||
of the Bear" survive round-trip — the link encodes the per-instance
|
||||
enchant / suffix / unique fields, not just the base itemID. For
|
||||
tooltip display, extract the payload form via
|
||||
`string.match(link, "|H(item:[^|]+)|h")` — vanilla's `SetHyperlink`
|
||||
requires the literal `"item:"` prefix and rejects full `|cff...|Hitem...|h`
|
||||
input.
|
||||
|
||||
## Macros
|
||||
|
||||
Engine-level extensions to how macros are parsed and dispatched. These
|
||||
@@ -4889,6 +5489,74 @@ MPQ will appear in both the loose and mpq lists).
|
||||
The vanilla legacy globals `GetNumMacroIcons` / `GetMacroIconInfo`
|
||||
remain available unchanged.
|
||||
|
||||
## Mail
|
||||
|
||||
Backports of modern WoW's mail-attachment link getters. Both follow
|
||||
the modern signature including the optional `attachmentIndex` arg
|
||||
(added with TBC's multi-attachment mail), even though vanilla 1.12
|
||||
only supports one attachment per message — pass-through compatibility
|
||||
for addons originally written against later API versions. Any
|
||||
`attachmentIndex` other than `1` is treated as no-op.
|
||||
|
||||
Both functions return `(link, itemID)` — modern returns just `link`,
|
||||
but `itemID` is cheap to surface at the same lookup site and saves
|
||||
callers a `string.match` on the link.
|
||||
|
||||
### `GetSendMailItemLink([attachmentIndex])`
|
||||
|
||||
Returns the **fully-decorated per-instance** hyperlink for the item
|
||||
currently attached to the outgoing-mail slot, plus that item's
|
||||
`itemID`. Same enchant / random-suffix / unique-ID decoration as
|
||||
`GetContainerItemLink` would return for the same item back in the
|
||||
player's bag — the attached item still exists as a live CGItem until
|
||||
the mail is sent, so the link builder has full instance state to
|
||||
draw from.
|
||||
|
||||
Returns nothing when:
|
||||
- No item is attached.
|
||||
- `attachmentIndex` is anything other than `nil` or `1`.
|
||||
- The attachment's CGItem can't be resolved (shouldn't normally
|
||||
happen — engine writes the GUID on attach and clears it on detach).
|
||||
|
||||
```lua
|
||||
local link, itemID = GetSendMailItemLink()
|
||||
if link then
|
||||
ChatFrame1:AddMessage("Attached: " .. link .. " (id " .. itemID .. ")")
|
||||
end
|
||||
```
|
||||
|
||||
### `GetInboxItemLink(messageIndex[, attachmentIndex])`
|
||||
|
||||
Returns the **basic itemID-only** hyperlink (`|cff…|Hitem:N:0:0:0|h[Name]|h|r`)
|
||||
for the item attached to inbox message `messageIndex` (1-based),
|
||||
plus the `itemID`.
|
||||
|
||||
**Why basic-link instead of per-instance:** vanilla inbox entries do
|
||||
store per-instance modifiers inline (enchant at `+0x12C`, suffix
|
||||
factor at `+0x134`, random property at `+0x138` — the same fields
|
||||
`GameTooltip:SetInboxItem` copies onto the tooltip to render a fully-
|
||||
decorated preview). We intentionally ignore them in the link to match
|
||||
modern 3.3.5's behavior — its `GetInboxItemLink` calls the
|
||||
itemID-only link builder (`FUN_0061E290(itemID)`) and ignores the
|
||||
per-instance fields. Per-instance data only fully manifests on the
|
||||
client side once the player takes the item out of the mail and the
|
||||
engine spawns a real CGItem; the inbox-entry fields are display-only
|
||||
hints used by the tooltip builder.
|
||||
|
||||
Errors on missing or non-numeric `messageIndex`. Returns nothing when:
|
||||
- `messageIndex` is out of range (`< 1` or `> GetInboxNumItems()`).
|
||||
- The message slot is empty / has no attached item.
|
||||
- `attachmentIndex` is anything other than `nil` or `1`.
|
||||
|
||||
```lua
|
||||
for i = 1, GetInboxNumItems() do
|
||||
local link, itemID = GetInboxItemLink(i)
|
||||
if link then
|
||||
ChatFrame1:AddMessage(i .. ": " .. link .. " (id " .. itemID .. ")")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Map
|
||||
|
||||
### `C_Map.GetBestMapForUnit(unitToken)`
|
||||
@@ -5626,9 +6294,11 @@ cache. Synchronously fired when the data was already cached
|
||||
`SMSG_QUEST_QUERY_RESPONSE` lands when the engine had to round-trip to the
|
||||
server.
|
||||
|
||||
> **Vanilla quirk:** the engine has no native bool format code — `success`
|
||||
> arrives as `1`/`0` (number), not `true`/`false`, just like
|
||||
> `ITEM_DATA_LOAD_RESULT`. Idiomatic check is `if success == 1 then ...`.
|
||||
> **`success` is `1` or `nil`.** The engine's printf-style event
|
||||
> dispatcher has no `%b` token, so we encode the boolean as `1` for
|
||||
> success / `nil` for failure. `if success then ...` works as expected
|
||||
> (`nil` falsy, `1` truthy). Same encoding as `ITEM_DATA_LOAD_RESULT`
|
||||
> and `GET_ITEM_INFO_RECEIVED`.
|
||||
|
||||
> **Vanilla limitation:** for *invalid* questIDs (ones the server doesn't
|
||||
> have), the 1.12 server silently drops the query — it doesn't send a
|
||||
|
||||
+9
-2
@@ -3,7 +3,9 @@
|
||||
Catalogue of the Lua C API functions linked into the client. Vanilla WoW is
|
||||
built against **Lua 5.0** (not 5.1), and the entire API plus the auxiliary
|
||||
library lives in a contiguous block of `.text` from roughly `0x006F2000` to
|
||||
`0x006F5100`. ~120 functions; the public C API is the core of that.
|
||||
`0x006F5100`. ~120 functions; the public C API is the core of that. The
|
||||
thread / coroutine machinery (`lua_resume`, `lua_yield`, the inner
|
||||
`luaE_newthread`) lives slightly higher at `0x006F6000`–`0x006F6900`.
|
||||
|
||||
**Cross-verified against two binaries:**
|
||||
|
||||
@@ -75,6 +77,8 @@ context but not byte-by-byte verified.
|
||||
|
||||
| VA | Function | Conv. | Notes | Conf |
|
||||
|--------------|-------------------------|----------|--------------------------------------------------------------|------|
|
||||
| `0x006F2F80` | `lua_xmove` | fastcall | `(from, to, n)` — decr `from->top` by `n*0x10`, copy each 16-byte TValue, incr `to->top`. Used by `coroutine.*` to shuttle args/returns between threads | ★ |
|
||||
| `0x006F3030` | `lua_newthread` | fastcall | GC check then calls inner `luaE_newthread` at `0x006F6B10`; pushes the new state onto L with tag 8 (LUA_TTHREAD). Returns `lua_State *` in eax — Ghidra reports `void` (decompiler artifact) | ★ |
|
||||
| `0x006F3070` | `lua_gettop` | fastcall | `(top - base) >> 4` | ★ |
|
||||
| `0x006F3080` | `lua_settop` | fastcall | | ✓ |
|
||||
| `0x006F30D0` | `lua_remove` | fastcall | shift-down loop + decr_top; previously misidentified as `lua_pushvalue` | ★ |
|
||||
@@ -155,7 +159,10 @@ behaviour the comment describes (`push 0; mov edx, 1; mov ecx, L`) is
|
||||
| `0x006F4290` | *internal CFunction trampoline* | | called via `lua_cpcall` | ★ |
|
||||
| `0x006F4320` | `lua_load` | fastcall | reader / loader entry | ★ |
|
||||
| `0x006F4370` | `lua_dump` | fastcall | requires tag 6 + `Closure.isC == 0` | ★ |
|
||||
| `0x006F4940` | `lua_error` | cdecl | `(L, msg)` — args on stack; reaches `luaD_throw` via wrappers | ✓ |
|
||||
| `0x006F4940` | `lua_error` | **cdecl** varargs | `(L, fmt, ...)` — printf-style; engine prepends `luaL_where` then `lua_concat`. Single-string callers OK (cdecl tolerates extra-arg-less calls); user-controlled strings should use `Error(L, "%s", msg)` to avoid format-string interpretation | ✓ |
|
||||
| `0x006F6620` | `lua_resume` | fastcall | `(L_co, nargs) → int`. Zero internal callers (coroutine lib was stripped from registration tables); linker kept the symbol. Stores nargs in a local, calls `luaD_rawrunprotected(L, &resume_inner, &nargs)` at `0x006F5DAC`; inner static helper at `0x006F67B0` | ★ |
|
||||
| `0x006F6860` | `lua_yield` | fastcall | `(L, nresults) → int (-1)`. Sets `0x10` flag on CallInfo state field rather than directly writing `L->status` like later Lua versions. Errors with "attempt to yield across metamethod/C-call boundary" or "cannot yield a C function" when called from the wrong context | ★ |
|
||||
| `0x006F6B10` | *internal* `luaE_newthread` | fastcall | Inner allocator called by `lua_newthread` at `0x006F3030`. Allocates the new state, sets tag 8 via the push helper at `0x006F7B20`, inits via `0x006F6C40`, copies global-state fields from the parent. Returns `lua_State *` | ★ |
|
||||
|
||||
### Misc / GC / version
|
||||
|
||||
|
||||
+9
-1
@@ -48,7 +48,15 @@ namespace Lua {
|
||||
F(PCall, lua_pcall, LUA_PCALL) \
|
||||
F(Next, lua_next, LUA_NEXT) \
|
||||
F(Type, lua_type, LUA_TYPE) \
|
||||
F(Error, lua_error, LUA_ERROR)
|
||||
F(Error, lua_error, LUA_ERROR) \
|
||||
F(ToPointer, lua_topointer, LUA_TO_POINTER) \
|
||||
F(ToThread, lua_tothread, LUA_TO_THREAD) \
|
||||
F(IsCFunction, lua_iscfunction, LUA_IS_C_FUNCTION)\
|
||||
F(XMove, lua_xmove, LUA_X_MOVE) \
|
||||
F(NewThread, lua_newthread, LUA_NEW_THREAD) \
|
||||
F(Resume, lua_resume, LUA_RESUME) \
|
||||
F(Yield, lua_yield, LUA_YIELD) \
|
||||
F(ArgError, luaL_argerror, LUAL_ARG_ERROR)
|
||||
|
||||
#define CLASSICAPI_BIND_LUA(Name, Typedef, Offset) \
|
||||
const Typedef##_t Name = reinterpret_cast<Typedef##_t>(Offsets::Offset);
|
||||
|
||||
+31
-1
@@ -49,6 +49,8 @@ constexpr int TYPE_NUMBER = 3;
|
||||
constexpr int TYPE_STRING = 4;
|
||||
constexpr int TYPE_TABLE = 5;
|
||||
constexpr int TYPE_FUNCTION = 6;
|
||||
constexpr int TYPE_USERDATA = 7;
|
||||
constexpr int TYPE_THREAD = 8;
|
||||
|
||||
using lua_isnumber_t = bool(__fastcall *)(void *L, int index);
|
||||
using lua_isstring_t = bool(__fastcall *)(void *L, int index);
|
||||
@@ -76,7 +78,27 @@ using lua_call_t = void(__fastcall *)(void *L, int nargs, int nresults);
|
||||
using lua_pcall_t = int(__fastcall *)(void *L, int nargs, int nresults, int errfunc);
|
||||
using lua_next_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_type_t = int(__fastcall *)(void *L, int index);
|
||||
using lua_error_t = void(__cdecl *)(void *L, const char *);
|
||||
// The 1.12 `lua_error` wrapper at `0x6F4940` is actually variadic —
|
||||
// it does `lua_pushvfstring(L, fmt, args)` then prepends `luaL_where`
|
||||
// and throws. Existing callers pass a single literal (no `%`), which
|
||||
// is fine because cdecl tolerates extra-arg-less calls. New callers
|
||||
// forwarding user-supplied error strings should use `Error(L, "%s",
|
||||
// userMsg)` to avoid format-string interpretation of `%` in the
|
||||
// message body.
|
||||
using lua_error_t = void(__cdecl *)(void *L, const char *fmt, ...);
|
||||
using lua_topointer_t = const void *(__fastcall *)(void *L, int idx);
|
||||
using lua_tothread_t = void *(__fastcall *)(void *L, int idx);
|
||||
using lua_iscfunction_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_xmove_t = void(__fastcall *)(void *from, void *to, int n);
|
||||
// `lua_newthread` returns the new `lua_State *` in eax, but Ghidra
|
||||
// infers `void` because the inner call's value just flows through.
|
||||
// We declare the return type honestly here; callers that don't want
|
||||
// to rely on the calling convention preserving eax can read the
|
||||
// thread back from L's top via `ToPointer(L, -1)`.
|
||||
using lua_newthread_t = void *(__fastcall *)(void *L);
|
||||
using lua_resume_t = int(__fastcall *)(void *L, int nargs);
|
||||
using lua_yield_t = int(__fastcall *)(void *L, int nresults);
|
||||
using luaL_argerror_t = void(__fastcall *)(void *L, int narg, const char *msg);
|
||||
|
||||
extern const lua_isnumber_t IsNumber;
|
||||
extern const lua_isstring_t IsString;
|
||||
@@ -110,6 +132,14 @@ extern const lua_pcall_t PCall;
|
||||
extern const lua_next_t Next;
|
||||
extern const lua_type_t Type;
|
||||
extern const lua_error_t Error;
|
||||
extern const lua_topointer_t ToPointer;
|
||||
extern const lua_tothread_t ToThread;
|
||||
extern const lua_iscfunction_t IsCFunction;
|
||||
extern const lua_xmove_t XMove;
|
||||
extern const lua_newthread_t NewThread;
|
||||
extern const lua_resume_t Resume;
|
||||
extern const lua_yield_t Yield;
|
||||
extern const luaL_argerror_t ArgError;
|
||||
|
||||
// Returns the global `lua_State *` (read on demand from the engine's global).
|
||||
// Callable outside a Lua callback, e.g. during LoadScriptFunctions setup.
|
||||
|
||||
+408
-1
@@ -286,7 +286,10 @@ enum Offsets {
|
||||
// holds the local player's 64-bit GUID, and the visible-object
|
||||
// iterator at FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS walks its
|
||||
// +0xAC list. Useful for "is this GUID me?" checks without
|
||||
// round-tripping through Lua.
|
||||
// round-tripping through Lua. Also a NULL-check proxy for "is
|
||||
// the object manager initialised" — engine iterators deref this
|
||||
// unconditionally on entry, so callers must skip on the glue /
|
||||
// character-select screen.
|
||||
VAR_LOCAL_PLAYER_PTR = 0x00B41414,
|
||||
OFF_LOCAL_PLAYER_GUID = 0xC0,
|
||||
// `__fastcall(ecx = const char *token) → uint64_t GUID` — the
|
||||
@@ -1068,6 +1071,26 @@ enum Offsets {
|
||||
// With `requestIfMissing=false`, returns NULL if not in cache (no server
|
||||
// round-trip) — exactly the "instant" semantics we want.
|
||||
FUN_DBCACHE_ITEMSTATS_GET_RECORD = 0x0055BA30,
|
||||
// Item-cache response handler — the function called from
|
||||
// `0x00555140` (success path for `SMSG_ITEM_QUERY_SINGLE_RESPONSE`)
|
||||
// and `0x00555160` (failure path). Parses one or more itemIDs out
|
||||
// of the packet, looks up (or creates) the cache entry per ID,
|
||||
// copies the item data block via `FUN_007c9640` to `entry+0x18`,
|
||||
// sets `[entry+0x1F0]=1` (the loaded byte), then walks the entry's
|
||||
// pending callback list at `[entry+0x1FC]` and invokes each with
|
||||
// `(userData, success)`. High-bit-on itemID in the packet signals
|
||||
// a per-item failure for the batch.
|
||||
//
|
||||
// We hook this to observe any item-cache fill the engine performs
|
||||
// — covers the engine's natural inventory prefetch and any
|
||||
// addon-triggered queries without having to register our own
|
||||
// engine callbacks. Lets `RequestLoadItemData` be purely passive:
|
||||
// we track itemIDs, the engine (or whoever) issues the query, we
|
||||
// see the fill via this hook and fire `ITEM_DATA_LOAD_RESULT`.
|
||||
//
|
||||
// Signature: `__thiscall(void *cache, void *packetReader, int flag)`.
|
||||
// `cache` is the same `VAR_ITEMDB_CACHE` we already use.
|
||||
FUN_ITEMSTATS_CACHE_RESPONSE = 0x0055BDB0,
|
||||
// ItemStats_C field offsets we read. Full struct layout in
|
||||
// VanillaHelpers's `Game.h` (`struct ItemStats_C`); we only need these.
|
||||
OFF_ITEMSTATS_CLASS = 0x00,
|
||||
@@ -1182,6 +1205,10 @@ enum Offsets {
|
||||
// always at index 0 (some Turtle WoW custom items put it later).
|
||||
OFF_ITEMSTATS_SPELL_ID = 0x11C, // u32[5] — spell ID per slot
|
||||
OFF_ITEMSTATS_SPELL_TRIGGER = 0x130, // u32[5] — trigger code per slot
|
||||
OFF_ITEMSTATS_SPELL_CHARGES = 0x144, // i32[5] — charges per slot (-1 = infinite)
|
||||
OFF_ITEMSTATS_SPELL_COOLDOWN = 0x158, // u32[5] — cooldown ms per slot
|
||||
OFF_ITEMSTATS_SPELL_CATEGORY = 0x16C, // u32[5] — category (groups shared cooldowns)
|
||||
OFF_ITEMSTATS_SPELL_CATEGORY_CD = 0x180, // u32[5] — category cooldown ms
|
||||
ITEMSTATS_SPELL_SLOT_COUNT = 5,
|
||||
ITEM_SPELLTRIGGER_ON_USE = 0,
|
||||
|
||||
@@ -1193,6 +1220,63 @@ enum Offsets {
|
||||
// members in VanillaHelpers's `ItemStats_C` definition.
|
||||
OFF_ITEMSTATS_MAX_COUNT = 0x5C,
|
||||
|
||||
// Remaining `ItemStats_C` fields used by `C_Item.GetItemData(ByID)`.
|
||||
// Offsets derived from VanillaHelpers's `struct ItemStats_C`
|
||||
// definition (`C:\Git\VanillaHelpers\src\Game.h`), sized to match
|
||||
// the verified anchors above (`+0x60 m_stackable`, `+0x1D0
|
||||
// m_bagFamily`, `+0x1AC m_lockID`, etc.). Block follows the same
|
||||
// ordering as the struct so it's easy to cross-reference.
|
||||
OFF_ITEMSTATS_BUY_PRICE = 0x24, // u32
|
||||
OFF_ITEMSTATS_ALLOWABLE_CLASS = 0x30, // u32 — class bitmask, -1 = all
|
||||
OFF_ITEMSTATS_ALLOWABLE_RACE = 0x34, // u32 — race bitmask, -1 = all
|
||||
OFF_ITEMSTATS_REQUIRED_LEVEL = 0x3C, // u32
|
||||
OFF_ITEMSTATS_REQUIRED_SKILL = 0x40, // u32 — SkillLine.dbc row
|
||||
OFF_ITEMSTATS_REQUIRED_SKILL_RANK = 0x44, // u32
|
||||
OFF_ITEMSTATS_REQUIRED_SPELL = 0x48, // u32 — Spell.dbc row
|
||||
OFF_ITEMSTATS_REQUIRED_HONOR_RANK = 0x4C, // u32
|
||||
OFF_ITEMSTATS_REQUIRED_CITY_RANK = 0x50, // u32
|
||||
OFF_ITEMSTATS_REQUIRED_FACTION = 0x54, // u32 — Faction.dbc row
|
||||
OFF_ITEMSTATS_REQUIRED_FACTION_RANK = 0x58, // u32 — Friendly/Honored/etc.
|
||||
OFF_ITEMSTATS_STAT_TYPE = 0x68, // u32[10] — ItemModType enum per slot
|
||||
OFF_ITEMSTATS_STAT_VALUE = 0x90, // i32[10] — magnitude per slot
|
||||
ITEMSTATS_STAT_SLOT_COUNT = 10,
|
||||
OFF_ITEMSTATS_DAMAGE_MIN = 0xB8, // f32[5]
|
||||
OFF_ITEMSTATS_DAMAGE_MAX = 0xCC, // f32[5]
|
||||
OFF_ITEMSTATS_DAMAGE_TYPE = 0xE0, // u32[5] — schools per damage slot
|
||||
ITEMSTATS_DAMAGE_SLOT_COUNT = 5,
|
||||
OFF_ITEMSTATS_ARMOR = 0xF4, // u32
|
||||
OFF_ITEMSTATS_RES_HOLY = 0xF8, // u32 (six resists contiguous)
|
||||
OFF_ITEMSTATS_RES_FIRE = 0xFC,
|
||||
OFF_ITEMSTATS_RES_NATURE = 0x100,
|
||||
OFF_ITEMSTATS_RES_FROST = 0x104,
|
||||
OFF_ITEMSTATS_RES_SHADOW = 0x108,
|
||||
OFF_ITEMSTATS_RES_ARCANE = 0x10C,
|
||||
OFF_ITEMSTATS_DELAY = 0x110, // u32 — weapon swing time (ms)
|
||||
OFF_ITEMSTATS_AMMO_TYPE = 0x114, // u32 — ammo subclass for ranged
|
||||
OFF_ITEMSTATS_RANGED_MOD_RANGE = 0x118, // f32 — range modifier on bows/guns
|
||||
OFF_ITEMSTATS_BONDING = 0x194, // u32 — 0=none, 1=BoP, 2=BoE, 3=BoU, 4=Quest
|
||||
OFF_ITEMSTATS_DESCRIPTION = 0x198, // char * — flavor text (locale-applied)
|
||||
OFF_ITEMSTATS_PAGE_TEXT = 0x19C, // u32 — PageText.dbc row (readable books)
|
||||
OFF_ITEMSTATS_LANGUAGE_ID = 0x1A0, // u32 — language for book text
|
||||
OFF_ITEMSTATS_PAGE_MATERIAL = 0x1A4, // u32 — book material
|
||||
OFF_ITEMSTATS_START_QUEST = 0x1A8, // u32 — questID started by right-click
|
||||
OFF_ITEMSTATS_MATERIAL = 0x1B0, // i32 — material type
|
||||
OFF_ITEMSTATS_SHEATH = 0x1B4, // u32 — weapon sheath style
|
||||
OFF_ITEMSTATS_RANDOM_PROPERTY = 0x1B8, // i32 — random property template
|
||||
OFF_ITEMSTATS_BLOCK = 0x1BC, // u32 — shield block value
|
||||
OFF_ITEMSTATS_MAX_DURABILITY = 0x1C4, // u32
|
||||
OFF_ITEMSTATS_AREA = 0x1C8, // u32 — bound area (AreaTable.dbc)
|
||||
OFF_ITEMSTATS_MAP = 0x1CC, // u32 — bound map (Map.dbc)
|
||||
|
||||
// `m_flags` bits we surface as decoded booleans. The OPENABLE bit
|
||||
// (0x4) is verified by the tooltip-builder's reader at 0x0052E323;
|
||||
// the others are documented in the standard 1.12 protocol
|
||||
// (CMaNGOS / TrinityCore `ITEM_FLAG_*`) and exposed as-is. Add
|
||||
// verification anchors as consumers materialize.
|
||||
ITEM_FLAG_CONJURED = 0x2,
|
||||
ITEM_FLAG_LOOTABLE = 0x10,
|
||||
ITEM_FLAG_WRAPPER = 0x200,
|
||||
|
||||
// `Spell.dbc` `Name[9]` field at record +0x1E0 (9-locale string
|
||||
// array, indexed by `[VAR_LOCALE_INDEX]`). Used by `GetItemSpell`
|
||||
// to push the spell name half of its return.
|
||||
@@ -1703,6 +1787,10 @@ enum Offsets {
|
||||
// Note: the bit's MEANING is "can't toggle" — `canToggleAtWar` is
|
||||
// its inverse, NOT its value.
|
||||
REP_SLOT_FLAG_PEACE_FORCED = 0x10,
|
||||
// INACTIVE flag — set by `Script_SetFactionInactive` (the "Inactive"
|
||||
// collapsible category in FactionFrame). Toggles via
|
||||
// `FUN_FACTION_SET_INACTIVE`; same byte as AT_WAR and PEACE_FORCED.
|
||||
REP_SLOT_FLAG_INACTIVE = 0x20,
|
||||
MAX_REP_SLOTS = 64,
|
||||
|
||||
// Static reaction-band threshold tables, indexed by reaction band
|
||||
@@ -1722,6 +1810,81 @@ enum Offsets {
|
||||
// against `FUN_004D5620(slot)`'s return.
|
||||
OFF_CGPLAYER_INFO_WATCHED_REP_LIST_ID = 0x10C4,
|
||||
|
||||
// `FactionSetAtWar(factionID, newState)` — `__fastcall(ecx = factionID,
|
||||
// edx = char newState)`. The sole worker behind
|
||||
// `Script_FactionToggleAtWar` (`0x004D6950`); the script wrapper just
|
||||
// reads the current at-war flag, negates it, and tail-calls here.
|
||||
//
|
||||
// Validates (only when turning OFF) that current standing ≥ -3000,
|
||||
// resolves factionID → repListID, bails if the player is in a loot
|
||||
// session (UNIT_FIELD_FLAGS bit `0x80000`, error code `0x1A1`),
|
||||
// mutates the rep-slot flag byte (`AT_WAR` gated by `PEACE_FORCED`),
|
||||
// and sends `CMSG_SET_FACTION_ATWAR` (opcode `0x125`).
|
||||
//
|
||||
// **Does NOT fire `UNIT_FACTION`** — a vanilla engine omission. 3.3.5
|
||||
// added a `FUN_0071F8F0(player, 0)` call here, whose inner
|
||||
// `FUN_0060BF10(playerGUID, UNIT_FACTION_id)` dispatches the event
|
||||
// for every unit token referencing the player. We polyfill by
|
||||
// hooking this function and firing `UNIT_FACTION("player")` post-
|
||||
// original (see `src/faction/UnitFactionPolyfill.cpp`).
|
||||
FUN_FACTION_SET_AT_WAR = 0x004D5FD0,
|
||||
|
||||
// `FactionSetInactive(factionID, newState)` — `__fastcall(ecx = factionID,
|
||||
// edx = char newState)`. Inner setter behind `Script_SetFactionInactive`
|
||||
// (`0x004D69B0`, sets INACTIVE bit) and `Script_SetFactionActive`
|
||||
// (`0x004D6A00`, clears it) — both script wrappers tail-call here.
|
||||
//
|
||||
// Resolves factionID → repListID via `FUN_004D5600`, mutates the
|
||||
// rep-slot flag byte (sets/clears `INACTIVE` = `0x20`), writes via
|
||||
// `FUN_004D5FC0`, sends `CMSG_SET_FACTION_INACTIVE` (opcode `0x317`),
|
||||
// then calls `FUN_004D5C40` (recompute displayed-list, fire
|
||||
// `UPDATE_FACTION` event `0x174`).
|
||||
//
|
||||
// **Does NOT fire `UNIT_FACTION`** — same engine omission as the at-
|
||||
// war setter. Polyfilled in `src/faction/UnitFactionPolyfill.cpp`.
|
||||
FUN_FACTION_SET_INACTIVE = 0x004D60F0,
|
||||
|
||||
// SMSG_SET_FACTION_ATWAR handler — `__stdcall(uint32_t opcode,
|
||||
// void *packet)`. Fires when the server force-changes the player's
|
||||
// at-war state on a faction (e.g., a flag the player can't toggle
|
||||
// changes server-side, or faction reset). Reads `(u32 repListID,
|
||||
// u8 flags)`, mutates AT_WAR on the slot accordingly, writes via
|
||||
// `FUN_004D5FC0`, then fires `UPDATE_FACTION` (event `0x174`).
|
||||
//
|
||||
// **Does NOT fire `UNIT_FACTION`** — 3.3.5 does (unconditionally
|
||||
// after the byte write). Polyfilled in
|
||||
// `src/faction/UnitFactionPolyfill.cpp`.
|
||||
FUN_SMSG_SET_FACTION_ATWAR = 0x004D56B0,
|
||||
|
||||
// SMSG_INITIALIZE_FACTIONS handler — `__stdcall(uint32_t opcode,
|
||||
// void *packet)`. Fires once at login from the SMSG dispatcher. Reads
|
||||
// a count, then loops `(u8 flags, i32 standing)` pairs into the
|
||||
// 64-entry rep slot array: `FUN_004D5FC0(slot, flags)` +
|
||||
// `FUN_004D6330(slot, standing, 0 /*notify*/)`. Trailing
|
||||
// `FUN_004D5430` rebuilds the displayed-list snapshot, then fires
|
||||
// `UPDATE_FACTION` (event `0x174`).
|
||||
//
|
||||
// **Does NOT fire `UNIT_FACTION`** — modern WoW does. Addons that
|
||||
// observe UNIT_FACTION miss the initial sync entirely in vanilla.
|
||||
// Polyfilled in `src/faction/UnitFactionPolyfill.cpp`.
|
||||
FUN_SMSG_INITIALIZE_FACTIONS = 0x004D5640,
|
||||
|
||||
// SMSG_SET_FACTION_STANDINGS handler — `__stdcall(uint32_t opcode,
|
||||
// void *packet)`. Fires once per server-pushed rep change (kills,
|
||||
// quest turn-ins, etc.). Reads count, loops `(flags, standing)`
|
||||
// pairs, calling `FUN_004D6330(slot, standing, 1 /*notify*/)` which
|
||||
// routes through `FUN_REPUTATION_FIRE_NOTIFY` for the
|
||||
// `CHAT_MSG_COMBAT_FACTION_CHANGE` dispatch.
|
||||
//
|
||||
// Also mutates AT_WAR / VISIBLE flag bits as standing crosses
|
||||
// thresholds (e.g., recovering to -2999 clears the force-set AT_WAR
|
||||
// from "below Hated"). **Vanilla doesn't fire `UNIT_FACTION` for
|
||||
// these side-effect flag flips**; modern 3.3.5 does. Polyfilled in
|
||||
// `src/faction/UnitFactionPolyfill.cpp` — snapshots the 64-entry
|
||||
// flag-byte array before/after and fires a single
|
||||
// `UNIT_FACTION("player")` if any bit changed.
|
||||
FUN_SMSG_SET_FACTION_STANDINGS = 0x004D5760,
|
||||
|
||||
// `__fastcall(ecx = factionID, edx = signedDelta)`. This is the
|
||||
// engine's "reputation changed, fire the chat event" notify
|
||||
// helper — it formats the localized chat message and dispatches
|
||||
@@ -2365,6 +2528,55 @@ enum Offsets {
|
||||
LUA_NEXT = 0x6F4450,
|
||||
LUA_ERROR = 0x6F4940,
|
||||
|
||||
// `lua_xmove(from, to, n)` — moves the top `n` TValues from one
|
||||
// thread to another. Decrements `from->top` by `n*16`, copies each
|
||||
// 16-byte TValue, increments `to->top`. Needed by the `coroutine.*`
|
||||
// trampolines to shuttle args/returns between the main state and a
|
||||
// suspended coroutine.
|
||||
LUA_X_MOVE = 0x6F2F80,
|
||||
// `lua_newthread(L)` — does GC check, calls inner `luaE_newthread`
|
||||
// at `0x006F6B10` to allocate a new state, pushes it onto L's stack
|
||||
// with type tag 8 (LUA_TTHREAD). Ghidra reports the return as
|
||||
// `void` (decompiler limitation — the inner call's eax flows
|
||||
// through), so callers should retrieve the new state via
|
||||
// `ToPointer(L, -1)` rather than relying on the return value.
|
||||
LUA_NEW_THREAD = 0x6F3030,
|
||||
// `lua_resume(L_co, nargs)` — runs a coroutine. Zero internal
|
||||
// callers in the engine (coroutine library was stripped from the
|
||||
// standard-library registration tables at build time); linker
|
||||
// kept the symbol so it's callable from C. Goes through
|
||||
// `luaD_rawrunprotected` at `0x006F5DAC` for setjmp/longjmp
|
||||
// safety; inner static `resume` helper lives at `0x006F67B0`.
|
||||
LUA_RESUME = 0x6F6620,
|
||||
// `lua_yield(L, nresults)` — flags the current CallInfo to yield
|
||||
// and returns -1. Sets a `0x10` bit on the CI->state field rather
|
||||
// than writing `L->status` directly in this build. Zero internal
|
||||
// callers (same reason as `lua_resume`). Errors with
|
||||
// "attempt to yield across metamethod/C-call boundary" or
|
||||
// "cannot yield a C function" when called from the wrong context.
|
||||
LUA_YIELD = 0x6F6860,
|
||||
// `lua_topointer(L, idx)` — returns the pointer value of a
|
||||
// pointer-valued TValue: type 5 (table), 6 (function), 8 (thread)
|
||||
// return `tvalue[2]` directly; type 2 (lightuserdata) returns its
|
||||
// value; type 7 (userdata) returns the userdata payload (record +
|
||||
// 0x10). Returns 0 for anything else. We use it as `lua_tothread`
|
||||
// — for a known-thread TValue, returns the embedded `lua_State *`.
|
||||
LUA_TO_POINTER = 0x6F3790,
|
||||
// `luaL_argerror(L, narg, msg)` — formats "bad argument #%d to
|
||||
// '%s' (%s)" (or "calling '%s' on bad self (%s)" for method-style
|
||||
// calls) and calls `lua_error`. Doesn't return.
|
||||
LUAL_ARG_ERROR = 0x6F4810,
|
||||
// `lua_iscfunction(L, idx)` — returns 1 if value is a function
|
||||
// closure with the `isC` byte set (`Closure.c.isC == 1`), 0
|
||||
// otherwise. Used by `coroutine.create` to reject C functions
|
||||
// (Lua 5.0 only allows Lua-defined functions as coroutine bodies).
|
||||
LUA_IS_C_FUNCTION = 0x6F34A0,
|
||||
// `lua_tothread(L, idx)` — returns the embedded `lua_State *` for
|
||||
// type-8 (LUA_TTHREAD) TValues, NULL for anything else. Used by
|
||||
// the `coroutine` library to extract the coroutine pointer from a
|
||||
// stack slot.
|
||||
LUA_TO_THREAD = 0x6F3770,
|
||||
|
||||
// Global `lua_State *`. The engine keeps one main thread state here; we
|
||||
// read it on demand in helpers that run outside a Lua callback (e.g.
|
||||
// RegisterTableFunction during LoadScriptFunctions).
|
||||
@@ -3142,6 +3354,174 @@ enum Offsets {
|
||||
VAR_LOOT_LOOTABLE = 0x00B71BA0,
|
||||
LOOT_SLOT_STRIDE = 0x1C,
|
||||
LOOT_MAX_SLOTS = 0x10,
|
||||
// Per-slot field offsets within a `VAR_LOOT_SLOTS + slot * 0x1C`
|
||||
// entry. Decoded from `Script_GetLootSlotInfo` and the populator
|
||||
// at `FUN_004C1CB0`:
|
||||
// +0x00 itemID (Script_GetLootSlotInfo's first push)
|
||||
// +0x04 displayInfoID (icon lookup via DBC index)
|
||||
// +0x08 count (FUN_004C22E0 reads `[base+0x8]`)
|
||||
// +0x0C ?
|
||||
// +0x10 ?
|
||||
// +0x14 wire-slot byte (the server-side slot index — what
|
||||
// `CMSG_LOOT_ITEM` expects; differs from the local
|
||||
// densely-packed index when the response had gaps)
|
||||
OFF_LOOT_SLOT_COUNT = 0x08,
|
||||
OFF_LOOT_SLOT_WIRE_INDEX = 0x14,
|
||||
|
||||
// `__fastcall(uint userFacingSlot, char *outBuf, int bufSize) →
|
||||
// const char *outBuf` — builds the loot-slot hyperlink string
|
||||
// (e.g. `|cff1eff00|Hitem:12345:0:0:0|h[Item Name]|h|r`) and
|
||||
// copies it into `outBuf`. Returns the buffer pointer on success,
|
||||
// `NULL` if no loot window is open, the slot is empty, or the
|
||||
// item's cache record isn't loaded yet. Used by
|
||||
// `Script_GetLootSlotLink` at `0x004C2D20`.
|
||||
//
|
||||
// `userFacingSlot` is the post-`__ftol`-decrement slot index — the
|
||||
// same shape Lua's `LootSlot(slot)` produces internally after
|
||||
// `dec eax`:
|
||||
// - When coin is present (`VAR_LOOT_LOOTABLE != 0`), input `0`
|
||||
// is the coin slot itself and returns NULL; subsequent
|
||||
// internal items live at `userFacingSlot = internal_slot + 1`
|
||||
// (the function dec's again inside).
|
||||
// - When no coin, `userFacingSlot = internal_slot` directly.
|
||||
// The internal `_GetRecord` call inside uses `callback = NULL`, so
|
||||
// the link is a pure cache hit — no network request gets queued
|
||||
// when the cache is cold, just a NULL return.
|
||||
FUN_LOOT_SLOT_LINK_BUILDER = 0x004C2670,
|
||||
|
||||
// `__stdcall(uint8_t slotByte)` — sends `CMSG_LOOT_ITEM` (opcode
|
||||
// `0x108`) carrying the wire-protocol slot byte for the currently-
|
||||
// open loot window. Built directly from the engine's packet
|
||||
// primitives — bypasses the `Script_LootSlot` Lua wrapper at
|
||||
// `FUN_004C2790`, which routes through a `param_2 != 0`
|
||||
// BoP-confirm-after-dialog branch that's of no use to us. The
|
||||
// server cheerfully takes whatever slot you send for the current
|
||||
// loot session; no client-side BoP confirmation gate.
|
||||
//
|
||||
// **Calling convention matters.** The callee reads its arg from
|
||||
// `[ebp+8]` (stack) and ends with `ret 4`. Mis-typedef'ing as
|
||||
// `__fastcall` produces a stack-corrupting caller — symptoms are
|
||||
// delayed crashes on unrelated vtable reads.
|
||||
FUN_CMSG_LOOT_ITEM = 0x005E1AD0,
|
||||
|
||||
// UNIT_DYNAMIC_FLAGS within `m_objectFields`. Standard
|
||||
// server-driven dynamic-state bitmask. Empirically verified by
|
||||
// cross-referencing three `Script_*` accessors that all read
|
||||
// bytes at `[m_objectFields + 0x224]`:
|
||||
// - `Script_UnitIsTapped` (`0x00519C90`): `test [eax+0x224], 0x04`
|
||||
// - `Script_UnitIsTappedByPlayer` (`0x00519D00`): `test [eax+0x224], 0x08`
|
||||
// - `Script_UnitIsDead` (`0x00517AC0`): `[eax+0x224] >> 5 & 1`
|
||||
// Bit values match CMaNGOS conventions: 0x01 LOOTABLE,
|
||||
// 0x02 TRACK_UNIT, 0x04 TAPPED, 0x08 TAPPED_BY_PLAYER,
|
||||
// 0x10 SPECIALINFO, 0x20 DEAD, 0x40 REFER_A_FRIEND_LINKED,
|
||||
// 0x80 ZONE_OUT. The +0x224 offset is significantly later than
|
||||
// CMaNGOS-doc'd vanilla (which puts DYNAMIC_FLAGS at field 0x46
|
||||
// = +0x118) — yet another non-uniform shift in 1.12.1's actual
|
||||
// UNIT_FIELD layout. Direct empirical read is the authority.
|
||||
OFF_UNIT_FIELD_DYNAMIC_FLAGS = 0x224,
|
||||
UNIT_DYNFLAG_LOOTABLE = 0x01,
|
||||
UNIT_DYNFLAG_DEAD = 0x20,
|
||||
|
||||
// UNIT_FIELD_BOUNDINGRADIUS within m_objectFields — single
|
||||
// float, the unit's interact-radius in world yards. Both
|
||||
// `FUN_005EC110` (interact-state check) and `FUN_005DF130`
|
||||
// (CMSG_LOOT sender) compute the interaction-range threshold
|
||||
// as `target_radius + player_radius + INTERACT_REACH`, clamped
|
||||
// to a floor of `MIN_INTERACT_RANGE`. Verified at `0x005EC1A4`
|
||||
// where the function reads
|
||||
// `[(target_m_objectFields) + 0x1F0]`.
|
||||
OFF_UNIT_FIELD_BOUNDING_RADIUS = 0x1F0,
|
||||
|
||||
// `__thiscall(player, target, useDistanceCheck)` — engine's
|
||||
// CMSG_LOOT sender for **dead mobs** (CGCreature_C with
|
||||
// DYNFLAG_LOOTABLE set). Pre-checks: `FUN_005EC110` interact
|
||||
// validity + `FUN_006003A0` lootability (target dead + bit 0 of
|
||||
// `m_objectFields[+0x224]` set) + local player not in a state
|
||||
// that forbids interaction. Releases any open loot window before
|
||||
// sending the new request.
|
||||
//
|
||||
// `useDistanceCheck` is the auto-walk gate:
|
||||
// - `1` (engine default): if target out of range, start
|
||||
// walking toward it and *don't* send CMSG_LOOT. If in range,
|
||||
// send normally.
|
||||
// - `0`: skip the range check; send immediately. The server
|
||||
// enforces server-side range, so OOR sends silently fail
|
||||
// rather than walking the player toward distant corpses —
|
||||
// the right pick for AoE-loot-style callers.
|
||||
//
|
||||
// Engine callsite at `FUN_0060FA20` (right-click world handler)
|
||||
// resolves the target via
|
||||
// `ClntObjMgrObjectPtr(TYPEMASK_UNIT, ..., guid)` first, same as
|
||||
// we do.
|
||||
//
|
||||
// Distinct from the sibling sender at `0x005DF130`, which gates
|
||||
// on `FUN_CGCORPSE_IS_LOOTABLE` (player corpses only) instead of
|
||||
// the mob path. The CGCorpse helper is documented immediately
|
||||
// below.
|
||||
FUN_CMSG_LOOT_UNIT = 0x005DF2A0,
|
||||
|
||||
// `__fastcall(int loot_response_struct, undefined4 coin, int unk)`
|
||||
// — master loot-window state controller. Called by the
|
||||
// `SMSG_LOOT_RESPONSE` handler tail (via `FUN_0048F3A0`) and by
|
||||
// `CloseLoot`-inner (via `FUN_0048F200(0, 0, 0)`). Two phases:
|
||||
// - **Close prior**: if `VAR_LOOT_GUID_LO/HI != 0`, clears
|
||||
// them, releases item-cache callbacks for the prior slots,
|
||||
// fires `LOOT_CLOSED` (event id `0x10D`).
|
||||
// - **Open new**: if `param_1 != 0`, copies the GUID from
|
||||
// `[param_1 + 8]` into `VAR_LOOT_GUID_LO/HI`, zeroes
|
||||
// `VAR_LOOT_SLOTS`, walks 16 slots populating itemID via
|
||||
// `FUN_005EBC90(slot)` and sibling helpers, sets
|
||||
// `VAR_LOOT_LOOTABLE = param_2` (coin), then fires
|
||||
// `LOOT_OPENED` (event id `0x10B`).
|
||||
// Both event fires go through `FUN_FIRE_EVENT_NO_ARGS` —
|
||||
// intercepting that function lets a caller swallow the events
|
||||
// without touching this one.
|
||||
FUN_LOOT_CONTROLLER = 0x004C1CB0,
|
||||
|
||||
// `__fastcall(int eventID)` — engine's no-args event dispatcher,
|
||||
// the lighter-weight sibling of `FUN_FIRE_EVENT` (variadic).
|
||||
// Called from ~100 sites across the engine, including the
|
||||
// `LOOT_OPENED` / `LOOT_CLOSED` fires inside
|
||||
// `FUN_LOOT_CONTROLLER`. Hooking it lets us conditionally swallow
|
||||
// specific event ids while leaving the rest untouched — the
|
||||
// mechanism we use to do silent scan loops without flickering
|
||||
// the `LootFrame`. Fan-in is wide but call frequency is
|
||||
// event-driven (state changes only), not per-frame, so per-call
|
||||
// overhead from a hooked predicate is negligible.
|
||||
FUN_FIRE_EVENT_NO_ARGS = 0x00703E50,
|
||||
|
||||
// `__fastcall(int sendRelease, char earlyReturnOnGameObject,
|
||||
// char showError)` — engine's "close the current loot window"
|
||||
// inner. Behavior keyed off args:
|
||||
// - `sendRelease` (ECX): if non-zero, builds and sends
|
||||
// `CMSG_LOOT_RELEASE` (opcode `0x15F`) for the current loot
|
||||
// target before clearing client state.
|
||||
// - `earlyReturnOnGameObject` (EDX): if non-zero, bails early
|
||||
// when the loot target is a game object that's been
|
||||
// interacted with in a particular way (fishing-style?).
|
||||
// - `showError` (stack): if non-zero, calls `FUN_00496720(0x87)`
|
||||
// to display a "cannot close loot" error.
|
||||
// Side effects: clears `[player + 0x1D28/+0x1D2C]` (the player's
|
||||
// prior-interact GUID slots), then calls `FUN_004C1CB0(0, 0, 0)`
|
||||
// which clears `VAR_LOOT_GUID_LO/HI`, zeroes `VAR_LOOT_SLOTS`,
|
||||
// and fires the `LOOT_CLOSED` event (`0x10D`). Use
|
||||
// `(1, 0, 0)` for a clean release + clear.
|
||||
FUN_CLOSE_LOOT_INNER = 0x0048F200,
|
||||
|
||||
// `CGCorpse::IsLootable(this)` — `__fastcall(corpse) → uint`,
|
||||
// tests bit 0 of `[corpse_m_objectFields + 0x78]`. Specific to
|
||||
// `TYPEMASK_CORPSE` (player corpses) — `+0x78` of a CGCorpse_C's
|
||||
// m_objectFields holds dynamic flags with bit 0 = LOOTABLE. The
|
||||
// same offset in CGUnit_C / CGPlayer_C's m_objectFields holds
|
||||
// BYTES_0 (race / class / gender / power type), so this helper
|
||||
// returns nonsense for live units (e.g. it returns 1 for Human
|
||||
// players because race=1 has bit 0 = 1). Used by the engine's
|
||||
// right-click-on-corpse handler at `FUN_005D6BF0` to gate
|
||||
// CMSG_LOOT for player corpses, and from `FUN_005DF130` (one of
|
||||
// two CMSG_LOOT senders) for the same corpse path. Don't reuse
|
||||
// for live-unit lootability — use `OFF_UNIT_FIELD_DYNAMIC_FLAGS`
|
||||
// bit 0 against a CGUnit instead.
|
||||
FUN_CGCORPSE_IS_LOOTABLE = 0x005D6E20,
|
||||
|
||||
// Merchant inventory: flat array at `0xBDD118`, stride 0x1C,
|
||||
// itemID at `entry+4`. The "flag" globals are actually the
|
||||
@@ -3242,10 +3622,37 @@ enum Offsets {
|
||||
// `+0x120`. (The engine's `Script_GetInboxItem` passes a per-
|
||||
// entry completion callback at `0x004AF0A0` to GetRecord; for
|
||||
// ID-only retrieval we skip the callback.)
|
||||
//
|
||||
// Entries also carry per-instance modifiers at `+0x12C`
|
||||
// (enchantID), `+0x134` (suffix factor), `+0x138` (random
|
||||
// property ID), etc. — `Script_GameTooltip_SetInboxItem` at
|
||||
// `0x005354C0` copies them onto the tooltip object to render a
|
||||
// fully-decorated tooltip. We don't expose them in
|
||||
// `GetInboxItemLink` because 3.3.5's same function only emits
|
||||
// basic itemID-only links (`FUN_0061E290(itemID)`); per-instance
|
||||
// data only fully manifests when the player takes the item and
|
||||
// the engine spawns a real CGItem.
|
||||
VAR_INBOX_ENTRIES = 0x00B6EF54,
|
||||
VAR_INBOX_COUNT = 0x00B6EFC0,
|
||||
OFF_INBOX_ENTRY_ITEM_ID = 0x120,
|
||||
|
||||
// SendMail attached item — the engine stores the attached item's
|
||||
// 64-bit GUID at these globals when the player drops an item onto
|
||||
// the SendMail slot (`SetSendMailItem` / `ClickSendMailItemButton`).
|
||||
// Cleared to zero by `ClearSendMail` and on `MAIL_CLOSED`. Vanilla
|
||||
// allows exactly one attachment per outgoing mail (modern WoW lifted
|
||||
// this to 12+; we ignore the modern `attachmentIndex` arg since only
|
||||
// index 1 is meaningful).
|
||||
//
|
||||
// The attached item still exists as a live CGItem in the player's
|
||||
// inventory until the mail is actually sent, so the GUID resolves
|
||||
// via `FUN_GET_OBJECT_BY_GUID(OBJECT_TYPE_ITEM, ...)`. That lets
|
||||
// `GetSendMailItemLink` return the full per-instance link (enchant
|
||||
// / random suffix preserved) by feeding the resolved CGItem to
|
||||
// `Item::Link::FromCGItem`.
|
||||
VAR_SEND_MAIL_ITEM_GUID_LO = 0x00B6EF90,
|
||||
VAR_SEND_MAIL_ITEM_GUID_HI = 0x00B6EF94,
|
||||
|
||||
// Craft window (engineering, alchemy, etc. before the "trade
|
||||
// skill" overhaul). Same slot-indirection shape as inbox: the
|
||||
// address holds a pointer to the heap-allocated entry-pointer
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
// Same pattern WeirdUtils.dll uses (we reverse-engineered it from
|
||||
// `0x10015A74` while figuring this out). The addon load pass then
|
||||
// reads each `.lua` file through the same hooked read path —
|
||||
// `GlobalStrings.lua`, `Util\Print.lua`, etc. all surface from
|
||||
// `Compat.lua`, `Util\Print.lua`, etc. all surface from
|
||||
// embedded buffers.
|
||||
|
||||
#include "embedded_classicapi.h"
|
||||
|
||||
+11
-4
@@ -110,6 +110,13 @@ const uint8_t *FindPlayerBuffEntry(uint32_t spellID) {
|
||||
// what Lua's `GetTime()` returns (engineMs * 0.001), so converting
|
||||
// `expirationMs * 0.001` lands directly on the same timeline addons
|
||||
// compare against on the Lua side. No epoch reconciliation needed.
|
||||
//
|
||||
// **Read as `uint32_t`**: GetTickCount overflows `INT_MAX` at ~24.86
|
||||
// days of system uptime. A signed read flips negative past that
|
||||
// point, which broke a `<= 0` early-bail that was supposed to catch
|
||||
// only the "no timing" (0) sentinel. The engine's own arithmetic
|
||||
// works on the unsigned tick because both sides of `expiration -
|
||||
// currentMs` wrap together.
|
||||
double PlayerBuffExpirationSeconds(const uint8_t *entry) {
|
||||
if (entry == nullptr)
|
||||
return 0.0;
|
||||
@@ -117,12 +124,12 @@ double PlayerBuffExpirationSeconds(const uint8_t *entry) {
|
||||
entry + Offsets::OFF_PLAYER_BUFF_SLOT_CODE);
|
||||
if (slotCode < 0)
|
||||
return 0.0;
|
||||
auto *expirationTable = reinterpret_cast<const int *>(
|
||||
auto *expirationTable = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_BUFF_EXPIRATION_TABLE));
|
||||
const int expirationMs = expirationTable[slotCode];
|
||||
if (expirationMs <= 0)
|
||||
const uint32_t expirationMs = expirationTable[slotCode];
|
||||
if (expirationMs == 0)
|
||||
return 0.0;
|
||||
return static_cast<double>(static_cast<uint32_t>(expirationMs)) * 0.001;
|
||||
return static_cast<double>(expirationMs) * 0.001;
|
||||
}
|
||||
|
||||
// Computes the spell's base duration in seconds via Spell.dbc →
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
// 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/>.
|
||||
//
|
||||
// Restores Lua 5.0's stripped `coroutine.*` library to the in-game Lua
|
||||
// state. WoW 1.12.1 ships with the C-level coroutine machinery linked
|
||||
// in (`lua_resume`, `lua_yield`, `lua_newthread`, threads as a real
|
||||
// type tag 8) but the `coroutine` global table was never registered.
|
||||
// The five trampolines below mirror Lua 5.0's `lbaselib.c` shapes,
|
||||
// cross-checked against 5.4.8's wired-up library via Ghidra (see the
|
||||
// "Cross-binary reference" technique in CLAUDE.md).
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Coroutine {
|
||||
|
||||
// Lua 5.0 result codes returned by `lua_resume`.
|
||||
static constexpr int LUA_OK = 0;
|
||||
static constexpr int LUA_YIELD = 1;
|
||||
|
||||
// Field offsets in `lua_State`, confirmed from `lua_resume`'s error
|
||||
// cleanup path (`L->ci = L->base_ci; L->base = base_ci->base`):
|
||||
// *(L+0x14) = *(L+0x28)
|
||||
// *(L+0x0C) = **(L+0x28)
|
||||
static constexpr uintptr_t OFF_L_CI = 0x14;
|
||||
static constexpr uintptr_t OFF_L_BASE_CI = 0x28;
|
||||
|
||||
// Reads `L->ci` (the current CallInfo). When greater than `L->base_ci`
|
||||
// the coroutine's body function is mid-execution (paused at a yield);
|
||||
// when equal, no Lua function is on this thread's call stack.
|
||||
static void *LuaCI(void *L) {
|
||||
return *reinterpret_cast<void **>(reinterpret_cast<uintptr_t>(L) + OFF_L_CI);
|
||||
}
|
||||
static void *LuaBaseCI(void *L) {
|
||||
return *reinterpret_cast<void **>(reinterpret_cast<uintptr_t>(L) + OFF_L_BASE_CI);
|
||||
}
|
||||
|
||||
// Coroutine status codes returned by `CoStatus` below — fed into the
|
||||
// `kStatusNames` table for the `status` API. The dense 0..3 encoding
|
||||
// also lets us label "unsuspended" states for resume's error path.
|
||||
static constexpr int CO_RUNNING = 0;
|
||||
static constexpr int CO_SUSPENDED = 1;
|
||||
static constexpr int CO_NORMAL = 2;
|
||||
static constexpr int CO_DEAD = 3;
|
||||
|
||||
static const char *const kStatusNames[] = {
|
||||
"running",
|
||||
"suspended",
|
||||
"normal",
|
||||
"dead",
|
||||
};
|
||||
|
||||
// Inner status classifier. Equivalent to Lua 5.0's `auxstatus` from
|
||||
// lbaselib.c, but we can't rely on `L->status` here: this build's
|
||||
// `lua_yield` doesn't write status (it sets a CI state flag bit
|
||||
// `0x10` instead), so the canonical "ttype(status) == LUA_YIELD"
|
||||
// check fails. We use the `ci > base_ci` signal instead — true iff
|
||||
// a Lua function is currently paused on this thread's call stack.
|
||||
//
|
||||
// State map:
|
||||
// co->ci > co->base_ci → SUSPENDED (yielded; body paused)
|
||||
// co->ci == co->base_ci, top==0 → DEAD (returned, all consumed)
|
||||
// co->ci == co->base_ci, top>0 → SUSPENDED (initial: fn pushed,
|
||||
// never resumed)
|
||||
// After AuxResume's XMove cycle, both "returned" and "yielded" leave
|
||||
// co's stack empty; the ci comparison is what distinguishes them.
|
||||
// "Normal" (coroutine that resumed a deeper coroutine — appears as
|
||||
// the parent in the call chain) is rare and would require walking
|
||||
// the global thread list; we collapse it into "suspended".
|
||||
static int CoStatus(void *L, void *co) {
|
||||
if (co == L)
|
||||
return CO_RUNNING;
|
||||
if (LuaCI(co) > LuaBaseCI(co))
|
||||
return CO_SUSPENDED;
|
||||
return Game::Lua::GetTop(co) == 0 ? CO_DEAD : CO_SUSPENDED;
|
||||
}
|
||||
|
||||
// Common back-end for resume / wrap. Returns nres ≥ 0 on success
|
||||
// (with values already xmove'd onto L), or -1 on error (with the
|
||||
// error message left on L's stack).
|
||||
//
|
||||
// WoW's modified Lua doesn't follow the standard `lua_resume` status
|
||||
// contract: it returns `0` for normal completion and `1` for both
|
||||
// "yielded" AND "errored mid-execution". We disambiguate the two
|
||||
// `s==1` cases by checking the call-stack depth — a yielded
|
||||
// coroutine still has its body paused (`ci > base_ci`), while an
|
||||
// errored coroutine has been unwound back to `base_ci`. Errors hit
|
||||
// from the "dead/already-resumed" early-out (e.g. the inner
|
||||
// `cannot resume dead coroutine` path) also land in `s != 0` with
|
||||
// `ci == base_ci`, so the same branch covers them.
|
||||
static int AuxResume(void *L, void *co, int nargs) {
|
||||
const int st = CoStatus(L, co);
|
||||
if (st != CO_SUSPENDED) {
|
||||
Game::Lua::PushString(L, st == CO_DEAD
|
||||
? "cannot resume dead coroutine"
|
||||
: "cannot resume non-suspended coroutine");
|
||||
return -1;
|
||||
}
|
||||
Game::Lua::XMove(L, co, nargs);
|
||||
const int s = Game::Lua::Resume(co, nargs);
|
||||
const bool yielded = LuaCI(co) > LuaBaseCI(co);
|
||||
if (s == LUA_OK || yielded) {
|
||||
const int nres = Game::Lua::GetTop(co);
|
||||
Game::Lua::XMove(co, L, nres);
|
||||
return nres;
|
||||
}
|
||||
// Error path: engine left a single error message on co's stack.
|
||||
Game::Lua::XMove(co, L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Create(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_FUNCTION || Game::Lua::IsCFunction(L, 1)) {
|
||||
Game::Lua::ArgError(L, 1, "Lua function expected");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::NewThread(L); // [arg1, NL]
|
||||
void *NL = Game::Lua::ToThread(L, -1);
|
||||
if (NL == nullptr) {
|
||||
Game::Lua::Error(L, "coroutine.create: failed to allocate thread");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::PushValue(L, 1); // [arg1, NL, fn]
|
||||
Game::Lua::XMove(L, NL, 1); // [arg1, NL]; NL has fn
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Yield(void *L) {
|
||||
return Game::Lua::Yield(L, Game::Lua::GetTop(L));
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Resume(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_THREAD) {
|
||||
Game::Lua::ArgError(L, 1, "coroutine expected");
|
||||
return 0; // unreachable
|
||||
}
|
||||
void *co = Game::Lua::ToThread(L, 1);
|
||||
const int nargs = Game::Lua::GetTop(L) - 1;
|
||||
const int r = AuxResume(L, co, nargs);
|
||||
if (r < 0) {
|
||||
// Error message is on top of L's stack. Push `false` and
|
||||
// shuffle it below the message: [..., msg] -> [..., false, msg].
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::Insert(L, -2);
|
||||
return 2;
|
||||
}
|
||||
// Success: `r` values on top of L's stack. Push `true` and slide
|
||||
// it into position so returns are [..., true, v1, v2, ..., vr].
|
||||
Game::Lua::PushBool(L, true);
|
||||
Game::Lua::Insert(L, -(r + 1));
|
||||
return r + 1;
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Status(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_THREAD) {
|
||||
Game::Lua::ArgError(L, 1, "coroutine expected");
|
||||
return 0; // unreachable
|
||||
}
|
||||
void *co = Game::Lua::ToThread(L, 1);
|
||||
Game::Lua::PushString(L, kStatusNames[CoStatus(L, co)]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `coroutine.wrap`'s returned closure. The captured coroutine lives
|
||||
// in upvalue 1; each invocation forwards args via resume and either
|
||||
// returns the yielded/returned values directly (no { ok, ... }
|
||||
// wrapping like resume does) or re-raises on error.
|
||||
static int __fastcall WrapAux(void *L) {
|
||||
void *co = Game::Lua::ToThread(L, Game::Lua::UpvalueIndex(1));
|
||||
if (co == nullptr) {
|
||||
Game::Lua::Error(L, "coroutine.wrap: missing thread upvalue");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const int nargs = Game::Lua::GetTop(L);
|
||||
const int r = AuxResume(L, co, nargs);
|
||||
if (r < 0) {
|
||||
// The error message is at -1. `lua_error`'s 1.12 wrapper takes
|
||||
// a printf-format string + va args; we pass `%s` so any `%`
|
||||
// characters in the message are treated as literal.
|
||||
const char *msg = Game::Lua::ToString(L, -1);
|
||||
Game::Lua::Error(L, "%s", msg != nullptr ? msg : "coroutine error");
|
||||
return 0; // unreachable
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Wrap(void *L) {
|
||||
// Reuse cocreate to build the thread (validates arg, allocates,
|
||||
// moves fn into the new state). Leaves the thread on top of L.
|
||||
Script_Coroutine_Create(L);
|
||||
Game::Lua::PushCClosure(L, &WrapAux, 1); // captures thread as upvalue 1
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("coroutine", "create", &Script_Coroutine_Create);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "resume", &Script_Coroutine_Resume);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "yield", &Script_Coroutine_Yield);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "status", &Script_Coroutine_Status);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "wrap", &Script_Coroutine_Wrap);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Coroutine
|
||||
@@ -105,6 +105,23 @@ int Lookup(const char *name) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int LookupByName(const char *name) {
|
||||
if (name == nullptr)
|
||||
return -1;
|
||||
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || count <= 0)
|
||||
return -1;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const char *entryName = *reinterpret_cast<const char *const *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE +
|
||||
Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (entryName != nullptr && std::strcmp(entryName, name) == 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void RetryClaims() {
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
if (g_reserved[i].slot < 0)
|
||||
|
||||
@@ -47,8 +47,22 @@ struct AutoReserve {
|
||||
// claimed (e.g. before the first Lua-side `RegisterEvent` has
|
||||
// triggered `RetryClaims`). Slot indices may change across `/reload`,
|
||||
// so call this at fire time rather than caching the value.
|
||||
//
|
||||
// Only consults our reservation cache — for engine-defined events
|
||||
// (UNIT_FACTION, BAG_UPDATE, etc.) use `LookupByName`.
|
||||
int Lookup(const char *name);
|
||||
|
||||
// Walks the live engine event table at `[VAR_EVENT_TABLE_BASE_PTR]`
|
||||
// looking for an entry whose name strcmps equal to `name`. Returns
|
||||
// the slot index, or -1 if no entry matches.
|
||||
//
|
||||
// Works for both engine-defined events AND our `AutoReserve`-claimed
|
||||
// custom events (both populate the same table). Slightly slower than
|
||||
// `Lookup` (O(table size) vs O(reservations)) — prefer `Lookup` for
|
||||
// names we've reserved. Use this for engine-defined events we want to
|
||||
// fire ourselves (e.g., polyfilling missing event dispatches).
|
||||
int LookupByName(const char *name);
|
||||
|
||||
// Dispatches a custom event via the engine's printf-style event
|
||||
// dispatcher at `FUN_FIRE_EVENT` (`0x00703F50`). `format` is a
|
||||
// concatenation of `%d` (int), `%u` (uint), `%f` (double), `%s`
|
||||
@@ -74,6 +88,22 @@ inline void Fire(int eventID, const char *format, Args... args) {
|
||||
fn(eventID, format, args...);
|
||||
}
|
||||
|
||||
// Convenience for the modern WoW `(id: number, success: bool)` event
|
||||
// shape (ITEM_DATA_LOAD_RESULT, GET_ITEM_INFO_RECEIVED,
|
||||
// QUEST_DATA_LOAD_RESULT, …). The engine's printf-style dispatcher has
|
||||
// no `%b`, so a naive `%d%d` with `0` for failure surfaces `arg2 = 0` —
|
||||
// truthy in Lua, breaking the canonical `if success then` branch. We
|
||||
// push `1` for success and `nil` for failure instead, leaning on the
|
||||
// engine's `lua_pushstring(L, NULL) → lua_pushnil` tail-jump so the
|
||||
// dispatcher itself emits the nil without any pre-staging. Lua handlers
|
||||
// can then use the natural idiom: nil is falsy, `1` is truthy.
|
||||
inline void FireIdSuccess(int eventID, int id, bool success) {
|
||||
if (success)
|
||||
Fire(eventID, "%d%d", id, 1);
|
||||
else
|
||||
Fire(eventID, "%d%s", id, static_cast<const char *>(nullptr));
|
||||
}
|
||||
|
||||
// Internal: try to claim a slot for every reservation that's still
|
||||
// unclaimed (`slot < 0`). Called from the `Frame::RegisterEvent` hook
|
||||
// in DllMain — every time Lua calls `frame:RegisterEvent(...)`, we
|
||||
|
||||
+2
-28
@@ -12,36 +12,10 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include "event/Custom.h"
|
||||
|
||||
namespace Event::Util {
|
||||
|
||||
// Walks the same `EventEntry` array that `Frame::RegisterEvent` strcmps
|
||||
// against (base at `[VAR_EVENT_TABLE_BASE_PTR]`, count at
|
||||
// `[VAR_EVENT_TABLE_COUNT]`, stride 0x10, name field at +0x00). This is
|
||||
// the source of truth for "events the engine will accept registration
|
||||
// for", so it picks up anything we've injected via `Event::Custom::Register`
|
||||
// in addition to all built-in events.
|
||||
static bool IsKnownEventName(const char *needle) {
|
||||
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || count <= 0)
|
||||
return false;
|
||||
for (int i = 0; i < count; i++) {
|
||||
const uint8_t *entry = base + i * Offsets::EVENT_ENTRY_STRIDE;
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
entry + Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (name == nullptr)
|
||||
continue;
|
||||
if (std::strcmp(name, needle) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int __fastcall Script_IsEventValid(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_STRING) {
|
||||
Game::Lua::Error(L, "Usage: C_EventUtils.IsEventValid(eventName)");
|
||||
@@ -52,7 +26,7 @@ static int __fastcall Script_IsEventValid(void *L) {
|
||||
Game::Lua::PushBool(L, 0);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBoolean(L, IsKnownEventName(needle));
|
||||
Game::Lua::PushBoolean(L, Event::Custom::LookupByName(needle) >= 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
// 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/>.
|
||||
|
||||
// Restores `UNIT_FACTION("player")` event semantics that vanilla 1.12.1
|
||||
// silently dropped from four engine paths. In 3.3.5 each of these calls
|
||||
// `FUN_0071F8F0(player, 0) → FUN_0060BF10(playerGUID, UNIT_FACTION_id)`
|
||||
// which broadcasts the event for every unit token referencing the
|
||||
// local player; vanilla just returns. We hook each path and fire
|
||||
// `UNIT_FACTION("player")` post-original via the engine's printf-style
|
||||
// dispatcher.
|
||||
//
|
||||
// Why one file and not four: every hook does the same shape work
|
||||
// (snapshot rep-slot state, run original, compare, fire). The shared
|
||||
// `LookupEngineEvent` + `FireUnitFactionPlayer` helpers are the value;
|
||||
// splitting four 30-line hooks into four files would just spread that
|
||||
// helper around.
|
||||
//
|
||||
// Why hook the inner setters (vs. `Script_*` wrappers): same coverage,
|
||||
// and any future C++ caller of the setter inherits the polyfill.
|
||||
//
|
||||
// Coverage map:
|
||||
//
|
||||
// 1. `FUN_FACTION_SET_AT_WAR` — `FactionToggleAtWar` toggles
|
||||
// AT_WAR. Hook fires UNIT_FACTION if the flag byte changed.
|
||||
//
|
||||
// 2. `FUN_FACTION_SET_INACTIVE` — `SetFactionInactive` /
|
||||
// `SetFactionActive` toggle INACTIVE. Same change-detection
|
||||
// shape as at-war.
|
||||
//
|
||||
// 3. `FUN_SMSG_SET_FACTION_STANDINGS` — server rep update.
|
||||
// The handler mutates AT_WAR / VISIBLE bits as standing crosses
|
||||
// thresholds (recovery from below-Hated clears the force-set
|
||||
// AT_WAR). Snapshots the 64-entry flag-byte array before/after
|
||||
// and fires once if any byte changed.
|
||||
//
|
||||
// 4. `FUN_SMSG_INITIALIZE_FACTIONS` — server initial sync at login.
|
||||
// Fires unconditionally after the engine populates the rep table.
|
||||
// Lets addons that observe UNIT_FACTION (rather than
|
||||
// PLAYER_LOGIN / VARIABLES_LOADED) see the initial state.
|
||||
//
|
||||
// 5. `FUN_SMSG_SET_FACTION_ATWAR` — server force-change of AT_WAR
|
||||
// (e.g., faction reset, server-side state change). Fires
|
||||
// unconditionally after the engine writes the slot byte, matching
|
||||
// 3.3.5's behavior — server-pushed packets are infrequent and
|
||||
// always indicate a state change worth signaling.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Faction::UnitFactionPolyfill {
|
||||
|
||||
namespace {
|
||||
|
||||
// Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// `UNIT_FACTION`'s event-table slot. The engine's `RebuildEventTable`
|
||||
// preserves slot order across reloads and other DLLs claim only NULL
|
||||
// gaps, so this is stable once resolved. Lazy-init on first fire.
|
||||
int s_unitFactionSlot = -1;
|
||||
|
||||
void FireUnitFactionPlayer() {
|
||||
if (s_unitFactionSlot < 0)
|
||||
s_unitFactionSlot = Event::Custom::LookupByName("UNIT_FACTION");
|
||||
if (s_unitFactionSlot < 0)
|
||||
return;
|
||||
using FireEventFn_t = void(__cdecl *)(int eventID, const char *format, ...);
|
||||
auto fire = reinterpret_cast<FireEventFn_t>(Offsets::FUN_FIRE_EVENT);
|
||||
fire(s_unitFactionSlot, "%s", "player");
|
||||
}
|
||||
|
||||
uint8_t ReadRepSlotFlags(int repListID) {
|
||||
return *reinterpret_cast<uint8_t *>(
|
||||
Offsets::VAR_PLAYER_REP_SLOTS +
|
||||
repListID * Offsets::REP_SLOT_STRIDE +
|
||||
Offsets::OFF_REP_SLOT_FLAGS);
|
||||
}
|
||||
|
||||
// Resolves `factionID → repListID` (0..63 rep-slot index in
|
||||
// `VAR_PLAYER_REP_SLOTS`) by reading the `+0x04` field on the
|
||||
// `Faction.dbc` record directly. Same value the engine's helper at
|
||||
// `0x004D5600` returns — it just dereferences the same DBC field after
|
||||
// its own bounds check. Returns -1 for factionIDs that don't resolve
|
||||
// to a record (out of range / empty slot) or are header categories
|
||||
// (record stores -1 in the field).
|
||||
int FactionIDToRepListID(int factionID) {
|
||||
if (factionID <= 0)
|
||||
return -1;
|
||||
const auto *record = DBC::Record(Offsets::VAR_FACTION_DBC_RECORDS,
|
||||
Offsets::VAR_FACTION_DBC_COUNT,
|
||||
static_cast<uint32_t>(factionID));
|
||||
if (record == nullptr)
|
||||
return -1;
|
||||
return *reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_FACTION_REP_LIST_INDEX);
|
||||
}
|
||||
|
||||
// Hook 1 — at-war setter ──────────────────────────────────────────────
|
||||
|
||||
using SetAtWar_t = void(__fastcall *)(int factionID, int newState);
|
||||
SetAtWar_t s_setAtWar_o = nullptr;
|
||||
|
||||
void __fastcall SetAtWar_h(int factionID, int newState) {
|
||||
const int repListID = FactionIDToRepListID(factionID);
|
||||
const bool slotValid = repListID >= 0 && repListID < Offsets::MAX_REP_SLOTS;
|
||||
const uint8_t beforeFlags = slotValid ? ReadRepSlotFlags(repListID) : 0;
|
||||
|
||||
s_setAtWar_o(factionID, newState);
|
||||
|
||||
if (!slotValid)
|
||||
return;
|
||||
if (ReadRepSlotFlags(repListID) == beforeFlags)
|
||||
return; // engine bailed (looting / PEACE_FORCED no-op / standing block)
|
||||
|
||||
FireUnitFactionPlayer();
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAtWar{
|
||||
Offsets::FUN_FACTION_SET_AT_WAR,
|
||||
reinterpret_cast<void *>(&SetAtWar_h),
|
||||
reinterpret_cast<void **>(&s_setAtWar_o)};
|
||||
|
||||
// Hook 2 — inactive setter ────────────────────────────────────────────
|
||||
|
||||
using SetInactive_t = void(__fastcall *)(int factionID, int newState);
|
||||
SetInactive_t s_setInactive_o = nullptr;
|
||||
|
||||
void __fastcall SetInactive_h(int factionID, int newState) {
|
||||
const int repListID = FactionIDToRepListID(factionID);
|
||||
const bool slotValid = repListID >= 0 && repListID < Offsets::MAX_REP_SLOTS;
|
||||
const uint8_t beforeFlags = slotValid ? ReadRepSlotFlags(repListID) : 0;
|
||||
|
||||
s_setInactive_o(factionID, newState);
|
||||
|
||||
if (!slotValid)
|
||||
return;
|
||||
if (ReadRepSlotFlags(repListID) == beforeFlags)
|
||||
return; // no-op (already in requested state)
|
||||
|
||||
FireUnitFactionPlayer();
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookInactive{
|
||||
Offsets::FUN_FACTION_SET_INACTIVE,
|
||||
reinterpret_cast<void *>(&SetInactive_h),
|
||||
reinterpret_cast<void **>(&s_setInactive_o)};
|
||||
|
||||
// Hook 3 — SMSG_SET_FACTION_STANDINGS ─────────────────────────────────
|
||||
|
||||
// Snapshot all 64 rep-slot flag bytes into a packed buffer. The engine's
|
||||
// per-rep-change handler mutates AT_WAR / VISIBLE bits as standing
|
||||
// crosses thresholds, so we need a delta-detect across the whole table
|
||||
// (single rep change may flip flags on an unrelated slot — and the SMSG
|
||||
// can carry multiple rep updates in one packet).
|
||||
struct RepFlagsSnapshot {
|
||||
uint8_t flags[Offsets::MAX_REP_SLOTS];
|
||||
};
|
||||
|
||||
void TakeSnapshot(RepFlagsSnapshot *out) {
|
||||
for (int i = 0; i < Offsets::MAX_REP_SLOTS; ++i)
|
||||
out->flags[i] = ReadRepSlotFlags(i);
|
||||
}
|
||||
|
||||
bool SnapshotsDiffer(const RepFlagsSnapshot *a, const RepFlagsSnapshot *b) {
|
||||
return std::memcmp(a->flags, b->flags, sizeof(a->flags)) != 0;
|
||||
}
|
||||
|
||||
using SmsgHandler_t = int(__stdcall *)(uint32_t opcode, void *packet);
|
||||
|
||||
SmsgHandler_t s_smsgSetStandings_o = nullptr;
|
||||
|
||||
int __stdcall SmsgSetStandings_h(uint32_t opcode, void *packet) {
|
||||
RepFlagsSnapshot before;
|
||||
TakeSnapshot(&before);
|
||||
|
||||
const int result = s_smsgSetStandings_o(opcode, packet);
|
||||
|
||||
RepFlagsSnapshot after;
|
||||
TakeSnapshot(&after);
|
||||
if (SnapshotsDiffer(&before, &after))
|
||||
FireUnitFactionPlayer();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookSetStandings{
|
||||
Offsets::FUN_SMSG_SET_FACTION_STANDINGS,
|
||||
reinterpret_cast<void *>(&SmsgSetStandings_h),
|
||||
reinterpret_cast<void **>(&s_smsgSetStandings_o)};
|
||||
|
||||
// Hook 4 — SMSG_INITIALIZE_FACTIONS ──────────────────────────────────
|
||||
|
||||
SmsgHandler_t s_smsgInitFactions_o = nullptr;
|
||||
|
||||
int __stdcall SmsgInitFactions_h(uint32_t opcode, void *packet) {
|
||||
const int result = s_smsgInitFactions_o(opcode, packet);
|
||||
// Initial sync — engine has just populated all 64 rep slots from
|
||||
// server state. Always fire so addons listening for UNIT_FACTION
|
||||
// (rather than PLAYER_LOGIN / VARIABLES_LOADED) see the initial
|
||||
// load. Modern WoW's behavior.
|
||||
FireUnitFactionPlayer();
|
||||
return result;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookInitFactions{
|
||||
Offsets::FUN_SMSG_INITIALIZE_FACTIONS,
|
||||
reinterpret_cast<void *>(&SmsgInitFactions_h),
|
||||
reinterpret_cast<void **>(&s_smsgInitFactions_o)};
|
||||
|
||||
// Hook 5 — SMSG_SET_FACTION_ATWAR ─────────────────────────────────────
|
||||
|
||||
SmsgHandler_t s_smsgSetAtWar_o = nullptr;
|
||||
|
||||
int __stdcall SmsgSetAtWar_h(uint32_t opcode, void *packet) {
|
||||
const int result = s_smsgSetAtWar_o(opcode, packet);
|
||||
// Server-pushed AT_WAR change — packet is rare and always carries
|
||||
// intent to update. Match 3.3.5's behavior: fire unconditionally
|
||||
// after the byte write, without delta-detecting against the slot
|
||||
// (the engine writes the byte regardless of whether it differs, so
|
||||
// a delta check would conflate "value didn't change" with "server
|
||||
// had nothing to say" — but the server only sends this when
|
||||
// something did).
|
||||
FireUnitFactionPlayer();
|
||||
return result;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookSetAtWar{
|
||||
Offsets::FUN_SMSG_SET_FACTION_ATWAR,
|
||||
reinterpret_cast<void *>(&SmsgSetAtWar_h),
|
||||
reinterpret_cast<void **>(&s_smsgSetAtWar_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Faction::UnitFactionPolyfill
|
||||
+153
-29
@@ -17,6 +17,7 @@
|
||||
#include "item/Arg.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/Location.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -52,16 +53,14 @@ static constexpr const char *kGetItemInfoReceived = "GET_ITEM_INFO_RECEIVED";
|
||||
static const Event::Custom::AutoReserve _reserveItemDataLoadResult{kItemDataLoadResult};
|
||||
static const Event::Custom::AutoReserve _reserveGetItemInfoReceived{kGetItemInfoReceived};
|
||||
|
||||
static void FireGetItemInfoReceived(int itemID, int success) {
|
||||
const int slot = Event::Custom::Lookup(kGetItemInfoReceived);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%d%d", itemID, success);
|
||||
static void FireGetItemInfoReceived(int itemID, bool success) {
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kGetItemInfoReceived),
|
||||
itemID, success);
|
||||
}
|
||||
|
||||
static void FireItemDataLoadResult(int itemID, int success) {
|
||||
const int slot = Event::Custom::Lookup(kItemDataLoadResult);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%d%d", itemID, success);
|
||||
static void FireItemDataLoadResult(int itemID, bool success) {
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kItemDataLoadResult),
|
||||
itemID, success);
|
||||
}
|
||||
|
||||
// Engine callback for **implicit** cache fills (transparent warmup
|
||||
@@ -76,16 +75,7 @@ static void FireItemDataLoadResult(int itemID, int success) {
|
||||
// callback would crash the engine, so stdcall is verified by induction.
|
||||
static void __stdcall ItemLoadCallback_Implicit(void *userData, int success) {
|
||||
const auto itemID = static_cast<int>(reinterpret_cast<uintptr_t>(userData));
|
||||
FireGetItemInfoReceived(itemID, static_cast<int>(success != 0));
|
||||
}
|
||||
|
||||
// Engine callback for **explicit** cache fills (player-side
|
||||
// `C_Item.RequestLoadItemData(ByID)` calls). Fires ITEM_DATA_LOAD_RESULT
|
||||
// only — the implicit GET_ITEM_INFO_RECEIVED path is for ambient cache
|
||||
// fills, not for callers who explicitly asked.
|
||||
static void __stdcall ItemLoadCallback_Explicit(void *userData, int success) {
|
||||
const auto itemID = static_cast<int>(reinterpret_cast<uintptr_t>(userData));
|
||||
FireItemDataLoadResult(itemID, static_cast<int>(success != 0));
|
||||
FireGetItemInfoReceived(itemID, success != 0);
|
||||
}
|
||||
|
||||
using ItemLoadCallback_t = void(__stdcall *)(void *userData, int success);
|
||||
@@ -103,6 +93,15 @@ using ItemLoadCallback_t = void(__stdcall *)(void *userData, int success);
|
||||
// VanillaHelpers documented as `requestIfMissing` — that slot is reused
|
||||
// as a local immediately after entry, never read. The trigger for the
|
||||
// request path is `callback != NULL`.
|
||||
//
|
||||
// The 6th arg IS used elsewhere though: when non-zero, the engine
|
||||
// walks the entry's existing pending-callback list and skips the
|
||||
// append if a node with matching `(callback, userData)` is already
|
||||
// queued. We always pass `1` so that bursts of `WarmCache` calls for
|
||||
// the same itemID (e.g. Bagnon enumerating the same slot repeatedly
|
||||
// while the SMSG_ITEM_QUERY_SINGLE_RESPONSE is in flight) collapse to
|
||||
// a single callback registration — otherwise the response fires our
|
||||
// `GET_ITEM_INFO_RECEIVED` once per call.
|
||||
static const uint8_t *CacheFetch(uint32_t itemID, ItemLoadCallback_t callback) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
@@ -110,7 +109,7 @@ static const uint8_t *CacheFetch(uint32_t itemID, ItemLoadCallback_t callback) {
|
||||
void *cb = reinterpret_cast<void *>(callback);
|
||||
void *userData =
|
||||
callback ? reinterpret_cast<void *>(static_cast<uintptr_t>(itemID)) : nullptr;
|
||||
return fn(cache, itemID, &zeroGuid, cb, userData, 0);
|
||||
return fn(cache, itemID, &zeroGuid, cb, userData, 1);
|
||||
}
|
||||
|
||||
// Resolves an item-location table at stack idx to the itemID by walking
|
||||
@@ -148,18 +147,143 @@ static int __fastcall Script_IsItemDataCached(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Explicit-request path: kicks off the cache fill via the explicit
|
||||
// callback (which fires ITEM_DATA_LOAD_RESULT). If the item is already
|
||||
// cached, the engine won't invoke our callback — synthesize the event
|
||||
// so addons get the same notification regardless of cache state,
|
||||
// matching modern `C_Item.RequestLoadItemData(ByID)` semantics.
|
||||
static void RequestAndMaybeNotify(uint32_t itemID) {
|
||||
const bool wasCached = (CacheFetch(itemID, nullptr) != nullptr);
|
||||
CacheFetch(itemID, &ItemLoadCallback_Explicit);
|
||||
if (wasCached)
|
||||
FireItemDataLoadResult(static_cast<int>(itemID), 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.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// 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)`.
|
||||
struct Pending {
|
||||
uint32_t itemID;
|
||||
int ticksWaiting;
|
||||
bool requestIssued;
|
||||
};
|
||||
|
||||
constexpr int kMaxPending = 64;
|
||||
constexpr int kEscalateTicks = 60;
|
||||
constexpr int kMaxWaitTicks = 1200;
|
||||
|
||||
static Pending g_pending[kMaxPending];
|
||||
static int g_pendingCount = 0;
|
||||
|
||||
static int FindPending(uint32_t itemID) {
|
||||
for (int i = 0; i < g_pendingCount; ++i) {
|
||||
if (g_pending[i].itemID == itemID)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void RemovePendingAt(int idx) {
|
||||
--g_pendingCount;
|
||||
if (idx != g_pendingCount)
|
||||
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*/) {}
|
||||
|
||||
static void RequestAndMaybeNotify(uint32_t itemID) {
|
||||
// Already loaded — fire success synchronously.
|
||||
if (CacheFetch(itemID, nullptr) != nullptr) {
|
||||
FireItemDataLoadResult(static_cast<int>(itemID), true);
|
||||
return;
|
||||
}
|
||||
// Already tracking — second request just dedups.
|
||||
if (FindPending(itemID) >= 0)
|
||||
return;
|
||||
if (g_pendingCount >= kMaxPending)
|
||||
return;
|
||||
g_pending[g_pendingCount].itemID = itemID;
|
||||
g_pending[g_pendingCount].ticksWaiting = 0;
|
||||
g_pending[g_pendingCount].requestIssued = false;
|
||||
++g_pendingCount;
|
||||
}
|
||||
|
||||
// Response handler hook. After the engine fills cache entries from
|
||||
// `SMSG_ITEM_QUERY_SINGLE_RESPONSE` and walks the per-entry callback
|
||||
// lists, we sweep `g_pending` for anything now-loaded and fire the
|
||||
// event. This is the engine-aligned alternative to per-tick polling:
|
||||
// we observe fills exactly when they happen, with zero added latency
|
||||
// and no risk of racing the engine's own callback walk.
|
||||
//
|
||||
// `__thiscall` is mimicked as `__fastcall(ecx, edx_unused, ...)` per
|
||||
// the convention used elsewhere in this codebase (see
|
||||
// `KeyringDescChange_h`, `FrameRegisterEvent_h`).
|
||||
using ItemStatsCacheResponse_t = void(__thiscall *)(void *cache, void *packetReader, int flag);
|
||||
static ItemStatsCacheResponse_t ItemStatsCacheResponse_o = nullptr;
|
||||
|
||||
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);
|
||||
// Sweep pending — the engine just finished filling any matching
|
||||
// entries. Peek (no callback) returns non-null iff the entry's
|
||||
// loaded flag is set.
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookCacheResponse{
|
||||
Offsets::FUN_ITEMSTATS_CACHE_RESPONSE,
|
||||
reinterpret_cast<void *>(&ItemStatsCacheResponse_h),
|
||||
reinterpret_cast<void **>(&ItemStatsCacheResponse_o)};
|
||||
|
||||
// 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.
|
||||
static void OnWorldTick() {
|
||||
for (int i = 0; i < g_pendingCount;) {
|
||||
if (!g_pending[i].requestIssued && g_pending[i].ticksWaiting >= kEscalateTicks) {
|
||||
CacheFetch(g_pending[i].itemID, &ItemLoadCallback_NoOp);
|
||||
g_pending[i].requestIssued = true;
|
||||
}
|
||||
if (g_pending[i].ticksWaiting >= kMaxWaitTicks) {
|
||||
const uint32_t itemID = g_pending[i].itemID;
|
||||
RemovePendingAt(i);
|
||||
FireItemDataLoadResult(static_cast<int>(itemID), false);
|
||||
continue;
|
||||
}
|
||||
++g_pending[i].ticksWaiting;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
// Public C++ API — see Item/Data.h. Implicit (transparent) warmup
|
||||
// path used by the `Script_GetItemInfo` hook and `SetItemByID`. Fires
|
||||
// the cache request only if the item isn't already cached; uses the
|
||||
|
||||
+22
-33
@@ -14,28 +14,12 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Item::Exists {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// `C_Item.DoesItemExist(itemLocation)` — true iff the location resolves
|
||||
// to a populated inventory slot on the active player. Equipment-slot
|
||||
// locations check the character pane; bag-slot locations walk the
|
||||
@@ -51,27 +35,32 @@ static int __fastcall Script_C_Item_DoesItemExist(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Item.DoesItemExistByID(itemInfo)` — true iff the cache currently
|
||||
// has data for this itemID (i.e. `GetItemInfo` would return non-nil
|
||||
// right now). Same semantics as the modern function and the existing
|
||||
// Lua shim: cache miss → false, but we kick off the network query
|
||||
// so a subsequent call after `GET_ITEM_INFO_RECEIVED` will succeed.
|
||||
// `C_Item.DoesItemExistByID(itemInfo)` — true if `itemID` looks like a
|
||||
// valid item identifier. Modern WoW answers from the client-side item
|
||||
// database (Item-sparse.db2) and is independent of whether the network
|
||||
// cache has been warmed yet. 1.12 has no client-side DB, so we
|
||||
// optimistically accept any positive itemID; callers needing the
|
||||
// "is the cache populated?" check should use `IsItemDataCachedByID`.
|
||||
//
|
||||
// This split matters for `ItemMixin:IsItemEmpty` (FrameXML ItemUtil),
|
||||
// which uses `DoesItemExistByID` to gate `ContinueOnItemLoad` — the
|
||||
// whole point of `ContinueOnItemLoad` is to wait for an uncached item
|
||||
// to load, so the existence check must succeed for items that aren't
|
||||
// in the cache yet.
|
||||
//
|
||||
// We deliberately do NOT auto-warm the cache here. Existence is a pure
|
||||
// query about the ID, not a load request — and during early-login
|
||||
// (cold WDB, pfQuest's PLAYER_ENTERING_WORLD path) a spurious cache
|
||||
// touch can race the engine's natural inventory prefetch and leave the
|
||||
// item stuck. Callers wanting to actually load data should use
|
||||
// `RequestLoadItemDataByID`; passive readers (`GetItemInfo`,
|
||||
// `GetItemNameByID`) already warm on their own miss path.
|
||||
//
|
||||
// Accepts numeric itemID or `"item:NNN..."` string (including full
|
||||
// chat links). Returns false for any input we can't resolve to an
|
||||
// itemID; never raises.
|
||||
// chat links); never raises.
|
||||
static int __fastcall Script_C_Item_DoesItemExistByID(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
if (itemID <= 0) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
if (PeekItemRecord(static_cast<uint32_t>(itemID)) == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBoolean(L, 1);
|
||||
Game::Lua::PushBoolean(L, itemID > 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
// 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_Item.GetItemDataByID(itemInfo)` / `C_Item.GetItemData(itemLocation)`
|
||||
// — return a single table containing every field we can extract from
|
||||
// the cached `ItemStats_C` record. Convenience for addons that want
|
||||
// the kitchen sink without making 15 separate `C_Item.*` calls.
|
||||
//
|
||||
// Returns `nil` if the item isn't cached (caller should
|
||||
// `RequestLoadItemData` and listen for `ITEM_DATA_LOAD_RESULT`).
|
||||
// Does not warm the cache itself — this is a passive reader, same as
|
||||
// `GetItemInfoInstant`'s contract.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Spell.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace Item::GetData {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, bool requestIfMissing);
|
||||
|
||||
const uint8_t *FetchItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, false);
|
||||
}
|
||||
|
||||
int CurrentLocaleIndex() {
|
||||
return *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
}
|
||||
|
||||
// Mirrors the lookup in `Item::Info`. ItemClass.dbc record `+0x0C` is
|
||||
// the 9-locale name array; null/empty slots fall back to "".
|
||||
const char *LookupClassName(uint32_t classID) {
|
||||
const char *s = DBC::LocalizedField(
|
||||
Offsets::VAR_ITEMCLASS_RECORDS, Offsets::VAR_ITEMCLASS_COUNT,
|
||||
classID, Offsets::OFF_ITEMCLASS_NAMES);
|
||||
return (s != nullptr && s[0] != '\0') ? s : "";
|
||||
}
|
||||
|
||||
// ItemSubClass.dbc walker — same fallback chain as Script_GetItemInfo
|
||||
// (verbose name first, short name on miss).
|
||||
const char *LookupSubClassName(uint32_t classID, uint32_t subClassID) {
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_ITEMSUBCLASS_COUNT);
|
||||
if (count <= 0)
|
||||
return "";
|
||||
auto *records = *reinterpret_cast<const uint8_t *const *>(Offsets::VAR_ITEMSUBCLASS_RECORDS);
|
||||
if (records == nullptr)
|
||||
return "";
|
||||
const int locale = CurrentLocaleIndex();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint8_t *record = records + i * Offsets::OFF_ITEMSUBCLASS_RECORD_STRIDE;
|
||||
if (*reinterpret_cast<const uint32_t *>(record + 0x00) != classID)
|
||||
continue;
|
||||
if (*reinterpret_cast<const uint32_t *>(record + 0x04) != subClassID)
|
||||
continue;
|
||||
const char *verbose = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSUBCLASS_DISPLAY_NAME + locale * 4);
|
||||
if (verbose != nullptr && verbose[0] != '\0')
|
||||
return verbose;
|
||||
const char *shortName = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSUBCLASS_NAME + locale * 4);
|
||||
return (shortName != nullptr && shortName[0] != '\0') ? shortName : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const char *LookupInvType(uint32_t invType) {
|
||||
if (invType > Offsets::INVTYPE_TABLE_MAX_INDEX)
|
||||
return "";
|
||||
auto **table = reinterpret_cast<const char **>(Offsets::VAR_INVTYPE_STRING_TABLE);
|
||||
const char *s = table[invType];
|
||||
return (s != nullptr && s[0] != '\0') ? s : "";
|
||||
}
|
||||
|
||||
bool BuildIconPath(uint32_t displayInfoID, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
const char *iconName = DBC::StringField(
|
||||
Offsets::VAR_ITEMDISPLAYINFO_RECORDS, Offsets::VAR_ITEMDISPLAYINFO_COUNT,
|
||||
displayInfoID, Offsets::OFF_ITEMDISPLAYINFO_ICON);
|
||||
if (iconName == nullptr || iconName[0] == '\0')
|
||||
return false;
|
||||
std::snprintf(out, outSize, "Interface\\Icons\\%s", iconName);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wrath flipped the bagFamily encoding from raw 1-based IDs to a
|
||||
// bitmask. We surface the modern shape so backported addons compile
|
||||
// cleanly. See `Item::Info::BagFamilyIdToBitmask` for the full story.
|
||||
uint32_t BagFamilyIdToBitmask(uint32_t rawId) {
|
||||
return (rawId == 0) ? 0 : (1u << (rawId - 1));
|
||||
}
|
||||
|
||||
// Shorthand: read a u32 field from the record at `offset`, push it on
|
||||
// the stack as a Lua number, and set `t[key] = value` on the table at
|
||||
// stack[-3]. `SetFieldNumber` already handles the push/set; this is
|
||||
// just the field-fetch convenience.
|
||||
void SetU32(void *L, const uint8_t *record, const char *key, int offset) {
|
||||
const uint32_t v = *reinterpret_cast<const uint32_t *>(record + offset);
|
||||
Game::Lua::SetFieldNumber(L, key, static_cast<double>(v));
|
||||
}
|
||||
|
||||
void SetI32(void *L, const uint8_t *record, const char *key, int offset) {
|
||||
const int32_t v = *reinterpret_cast<const int32_t *>(record + offset);
|
||||
Game::Lua::SetFieldNumber(L, key, static_cast<double>(v));
|
||||
}
|
||||
|
||||
void SetF32(void *L, const uint8_t *record, const char *key, int offset) {
|
||||
const float v = *reinterpret_cast<const float *>(record + offset);
|
||||
Game::Lua::SetFieldNumber(L, key, static_cast<double>(v));
|
||||
}
|
||||
|
||||
// Sets `t[key]` to a sub-array of N consecutive u32 values from the
|
||||
// record. Pushes 1-indexed (Lua array convention).
|
||||
void SetU32Array(void *L, const uint8_t *record, const char *key,
|
||||
int offset, int count) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::NewTable(L);
|
||||
auto *arr = reinterpret_cast<const uint32_t *>(record + offset);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(arr[i]));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
void SetI32Array(void *L, const uint8_t *record, const char *key,
|
||||
int offset, int count) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::NewTable(L);
|
||||
auto *arr = reinterpret_cast<const int32_t *>(record + offset);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(arr[i]));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
void SetF32Array(void *L, const uint8_t *record, const char *key,
|
||||
int offset, int count) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::NewTable(L);
|
||||
auto *arr = reinterpret_cast<const float *>(record + offset);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(arr[i]));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Sparse stats: walks the 10 (statType, statValue) slots and pushes
|
||||
// only the populated ones into a map keyed by statType. Modern addons
|
||||
// expect `stats[STAT_AGI] = 5`-style access; matching that here avoids
|
||||
// callers having to walk the fixed-10 array themselves.
|
||||
void SetStatsMap(void *L, const uint8_t *record) {
|
||||
Game::Lua::PushString(L, "stats");
|
||||
Game::Lua::NewTable(L);
|
||||
auto *types = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_STAT_TYPE);
|
||||
auto *values = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_STAT_VALUE);
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_STAT_SLOT_COUNT; ++i) {
|
||||
if (types[i] == 0 && values[i] == 0)
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(types[i]));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(values[i]));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Spell slots: walks all 5 slots, pushes only entries with a non-zero
|
||||
// spellID as `{ id, trigger, charges, cooldown, category,
|
||||
// categoryCooldown }` records. Sparse but 1-indexed in declaration
|
||||
// order so the slot index is preserved.
|
||||
void SetSpellsArray(void *L, const uint8_t *record) {
|
||||
Game::Lua::PushString(L, "spells");
|
||||
Game::Lua::NewTable(L);
|
||||
auto *ids = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_ID);
|
||||
auto *triggers = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_TRIGGER);
|
||||
auto *charges = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_CHARGES);
|
||||
auto *cooldowns = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_COOLDOWN);
|
||||
auto *categories = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_CATEGORY);
|
||||
auto *catCooldowns = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_CATEGORY_CD);
|
||||
int written = 0;
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; ++i) {
|
||||
if (ids[i] == 0)
|
||||
continue;
|
||||
++written;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(written));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "id", static_cast<double>(ids[i]));
|
||||
Game::Lua::SetFieldNumber(L, "trigger", static_cast<double>(triggers[i]));
|
||||
Game::Lua::SetFieldNumber(L, "charges", static_cast<double>(charges[i]));
|
||||
Game::Lua::SetFieldNumber(L, "cooldown", static_cast<double>(cooldowns[i]));
|
||||
Game::Lua::SetFieldNumber(L, "category", static_cast<double>(categories[i]));
|
||||
Game::Lua::SetFieldNumber(L, "categoryCooldown",
|
||||
static_cast<double>(catCooldowns[i]));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Pushes a single Lua table for `record` onto the stack, populated
|
||||
// with every field we can read out of `ItemStats_C`. Caller is
|
||||
// responsible for verifying `record` is non-null and pushing the
|
||||
// itemID as a setup step.
|
||||
void PushItemDataTable(void *L, uint32_t itemID, const uint8_t *record) {
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
Game::Lua::SetFieldNumber(L, "itemID", static_cast<double>(itemID));
|
||||
|
||||
// Identity strings
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
Game::Lua::SetFieldString(L, "name", name);
|
||||
const char *description = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DESCRIPTION);
|
||||
if (description != nullptr && description[0] != '\0')
|
||||
Game::Lua::SetFieldString(L, "description", description);
|
||||
|
||||
// Display
|
||||
const uint32_t displayInfoID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
Game::Lua::SetFieldNumber(L, "displayInfoID", static_cast<double>(displayInfoID));
|
||||
char iconPath[260];
|
||||
if (BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
Game::Lua::SetFieldString(L, "icon", iconPath);
|
||||
|
||||
// Quality / classification
|
||||
const uint32_t quality = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
Game::Lua::SetFieldNumber(L, "quality", static_cast<double>(quality));
|
||||
|
||||
const uint32_t classID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CLASS);
|
||||
const uint32_t subClassID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
Game::Lua::SetFieldNumber(L, "classID", static_cast<double>(classID));
|
||||
Game::Lua::SetFieldNumber(L, "subclassID", static_cast<double>(subClassID));
|
||||
Game::Lua::SetFieldString(L, "className", LookupClassName(classID));
|
||||
Game::Lua::SetFieldString(L, "subclassName", LookupSubClassName(classID, subClassID));
|
||||
|
||||
const uint32_t invType = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
Game::Lua::SetFieldNumber(L, "inventoryType", static_cast<double>(invType));
|
||||
Game::Lua::SetFieldString(L, "equipLoc", LookupInvType(invType));
|
||||
|
||||
// Bind
|
||||
SetU32(L, record, "bindType", Offsets::OFF_ITEMSTATS_BONDING);
|
||||
|
||||
// Flags: raw + decoded bits.
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_FLAGS);
|
||||
Game::Lua::SetFieldNumber(L, "flags", static_cast<double>(flags));
|
||||
Game::Lua::SetFieldBool(L, "isConjured",
|
||||
(flags & Offsets::ITEM_FLAG_CONJURED) != 0);
|
||||
Game::Lua::SetFieldBool(L, "isOpenable",
|
||||
(flags & Offsets::ITEMSTATS_FLAG_OPENABLE) != 0);
|
||||
Game::Lua::SetFieldBool(L, "isLootable",
|
||||
(flags & Offsets::ITEM_FLAG_LOOTABLE) != 0);
|
||||
Game::Lua::SetFieldBool(L, "isWrapper",
|
||||
(flags & Offsets::ITEM_FLAG_WRAPPER) != 0);
|
||||
|
||||
// Stacking / inventory
|
||||
SetI32(L, record, "maxStackSize", Offsets::OFF_ITEMSTATS_STACKABLE);
|
||||
SetI32(L, record, "maxCount", Offsets::OFF_ITEMSTATS_MAX_COUNT);
|
||||
SetU32(L, record, "containerSlots", Offsets::OFF_ITEMSTATS_CONTAINER_SLOTS);
|
||||
const uint32_t rawBagFamily = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_BAG_FAMILY);
|
||||
Game::Lua::SetFieldNumber(L, "bagFamily",
|
||||
static_cast<double>(BagFamilyIdToBitmask(rawBagFamily)));
|
||||
|
||||
// Economy
|
||||
SetU32(L, record, "buyPrice", Offsets::OFF_ITEMSTATS_BUY_PRICE);
|
||||
SetU32(L, record, "sellPrice", Offsets::OFF_ITEMSTATS_SELL_PRICE);
|
||||
|
||||
// Level / requirements
|
||||
SetU32(L, record, "itemLevel", Offsets::OFF_ITEMSTATS_ITEM_LEVEL);
|
||||
SetU32(L, record, "requiredLevel", Offsets::OFF_ITEMSTATS_REQUIRED_LEVEL);
|
||||
SetU32(L, record, "requiredSkill", Offsets::OFF_ITEMSTATS_REQUIRED_SKILL);
|
||||
SetU32(L, record, "requiredSkillRank", Offsets::OFF_ITEMSTATS_REQUIRED_SKILL_RANK);
|
||||
SetU32(L, record, "requiredSpell", Offsets::OFF_ITEMSTATS_REQUIRED_SPELL);
|
||||
SetU32(L, record, "requiredHonorRank", Offsets::OFF_ITEMSTATS_REQUIRED_HONOR_RANK);
|
||||
SetU32(L, record, "requiredCityRank", Offsets::OFF_ITEMSTATS_REQUIRED_CITY_RANK);
|
||||
SetU32(L, record, "requiredFaction", Offsets::OFF_ITEMSTATS_REQUIRED_FACTION);
|
||||
SetU32(L, record, "requiredFactionRank", Offsets::OFF_ITEMSTATS_REQUIRED_FACTION_RANK);
|
||||
SetU32(L, record, "allowableClass", Offsets::OFF_ITEMSTATS_ALLOWABLE_CLASS);
|
||||
SetU32(L, record, "allowableRace", Offsets::OFF_ITEMSTATS_ALLOWABLE_RACE);
|
||||
|
||||
// Equipment defenses
|
||||
SetU32(L, record, "armor", Offsets::OFF_ITEMSTATS_ARMOR);
|
||||
SetU32(L, record, "block", Offsets::OFF_ITEMSTATS_BLOCK);
|
||||
SetU32(L, record, "maxDurability", Offsets::OFF_ITEMSTATS_MAX_DURABILITY);
|
||||
|
||||
// Sparse stats map (statType -> statValue, only populated entries)
|
||||
SetStatsMap(L, record);
|
||||
|
||||
// Resistances
|
||||
SetU32(L, record, "resistanceHoly", Offsets::OFF_ITEMSTATS_RES_HOLY);
|
||||
SetU32(L, record, "resistanceFire", Offsets::OFF_ITEMSTATS_RES_FIRE);
|
||||
SetU32(L, record, "resistanceNature", Offsets::OFF_ITEMSTATS_RES_NATURE);
|
||||
SetU32(L, record, "resistanceFrost", Offsets::OFF_ITEMSTATS_RES_FROST);
|
||||
SetU32(L, record, "resistanceShadow", Offsets::OFF_ITEMSTATS_RES_SHADOW);
|
||||
SetU32(L, record, "resistanceArcane", Offsets::OFF_ITEMSTATS_RES_ARCANE);
|
||||
|
||||
// Weapon
|
||||
SetF32Array(L, record, "damageMin", Offsets::OFF_ITEMSTATS_DAMAGE_MIN,
|
||||
Offsets::ITEMSTATS_DAMAGE_SLOT_COUNT);
|
||||
SetF32Array(L, record, "damageMax", Offsets::OFF_ITEMSTATS_DAMAGE_MAX,
|
||||
Offsets::ITEMSTATS_DAMAGE_SLOT_COUNT);
|
||||
SetU32Array(L, record, "damageType", Offsets::OFF_ITEMSTATS_DAMAGE_TYPE,
|
||||
Offsets::ITEMSTATS_DAMAGE_SLOT_COUNT);
|
||||
SetU32(L, record, "delay", Offsets::OFF_ITEMSTATS_DELAY);
|
||||
SetU32(L, record, "ammoType", Offsets::OFF_ITEMSTATS_AMMO_TYPE);
|
||||
SetF32(L, record, "rangedModRange", Offsets::OFF_ITEMSTATS_RANGED_MOD_RANGE);
|
||||
|
||||
// Spell slots (sparse array of records) + ON_USE convenience
|
||||
SetSpellsArray(L, record);
|
||||
const int useSpellID = Item::Spell::OnUseSpellIDForItemID(itemID);
|
||||
if (useSpellID > 0)
|
||||
Game::Lua::SetFieldNumber(L, "useSpellID", static_cast<double>(useSpellID));
|
||||
|
||||
// Lock / item set
|
||||
SetU32(L, record, "lockID", Offsets::OFF_ITEMSTATS_LOCK_ID);
|
||||
SetU32(L, record, "itemSet", Offsets::OFF_ITEMSTATS_ITEM_SET);
|
||||
|
||||
// Books / quest / binding
|
||||
SetU32(L, record, "pageText", Offsets::OFF_ITEMSTATS_PAGE_TEXT);
|
||||
SetU32(L, record, "pageMaterial", Offsets::OFF_ITEMSTATS_PAGE_MATERIAL);
|
||||
SetU32(L, record, "languageID", Offsets::OFF_ITEMSTATS_LANGUAGE_ID);
|
||||
SetU32(L, record, "startQuest", Offsets::OFF_ITEMSTATS_START_QUEST);
|
||||
SetI32(L, record, "material", Offsets::OFF_ITEMSTATS_MATERIAL);
|
||||
SetU32(L, record, "sheath", Offsets::OFF_ITEMSTATS_SHEATH);
|
||||
SetI32(L, record, "randomProperty", Offsets::OFF_ITEMSTATS_RANDOM_PROPERTY);
|
||||
SetU32(L, record, "area", Offsets::OFF_ITEMSTATS_AREA);
|
||||
SetU32(L, record, "map", Offsets::OFF_ITEMSTATS_MAP);
|
||||
}
|
||||
|
||||
// Common path for both entry points: take an itemID, peek the cache,
|
||||
// build & push the data table or push nil for uncached.
|
||||
int PushDataForItemID(void *L, int itemID) {
|
||||
if (itemID <= 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const uint8_t *record = FetchItemRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
PushItemDataTable(L, static_cast<uint32_t>(itemID), record);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Item.GetItemDataByID(itemInfo)` — accepts numeric itemID,
|
||||
// `"item:NNN..."` string, or full chat link. Returns the data table
|
||||
// or nil for invalid input / uncached items.
|
||||
int __fastcall Script_GetItemDataByID(void *L) {
|
||||
return PushDataForItemID(L, Item::Arg::ResolveItemID(L, 1));
|
||||
}
|
||||
|
||||
// `C_Item.GetItemData(itemLocation)` — resolves the equipment-slot /
|
||||
// bag-slot location to its CGItem, extracts the itemID, then peeks
|
||||
// the cache. Returns nil for empty slots, invalid locations, or
|
||||
// uncached items.
|
||||
int __fastcall Script_GetItemData(void *L) {
|
||||
if (!Item::Location::IsLocationArg(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_Item.GetItemData(itemLocation)");
|
||||
return 0;
|
||||
}
|
||||
return PushDataForItemID(L, Item::ID::FromCGItem(Item::Location::Resolve(L, 1)));
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemDataByID", &Script_GetItemDataByID);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemData", &Script_GetItemData);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Item::GetData
|
||||
+20
-3
@@ -93,13 +93,31 @@ static bool BuildIconPath(uint32_t displayInfoID, char *out, size_t outSize) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// `GetItemInfoInstant` is the modern "synchronous, never blocks" item
|
||||
// info call. In modern WoW the trailing 6 fields come from client-side
|
||||
// `Item-sparse.db2` and so the call always returns the full 7-tuple.
|
||||
// 1.12 has no client-side item DB — those fields live only in the
|
||||
// network-fed `DBCache_ItemStats`. We mirror the contract by:
|
||||
// - always returning the itemID (echoed from input) so callers using
|
||||
// `GetItemInfoInstant(link_or_id)` as an ID extractor never see nil
|
||||
// - returning the remaining 6 fields from the cache when present,
|
||||
// nil when not (preserving the 7-tuple shape)
|
||||
// - not warming the cache; "Instant" implies no side effects. Callers
|
||||
// that need the full info should use `GetItemInfo`, which already
|
||||
// triggers the warmup via the `Script_GetItemInfo` hook.
|
||||
static int __fastcall Script_GetItemInfoInstant(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(itemID));
|
||||
|
||||
const uint8_t *record = FetchItemRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
if (record == nullptr) {
|
||||
for (int i = 0; i < 6; ++i)
|
||||
Game::Lua::PushNil(L);
|
||||
return 7;
|
||||
}
|
||||
|
||||
const uint32_t classID =
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_ITEMSTATS_CLASS);
|
||||
@@ -114,7 +132,6 @@ static int __fastcall Script_GetItemInfoInstant(void *L) {
|
||||
if (!BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
iconPath[0] = 0;
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(itemID));
|
||||
Game::Lua::PushString(L, LookupItemClassName(classID));
|
||||
Game::Lua::PushString(L, LookupItemSubClassName(classID, subClassID));
|
||||
Game::Lua::PushString(L, LookupInvType(invType));
|
||||
|
||||
@@ -16,13 +16,31 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/QualityColor.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace Item::Link {
|
||||
|
||||
namespace {
|
||||
|
||||
using BuildItemLink_t = const char *(__fastcall *)(const void *cgItem);
|
||||
|
||||
// Cached ItemStats record lookup. Same `_GetRecord` pattern the rest of
|
||||
// the codebase uses; passes a noop NULL callback so no SMSG fires for
|
||||
// uncached items — caller treats "not cached" as a clean failure.
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int dedupFlag);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char *FromCGItem(const uint8_t *cgItem) {
|
||||
@@ -32,6 +50,25 @@ const char *FromCGItem(const uint8_t *cgItem) {
|
||||
return fn(cgItem);
|
||||
}
|
||||
|
||||
bool BasicFromItemID(uint32_t itemID, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
const uint8_t *record = PeekItemRecord(itemID);
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
if (name == nullptr || *name == '\0')
|
||||
return false;
|
||||
const uint32_t quality = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
const int n = std::snprintf(out, outSize,
|
||||
"%s|Hitem:%u:0:0:0|h[%s]|h|r",
|
||||
Item::QualityColor::Prefix(static_cast<int>(quality)),
|
||||
itemID, name);
|
||||
return n > 0 && static_cast<size_t>(n) < outSize;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// `C_Item.GetItemLink(itemLocation)` — returns the fully-decorated
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Item::Link {
|
||||
@@ -28,4 +29,15 @@ namespace Item::Link {
|
||||
// produce for the same slot.
|
||||
const char *FromCGItem(const uint8_t *cgItem);
|
||||
|
||||
// Builds a basic itemID-only hyperlink (`"|cffRRGGBB|Hitem:N:0:0:0|h[Name]|h|r"`)
|
||||
// into `out` from the cached ItemStats record for `itemID`. No
|
||||
// enchant / random-suffix decoration — use this when the caller has
|
||||
// only an itemID and no live CGItem (mail inbox attachments, cursor
|
||||
// drag-source items, vendor roster previews). Returns true on
|
||||
// success, false if the item isn't cached or the buffer is too small.
|
||||
//
|
||||
// `outSize` should be at least 256 to comfortably fit the longest
|
||||
// possible link (`item:` payload + colored prefix + bracketed name).
|
||||
bool BasicFromItemID(uint32_t itemID, char *out, size_t outSize);
|
||||
|
||||
} // namespace Item::Link
|
||||
|
||||
@@ -254,6 +254,24 @@ bool MoveCount(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot, int co
|
||||
if (srcItem == nullptr)
|
||||
return false;
|
||||
|
||||
// Vanilla server rejects `CMSG_SPLIT_ITEM` when count == srcStack
|
||||
// (`EQUIP_ERR_COULDNT_SPLIT_ITEMS`) — conceptually "split N off,
|
||||
// leave residual", and splitting the entire stack would leave
|
||||
// source empty. The drag-and-drop UI's "drop whole stack on
|
||||
// matching stack" goes through CMSG_SWAP_ITEM instead, which the
|
||||
// server treats as a merge for same-item destinations (up to
|
||||
// maxStack). Route there for count == srcStack so callers don't
|
||||
// have to special-case "move everything".
|
||||
auto *srcDescriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
srcItem + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
const int srcStack = srcDescriptor == nullptr ? 0 :
|
||||
static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
srcDescriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT));
|
||||
if (count > srcStack)
|
||||
return false; // server would reject anyway; fail fast and locally
|
||||
if (count == srcStack)
|
||||
return Containers(L, srcBag, srcSlot, dstBag, dstSlot);
|
||||
|
||||
void *player = ResolvePlayer();
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
|
||||
+9
-3
@@ -82,9 +82,15 @@ bool Containers(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot);
|
||||
// - `dst` same item & fits maxStack → merges `count` into dst
|
||||
// - `dst` different item / overflow → server rejects (no partial)
|
||||
//
|
||||
// Source must be occupied with at least `count` items; server rejects
|
||||
// if not. Returns false locally only on bad args or unresolvable bags.
|
||||
// All other validation is server-side — same as `Containers` above.
|
||||
// Source must be occupied with at least `count` items. Locally
|
||||
// validates `count <= srcStack` (server would reject otherwise with
|
||||
// `EQUIP_ERR_COULDNT_SPLIT_ITEMS` — fail fast). When `count ==
|
||||
// srcStack` ("move the whole stack"), routes through `Containers`
|
||||
// instead — vanilla's `CMSG_SPLIT_ITEM` rejects splits that would
|
||||
// leave the source empty, but `CMSG_SWAP_ITEM` handles the case
|
||||
// (merges into a matching destination stack, swaps otherwise).
|
||||
// Returns false locally on bad args, unresolvable bags, or
|
||||
// `count > srcStack`; all other validation is server-side.
|
||||
//
|
||||
// Requires Lua state for the same reason as `Containers`: the source
|
||||
// CGItem lookup goes through `Item::Location::ResolveBag`. Stomps the
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../guid/Guid.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Loot::Nearby {
|
||||
|
||||
namespace {
|
||||
|
||||
// Engine interact-range constants, read out of `.rdata` once and
|
||||
// pinned here as `constexpr`. Both are referenced by `FUN_005EC110`
|
||||
// (the master interact-state checker) and propagated through
|
||||
// `FUN_005DF130` / `FUN_005DF2A0` (CMSG_LOOT senders). Yards units;
|
||||
// values verified at runtime addresses `DAT_0080B058` and
|
||||
// `DAT_0080A1E8`.
|
||||
//
|
||||
// INTERACT_REACH (1.333…) is added to `target_radius + player_radius`
|
||||
// for the unclamped range; modeled as the "reach beyond your touch"
|
||||
// constant.
|
||||
// MIN_INTERACT_RANGE (5.0) clamps the result so even tiny
|
||||
// targets with near-zero bounding radius still get the standard
|
||||
// 5-yard right-click reach.
|
||||
constexpr float INTERACT_REACH = 1.3333333f;
|
||||
constexpr float MIN_INTERACT_RANGE = 5.0f;
|
||||
|
||||
// `ClntObjMgrEnumVisibleObjects(callback, context)` — walks the engine's
|
||||
// visible-object table. The callback shape's `void *unusedEdx` slot
|
||||
// absorbs the second `__fastcall` register so MSVC lays out the trailing
|
||||
// `uint64_t guid` on the stack exactly where the engine puts it. The
|
||||
// callback returns non-zero to keep iterating; zero stops the walk early.
|
||||
using ClntObjMgrEnumVisibleObjectsCallback_t = int(__fastcall *)(void *ctx,
|
||||
void *unusedEdx,
|
||||
uint64_t guid);
|
||||
using ClntObjMgrEnumVisibleObjects_t =
|
||||
int(__fastcall *)(ClntObjMgrEnumVisibleObjectsCallback_t cb, void *ctx);
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// `__thiscall(self, outBuf) → const C3Vector *` at vtable slot 5
|
||||
// (byte offset `0x14`). Both CGUnit_C and CGPlayer_C populate the
|
||||
// caller-provided 12-byte buffer with the unit's world position and
|
||||
// return it. Same call shape `FUN_005EC110` uses to read the
|
||||
// player / target positions when deciding interact range.
|
||||
struct C3Vector {
|
||||
float x, y, z;
|
||||
};
|
||||
using GetPosition_t = const C3Vector *(__thiscall *)(const void *self,
|
||||
C3Vector *outBuf);
|
||||
constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
|
||||
|
||||
// `ClntObjMgrObjectPtr(typeMask, debugMessage, guid_lo, guid_hi, debugCode)`
|
||||
// — resolves a 64-bit GUID to the loaded `CGObject_C *` it currently
|
||||
// refers to, filtered by `typeMask`. Returns NULL when no live object
|
||||
// matches the GUID *and* the typeMask. Splitting the GUID into lo/hi
|
||||
// DWORDs on the stack is equivalent to the engine's
|
||||
// `__fastcall(uint32_t, const char*, uint64_t, int)` signature once the
|
||||
// `uint64_t` argument is placed in the same stack slots.
|
||||
using ClntObjMgrObjectPtr_t = void *(__fastcall *)(uint32_t typeMask,
|
||||
const char *debugMsg,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int debugCode);
|
||||
|
||||
// Reads `(unit.m_objectFields[+0x224] & UNIT_DYNFLAG_LOOTABLE) != 0`
|
||||
// — the server-driven bit it sets when the local player has loot
|
||||
// rights on this unit. Works uniformly for live and dead CGUnit
|
||||
// subclasses; the bit is simply 0 for non-lootable targets.
|
||||
//
|
||||
// Don't call `FUN_CGCORPSE_IS_LOOTABLE` here even though the engine
|
||||
// itself uses it in `FUN_005D6BF0`: that helper reads `+0x78` of
|
||||
// m_objectFields, which is the CORPSE dynamic-flags slot for
|
||||
// CGCorpse_C but the BYTES_0 (race / class / gender / power type)
|
||||
// slot for CGUnit_C. Routing live units through it returns the low
|
||||
// bit of *race* — yields false positives for any Human / Dwarf /
|
||||
// Undead / Gnome player and false negatives for dead mobs.
|
||||
bool IsLootable(const void *unit) {
|
||||
auto *fields = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(unit) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
fields + Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
|
||||
return (flags & Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
|
||||
}
|
||||
|
||||
// Reads the bounding-radius float from a unit's m_objectFields. The
|
||||
// engine uses this as the contribution from both sides (player + target)
|
||||
// in its interact-range computation.
|
||||
float BoundingRadius(const void *unit) {
|
||||
auto *fields = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(unit) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return *reinterpret_cast<const float *>(
|
||||
fields + Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
// Invokes the unit's virtual `GetPosition` (vtable slot at offset
|
||||
// `0x14`). The engine's signature is `__thiscall(this, outBuf) →
|
||||
// outBuf`; we pass our own 12-byte stack buffer and read x/y/z out
|
||||
// of the returned pointer.
|
||||
const C3Vector *GetPosition(const void *unit, C3Vector *outBuf) {
|
||||
auto vtable = *reinterpret_cast<void *const *const *>(unit);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
*reinterpret_cast<const void *const *>(
|
||||
reinterpret_cast<const uint8_t *>(vtable) +
|
||||
VTBL_GET_POSITION_OFFSET));
|
||||
return fn(unit, outBuf);
|
||||
}
|
||||
|
||||
// Mirrors the range portion of `FUN_005EC110`. We skip the engine's
|
||||
// player-state checks (mounted / casting / prior-interaction GUID
|
||||
// gate) because they fire intermittently during normal play and would
|
||||
// make the enumerator return an empty list whenever the player has
|
||||
// already clicked something — surprising for a "what's lootable
|
||||
// nearby" reader. The pure distance test is what controls cursor
|
||||
// highlight, which is the contract the user asked for.
|
||||
bool InInteractRange(const void *player, const void *target) {
|
||||
C3Vector pBuf{};
|
||||
C3Vector tBuf{};
|
||||
const C3Vector *pPos = GetPosition(player, &pBuf);
|
||||
const C3Vector *tPos = GetPosition(target, &tBuf);
|
||||
if (pPos == nullptr || tPos == nullptr)
|
||||
return false;
|
||||
|
||||
const float dx = pPos->x - tPos->x;
|
||||
const float dy = pPos->y - tPos->y;
|
||||
const float dz = pPos->z - tPos->z;
|
||||
const float distSq = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
float range = BoundingRadius(player) + BoundingRadius(target) + INTERACT_REACH;
|
||||
if (range < MIN_INTERACT_RANGE)
|
||||
range = MIN_INTERACT_RANGE;
|
||||
return distSq <= range * range;
|
||||
}
|
||||
|
||||
// Only `TYPEMASK_UNIT` carries lootable-mob state in 1.12 — dead
|
||||
// creatures stay as CGCreature_C objects with the lootable bit flipped
|
||||
// on, rather than transitioning to a separate corpse object. Dead
|
||||
// players become CGCorpse_C (`TYPEMASK_CORPSE`) but those aren't
|
||||
// lootable by other players in vanilla — no reason to walk them here.
|
||||
|
||||
struct ScanCtx {
|
||||
void *L;
|
||||
const void *player; // resolved once outside the iterator
|
||||
int idx; // 1-based slot of the next entry to append
|
||||
};
|
||||
|
||||
int __fastcall ScanCallback(ScanCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
auto ObjectPtr = reinterpret_cast<ClntObjMgrObjectPtr_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *obj = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
if (obj == nullptr)
|
||||
return 1; // GUID isn't a unit; continue
|
||||
|
||||
if (!IsLootable(obj))
|
||||
return 1; // unit isn't lootable to us; continue
|
||||
|
||||
if (!InInteractRange(ctx->player, obj))
|
||||
return 1; // out of click-loot range; continue
|
||||
|
||||
char guidBuf[Guid::STRING_SIZE];
|
||||
Game::Lua::PushNumber(ctx->L, static_cast<double>(ctx->idx));
|
||||
Game::Lua::NewTable(ctx->L);
|
||||
Game::Lua::SetFieldString(ctx->L, "guid",
|
||||
Guid::FormatAsString(guid, guidBuf, sizeof guidBuf));
|
||||
Game::Lua::SetTable(ctx->L, -3);
|
||||
++ctx->idx;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Loot.GetNearbyLootableUnits()` — returns a 1-indexed array of
|
||||
// `{ guid = "0x..." }` subtables, one per visible unit that is (a)
|
||||
// flagged DYNFLAG_LOOTABLE by the server and (b) inside the engine's
|
||||
// own interact range (the distance threshold that lights up the
|
||||
// right-click-loot cursor). Empty table pre-login, when no lootable
|
||||
// units are in range, or when we don't have loot rights on visible
|
||||
// corpses (the server only sets the bit for clients with rights).
|
||||
int __fastcall Script_GetNearbyLootableUnits(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
// The engine derefs the object manager unconditionally on iterator
|
||||
// entry. NULL during glue / character-select; bail with an empty
|
||||
// table rather than crashing.
|
||||
if (*reinterpret_cast<void *volatile *>(Offsets::VAR_LOCAL_PLAYER_PTR) == nullptr)
|
||||
return 1;
|
||||
|
||||
// Resolve the local player once — the canonical CGPlayer_C the
|
||||
// engine's own interact-range checks use, distinct from the
|
||||
// `VAR_LOCAL_PLAYER_PTR` global. Bail if unavailable.
|
||||
auto ResolveUnitToken = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
const void *player = ResolveUnitToken("player");
|
||||
if (player == nullptr)
|
||||
return 1;
|
||||
|
||||
auto Enum = reinterpret_cast<ClntObjMgrEnumVisibleObjects_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS);
|
||||
ScanCtx ctx{L, player, 1};
|
||||
Enum(reinterpret_cast<ClntObjMgrEnumVisibleObjectsCallback_t>(&ScanCallback),
|
||||
&ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Game::Lua::RegisterTableFunction("C_Loot", "GetNearbyLootableUnits",
|
||||
&Script_GetNearbyLootableUnits);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace Loot::Nearby
|
||||
@@ -0,0 +1,479 @@
|
||||
// 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.
|
||||
//
|
||||
// Hook-driven loot scanner. Walks the nearby-lootable-units set, opens
|
||||
// each corpse's loot session via `FUN_CMSG_LOOT_UNIT`, then *intercepts
|
||||
// the engine's loot-window response* before any visual updates happen.
|
||||
// The intercept (`FUN_LOOT_CONTROLLER`) lets the original function run
|
||||
// so `VAR_LOOT_SLOTS` populates and the link builder works — but a
|
||||
// paired hook on `FUN_FIRE_EVENT_NO_ARGS` swallows the `LOOT_OPENED` /
|
||||
// `LOOT_CLOSED` event fires while the scan is in progress. Net result:
|
||||
// the engine's state machine cycles silently for each corpse and
|
||||
// `LootFrame` never reacts. Other Lua addons listening to those events
|
||||
// also see nothing during the scan; that's deliberate.
|
||||
//
|
||||
// Compared to the older polling-driven version this also collapses the
|
||||
// state machine — we no longer need `WaitingOpen` / `WaitingClose` /
|
||||
// per-tick `VAR_LOOT_GUID` reads, because the controller hook *is* the
|
||||
// state transition.
|
||||
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../event/Custom.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "../tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Loot::Scan {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kEventCompleted = "LOOT_SCAN_COMPLETED";
|
||||
const Event::Custom::AutoReserve _reserve{kEventCompleted};
|
||||
|
||||
// Per-step timeout for the controller hook to fire on a queued
|
||||
// CMSG_LOOT. WorldTick fires at the client frame rate (~30-60 Hz),
|
||||
// so 180 ticks ≈ 3-6s wall clock. Long enough for high-latency
|
||||
// servers, short enough that a single dropped response doesn't hang
|
||||
// the whole scan.
|
||||
constexpr int kStepTimeoutTicks = 180;
|
||||
|
||||
// Event IDs the engine fires through `FUN_FIRE_EVENT_NO_ARGS` for
|
||||
// loot-window transitions. The hook swallows these only when the
|
||||
// scanner has the suppression flag raised — normal play sees them
|
||||
// fire unmodified.
|
||||
constexpr int EVENT_LOOT_OPENED = 0x10B;
|
||||
constexpr int EVENT_LOOT_CLOSED = 0x10D;
|
||||
|
||||
enum class State {
|
||||
Idle,
|
||||
WaitingForResponse, // CMSG_LOOT sent; awaiting our controller hook
|
||||
};
|
||||
|
||||
struct SlotItem {
|
||||
uint32_t itemID;
|
||||
uint32_t count;
|
||||
std::string link; // empty if the cache record wasn't loaded
|
||||
};
|
||||
|
||||
struct LootEntry {
|
||||
uint64_t guid;
|
||||
uint32_t coin; // copper amount; 0 if no coin
|
||||
std::vector<SlotItem> items;
|
||||
};
|
||||
|
||||
struct ScanCtx {
|
||||
State state = State::Idle;
|
||||
void *player = nullptr;
|
||||
uint64_t currentGuid = 0;
|
||||
int ticksSinceTransition = 0;
|
||||
bool suppressEvents = false; // read by the FireEvent hook
|
||||
std::vector<uint64_t> queue;
|
||||
std::vector<LootEntry> results;
|
||||
};
|
||||
|
||||
ScanCtx s_scan;
|
||||
|
||||
// === Typedefs for engine helpers ===
|
||||
|
||||
using ClntObjMgrEnumVisibleObjectsCallback_t = int(__fastcall *)(void *ctx,
|
||||
void *unusedEdx,
|
||||
uint64_t guid);
|
||||
using ClntObjMgrEnumVisibleObjects_t =
|
||||
int(__fastcall *)(ClntObjMgrEnumVisibleObjectsCallback_t cb, void *ctx);
|
||||
using ClntObjMgrObjectPtr_t = void *(__fastcall *)(uint32_t typeMask,
|
||||
const char *debugMsg,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int debugCode);
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using LootUnit_t = void(__thiscall *)(void *player, void *target,
|
||||
char useDistanceCheck);
|
||||
using CloseLootInner_t = void(__fastcall *)(int sendRelease,
|
||||
char earlyReturnOnGameObject,
|
||||
char showError);
|
||||
using LootSlotLinkBuilder_t = const char *(__fastcall *)(uint32_t userFacingSlot,
|
||||
char *outBuf,
|
||||
int bufSize);
|
||||
// `FUN_LOOT_CONTROLLER` is `__fastcall(int, undefined4, int)` — three
|
||||
// args: lootStructPtr in ECX, coin in EDX, an extra unk on stack[0].
|
||||
// Mis-declaring as 4-arg __fastcall would push one phantom stack slot
|
||||
// the engine doesn't pop, drifting ESP by 4 bytes per call until a
|
||||
// later return reads `0x00000001` as a corrupt frame pointer and the
|
||||
// process crashes. We do see / forward the `coin` value because the
|
||||
// engine reads it as `DAT_00b71ba0` later.
|
||||
using LootController_t = void(__fastcall *)(int lootStructPtr,
|
||||
int coin,
|
||||
int unk);
|
||||
using FireEventNoArgs_t = void(__fastcall *)(int eventID,
|
||||
int /*edx unused*/);
|
||||
|
||||
struct C3Vector {
|
||||
float x, y, z;
|
||||
};
|
||||
using GetPosition_t = const C3Vector *(__thiscall *)(const void *self,
|
||||
C3Vector *outBuf);
|
||||
constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
|
||||
|
||||
constexpr float INTERACT_REACH = 1.3333333f;
|
||||
constexpr float MIN_INTERACT_RANGE = 5.0f;
|
||||
|
||||
// === Range / lootability helpers (identical to Loot::Nearby) ===
|
||||
|
||||
bool IsLootableUnit(const void *unit) {
|
||||
auto *fields = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(unit) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
fields + Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
|
||||
return (flags & Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
|
||||
}
|
||||
|
||||
float BoundingRadius(const void *unit) {
|
||||
auto *fields = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(unit) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return *reinterpret_cast<const float *>(
|
||||
fields + Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
const C3Vector *GetPosition(const void *unit, C3Vector *outBuf) {
|
||||
auto vtable = *reinterpret_cast<void *const *const *>(unit);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
*reinterpret_cast<const void *const *>(
|
||||
reinterpret_cast<const uint8_t *>(vtable) +
|
||||
VTBL_GET_POSITION_OFFSET));
|
||||
return fn(unit, outBuf);
|
||||
}
|
||||
|
||||
bool InInteractRange(const void *player, const void *target) {
|
||||
C3Vector pBuf{};
|
||||
C3Vector tBuf{};
|
||||
const C3Vector *pPos = GetPosition(player, &pBuf);
|
||||
const C3Vector *tPos = GetPosition(target, &tBuf);
|
||||
if (pPos == nullptr || tPos == nullptr)
|
||||
return false;
|
||||
const float dx = pPos->x - tPos->x;
|
||||
const float dy = pPos->y - tPos->y;
|
||||
const float dz = pPos->z - tPos->z;
|
||||
const float distSq = dx * dx + dy * dy + dz * dz;
|
||||
float range = BoundingRadius(player) + BoundingRadius(target) + INTERACT_REACH;
|
||||
if (range < MIN_INTERACT_RANGE)
|
||||
range = MIN_INTERACT_RANGE;
|
||||
return distSq <= range * range;
|
||||
}
|
||||
|
||||
// === Scrape ===
|
||||
|
||||
// Reads the post-population `VAR_LOOT_SLOTS` table into a LootEntry.
|
||||
// The controller hook calls this *after* letting the original
|
||||
// `FUN_LOOT_CONTROLLER` run, so the slot table is fully filled in
|
||||
// and the link builder works against it.
|
||||
void ScrapeCurrentLoot(LootEntry *out) {
|
||||
const uint32_t coinRaw = *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_LOOT_LOOTABLE);
|
||||
out->coin = (coinRaw == 0xFFFFFFFFu) ? 0u : coinRaw;
|
||||
|
||||
const bool hasCoin = coinRaw != 0;
|
||||
auto LinkBuilder = reinterpret_cast<LootSlotLinkBuilder_t>(
|
||||
Offsets::FUN_LOOT_SLOT_LINK_BUILDER);
|
||||
|
||||
for (int slot = 0; slot < Offsets::LOOT_MAX_SLOTS; ++slot) {
|
||||
auto *entry = reinterpret_cast<const uint8_t *>(
|
||||
Offsets::VAR_LOOT_SLOTS + slot * Offsets::LOOT_SLOT_STRIDE);
|
||||
const uint32_t itemID = *reinterpret_cast<const uint32_t *>(entry);
|
||||
if (itemID == 0)
|
||||
continue;
|
||||
const uint32_t count = *reinterpret_cast<const uint32_t *>(entry + 8);
|
||||
|
||||
SlotItem item;
|
||||
item.itemID = itemID;
|
||||
item.count = count;
|
||||
|
||||
char linkBuf[0x400];
|
||||
const uint32_t slotArg = hasCoin ? static_cast<uint32_t>(slot + 1)
|
||||
: static_cast<uint32_t>(slot);
|
||||
const char *link = LinkBuilder(slotArg, linkBuf, sizeof linkBuf);
|
||||
if (link != nullptr)
|
||||
item.link = link;
|
||||
|
||||
out->items.push_back(std::move(item));
|
||||
}
|
||||
}
|
||||
|
||||
// === State-machine transitions ===
|
||||
|
||||
void StartLoot(uint64_t guid);
|
||||
void TryStartNext();
|
||||
|
||||
void SendCloseLoot() {
|
||||
auto Close = reinterpret_cast<CloseLootInner_t>(Offsets::FUN_CLOSE_LOOT_INNER);
|
||||
Close(1, '\0', '\0');
|
||||
}
|
||||
|
||||
void Complete() {
|
||||
s_scan.state = State::Idle;
|
||||
s_scan.currentGuid = 0;
|
||||
s_scan.suppressEvents = false;
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEventCompleted), "");
|
||||
}
|
||||
|
||||
void TryStartNext() {
|
||||
if (s_scan.queue.empty()) {
|
||||
Complete();
|
||||
return;
|
||||
}
|
||||
uint64_t guid = s_scan.queue.back();
|
||||
s_scan.queue.pop_back();
|
||||
StartLoot(guid);
|
||||
}
|
||||
|
||||
void StartLoot(uint64_t guid) {
|
||||
auto ObjectPtr = reinterpret_cast<ClntObjMgrObjectPtr_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *target = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
if (target == nullptr) {
|
||||
TryStartNext();
|
||||
return;
|
||||
}
|
||||
auto LootUnit = reinterpret_cast<LootUnit_t>(Offsets::FUN_CMSG_LOOT_UNIT);
|
||||
LootUnit(s_scan.player, target, /*useDistanceCheck=*/0);
|
||||
s_scan.currentGuid = guid;
|
||||
s_scan.state = State::WaitingForResponse;
|
||||
s_scan.ticksSinceTransition = 0;
|
||||
}
|
||||
|
||||
// === Hooks ===
|
||||
|
||||
LootController_t s_lootController_o = nullptr;
|
||||
FireEventNoArgs_t s_fireEventNoArgs_o = nullptr;
|
||||
|
||||
// `LOOT_OPENED` / `LOOT_CLOSED` get suppressed only while a scan is
|
||||
// actively interleaving open/close cycles, signaled by
|
||||
// `s_scan.suppressEvents`. The flag is raised by the controller hook
|
||||
// just around the original-loot-controller call and the
|
||||
// `SendCloseLoot` that follows, then dropped before
|
||||
// `TryStartNext`-initiated CMSG_LOOTs go out. Every other engine
|
||||
// event fire — and normal play's loot events — pass through.
|
||||
void __fastcall FireEventNoArgs_h(int eventID, int /*unusedEdx*/) {
|
||||
if (s_scan.suppressEvents &&
|
||||
(eventID == EVENT_LOOT_OPENED || eventID == EVENT_LOOT_CLOSED)) {
|
||||
return;
|
||||
}
|
||||
s_fireEventNoArgs_o(eventID, 0);
|
||||
}
|
||||
|
||||
// Intercepts the engine's loot-window state controller. Two paths:
|
||||
//
|
||||
// 1. **Scan-driven open** — the incoming `lootStructPtr` carries a
|
||||
// GUID matching our expected one. Let the original run (so the
|
||||
// slot table populates), then scrape, send a release (which
|
||||
// will recursively re-enter via `FUN_0048F200` → close-path
|
||||
// `FUN_LOOT_CONTROLLER(NULL, …)`), and advance the scan. Both
|
||||
// the inner open and the recursive close fire their respective
|
||||
// events through our `FireEventNoArgs_h`, which swallows them
|
||||
// while `suppressEvents` is on.
|
||||
//
|
||||
// 2. **Anything else** — pass through unmodified. Normal loot
|
||||
// interactions outside a scan are unaffected; the inner-close
|
||||
// call generated by our scan's `SendCloseLoot` re-enters here
|
||||
// with `arg1 == 0` and is also a passthrough (its
|
||||
// suppression-flag check happens at the FireEvent hook layer).
|
||||
void __fastcall LootController_h(int lootStructPtr, int coin, int unk) {
|
||||
const bool scanOpenCall =
|
||||
s_scan.state == State::WaitingForResponse &&
|
||||
lootStructPtr != 0 &&
|
||||
[&] {
|
||||
// Read the GUID off the response struct (`+8` deref to a
|
||||
// `{uint32 lo; uint32 hi}` block — same shape the original
|
||||
// copies into `VAR_LOOT_GUID_LO/HI`).
|
||||
auto *guidPtr = *reinterpret_cast<const uint32_t *const *>(
|
||||
reinterpret_cast<const uint8_t *>(
|
||||
static_cast<intptr_t>(lootStructPtr)) + 8);
|
||||
if (guidPtr == nullptr) return false;
|
||||
const uint64_t guid =
|
||||
static_cast<uint64_t>(guidPtr[0]) |
|
||||
(static_cast<uint64_t>(guidPtr[1]) << 32);
|
||||
return guid == s_scan.currentGuid;
|
||||
}();
|
||||
|
||||
if (scanOpenCall) {
|
||||
s_scan.suppressEvents = true;
|
||||
s_lootController_o(lootStructPtr, coin, unk);
|
||||
|
||||
LootEntry entry;
|
||||
entry.guid = s_scan.currentGuid;
|
||||
ScrapeCurrentLoot(&entry);
|
||||
s_scan.results.push_back(std::move(entry));
|
||||
|
||||
// Sends `CMSG_LOOT_RELEASE` and recursively calls this hook
|
||||
// with `arg1 == 0`. That recursive call falls into the
|
||||
// passthrough branch but still sees `suppressEvents == true`
|
||||
// at the FireEvent layer, so the `LOOT_CLOSED` fire inside
|
||||
// gets swallowed.
|
||||
SendCloseLoot();
|
||||
|
||||
s_scan.suppressEvents = false;
|
||||
TryStartNext();
|
||||
return;
|
||||
}
|
||||
|
||||
s_lootController_o(lootStructPtr, coin, unk);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookController{
|
||||
Offsets::FUN_LOOT_CONTROLLER,
|
||||
reinterpret_cast<void *>(&LootController_h),
|
||||
reinterpret_cast<void **>(&s_lootController_o)};
|
||||
|
||||
const Game::HookAutoRegister _hookFireEvent{
|
||||
Offsets::FUN_FIRE_EVENT_NO_ARGS,
|
||||
reinterpret_cast<void *>(&FireEventNoArgs_h),
|
||||
reinterpret_cast<void **>(&s_fireEventNoArgs_o)};
|
||||
|
||||
// === Tick (timeout only) ===
|
||||
|
||||
void OnTick() {
|
||||
if (s_scan.state == State::Idle)
|
||||
return;
|
||||
++s_scan.ticksSinceTransition;
|
||||
if (s_scan.ticksSinceTransition >= kStepTimeoutTicks) {
|
||||
// Controller hook never fired — engine guards rejected the
|
||||
// send (player state / OOR), or the server dropped the
|
||||
// packet. Skip this corpse and advance.
|
||||
s_scan.suppressEvents = false;
|
||||
TryStartNext();
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
|
||||
// === Enumeration ===
|
||||
|
||||
struct EnumCtx {
|
||||
const void *player;
|
||||
std::vector<uint64_t> *out;
|
||||
};
|
||||
|
||||
int __fastcall EnumCallback(EnumCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
auto ObjectPtr = reinterpret_cast<ClntObjMgrObjectPtr_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *obj = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
if (obj == nullptr)
|
||||
return 1;
|
||||
if (!IsLootableUnit(obj))
|
||||
return 1;
|
||||
if (!InInteractRange(ctx->player, obj))
|
||||
return 1;
|
||||
ctx->out->push_back(guid);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void BuildQueue(void *player) {
|
||||
s_scan.queue.clear();
|
||||
auto Enum = reinterpret_cast<ClntObjMgrEnumVisibleObjects_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS);
|
||||
EnumCtx ctx{player, &s_scan.queue};
|
||||
Enum(reinterpret_cast<ClntObjMgrEnumVisibleObjectsCallback_t>(&EnumCallback),
|
||||
&ctx);
|
||||
}
|
||||
|
||||
// === Lua bindings ===
|
||||
|
||||
int __fastcall Script_ScanNearbyLoot(void *L) {
|
||||
if (s_scan.state != State::Idle) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (*reinterpret_cast<void *volatile *>(Offsets::VAR_LOCAL_PLAYER_PTR) == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
auto Resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
void *player = Resolve("player");
|
||||
if (player == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
s_scan.results.clear();
|
||||
s_scan.player = player;
|
||||
BuildQueue(player);
|
||||
|
||||
if (s_scan.queue.empty()) {
|
||||
Complete();
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
TryStartNext();
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsScanInProgress(void *L) {
|
||||
Game::Lua::PushBool(L, s_scan.state != State::Idle);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetLastScanResults(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
int outIdx = 1;
|
||||
for (const auto &entry : s_scan.results) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(outIdx++));
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
char guidBuf[Guid::STRING_SIZE];
|
||||
Game::Lua::SetFieldString(L, "guid",
|
||||
Guid::FormatAsString(entry.guid, guidBuf,
|
||||
sizeof guidBuf));
|
||||
Game::Lua::SetFieldNumber(L, "coin", static_cast<double>(entry.coin));
|
||||
|
||||
Game::Lua::PushString(L, "items");
|
||||
Game::Lua::NewTable(L);
|
||||
int itemIdx = 1;
|
||||
for (const auto &item : entry.items) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(itemIdx++));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "itemID", static_cast<double>(item.itemID));
|
||||
Game::Lua::SetFieldNumber(L, "count", static_cast<double>(item.count));
|
||||
if (!item.link.empty())
|
||||
Game::Lua::SetFieldString(L, "link", item.link.c_str());
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTable(L, -3);
|
||||
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Game::Lua::RegisterTableFunction("C_Loot", "ScanNearbyLoot",
|
||||
&Script_ScanNearbyLoot);
|
||||
Game::Lua::RegisterTableFunction("C_Loot", "IsScanInProgress",
|
||||
&Script_IsScanInProgress);
|
||||
Game::Lua::RegisterTableFunction("C_Loot", "GetLastScanResults",
|
||||
&Script_GetLastScanResults);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace Loot::Scan
|
||||
@@ -0,0 +1,104 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../guid/Guid.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Loot::Unit {
|
||||
|
||||
namespace {
|
||||
|
||||
using ClntObjMgrObjectPtr_t = void *(__fastcall *)(uint32_t typeMask,
|
||||
const char *debugMsg,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int debugCode);
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// `FUN_CMSG_LOOT_UNIT` is `__thiscall(player, target, useDistanceCheck)`.
|
||||
// MSVC `__thiscall` puts `player` in ECX and the rest on the stack —
|
||||
// matching the engine's own callsite at `FUN_0060FA20`. The trailing
|
||||
// `char` argument is padded to 4 stack bytes by the ABI.
|
||||
using LootUnit_t = void(__thiscall *)(void *player, void *target,
|
||||
char useDistanceCheck);
|
||||
|
||||
// `C_Loot.LootUnit(guid)` — initiates a loot session against the unit
|
||||
// identified by `guid` (a hex-string as returned by `UnitGUID` /
|
||||
// `C_Loot.GetNearbyLootableUnits`). The engine handles release-prior-
|
||||
// loot, packet construction, and dispatch; the loot window appears
|
||||
// asynchronously via the regular `LOOT_OPENED` event once the server's
|
||||
// `SMSG_LOOT_RESPONSE` lands.
|
||||
//
|
||||
// `useDistanceCheck` is hardcoded to `0` so the engine doesn't auto-
|
||||
// walk the player toward a corpse that happens to be just out of
|
||||
// range — which would be a surprise side-effect for an AoE-loot
|
||||
// caller looping over `GetNearbyLootableUnits`. The server still
|
||||
// enforces real range; OOR requests just don't get a response.
|
||||
//
|
||||
// Returns nothing. Silent no-op when:
|
||||
// - The object manager is NULL (glue / character-select).
|
||||
// - The GUID doesn't resolve to a visible `TYPEMASK_UNIT` object
|
||||
// (target despawned, out of render range, wrong typemask).
|
||||
// - The local player pointer can't be resolved.
|
||||
// Errors only on Lua usage problems (missing arg, unparseable GUID).
|
||||
int __fastcall Script_LootUnit(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_Loot.LootUnit(guid)");
|
||||
return 0;
|
||||
}
|
||||
const char *guidStr = Game::Lua::ToString(L, 1);
|
||||
uint64_t guid = 0;
|
||||
if (!Guid::Parse(guidStr, &guid) || guid == 0) {
|
||||
Game::Lua::Error(L, "Usage: C_Loot.LootUnit(guid) — unparseable GUID");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Object manager NULL pre-login; bail silently rather than
|
||||
// crashing inside the resolver.
|
||||
if (*reinterpret_cast<void *volatile *>(Offsets::VAR_LOCAL_PLAYER_PTR) == nullptr)
|
||||
return 0;
|
||||
|
||||
auto ObjectPtr = reinterpret_cast<ClntObjMgrObjectPtr_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *target = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
if (target == nullptr)
|
||||
return 0;
|
||||
|
||||
// `ResolveUnitToken("player")` gives the canonical CGPlayer_C the
|
||||
// engine uses for `__thiscall` calls — distinct from the global
|
||||
// at `VAR_LOCAL_PLAYER_PTR` (see the comment on that offset).
|
||||
auto ResolveUnitToken = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
void *player = ResolveUnitToken("player");
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
|
||||
auto LootUnit = reinterpret_cast<LootUnit_t>(Offsets::FUN_CMSG_LOOT_UNIT);
|
||||
LootUnit(player, target, /*useDistanceCheck=*/0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Game::Lua::RegisterTableFunction("C_Loot", "LootUnit", &Script_LootUnit);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace Loot::Unit
|
||||
@@ -0,0 +1,209 @@
|
||||
// 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.
|
||||
//
|
||||
// Targeted single-item looting. Given a corpse GUID and a desired
|
||||
// itemID, opens that corpse's loot window via the engine's standard
|
||||
// `FUN_005DF2A0` path, finds the slot containing that itemID once the
|
||||
// window populates, and dispatches `CMSG_LOOT_ITEM` directly through
|
||||
// `FUN_005E1AD0`. The Lua-side `LootSlot` wrapper isn't used — see
|
||||
// the `FUN_CMSG_LOOT_ITEM` comment in Offsets.h for why.
|
||||
//
|
||||
// Fast path when the window is already open for the requested target:
|
||||
// skip the open round-trip and dispatch immediately. This makes
|
||||
// "take items A, B, C from corpse X" a series of cheap calls — the
|
||||
// first opens the window, the rest land in fast-path.
|
||||
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "../tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Loot::UnitItem {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kStepTimeoutTicks = 180;
|
||||
|
||||
enum class State {
|
||||
Idle,
|
||||
WaitingOpen,
|
||||
};
|
||||
|
||||
using ClntObjMgrObjectPtr_t = void *(__fastcall *)(uint32_t typeMask,
|
||||
const char *debugMsg,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int debugCode);
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using LootUnit_t = void(__thiscall *)(void *player, void *target,
|
||||
char useDistanceCheck);
|
||||
// `FUN_005E1AD0` is `__stdcall(uint8_t)`, not `__fastcall` — the
|
||||
// callee reads its arg at `[ebp+8]` (stack) and ends with `ret 4`.
|
||||
// Mis-declaring as `__fastcall` makes MSVC skip the push but the
|
||||
// callee still `ret 4`'s, drifting the caller's ESP by 4 bytes per
|
||||
// call and corrupting the surrounding frame. The first symptom is a
|
||||
// crash several frames later on an unrelated vtable lookup —
|
||||
// exactly the EIP=0x00000A70-style faults we saw before this fix.
|
||||
using SendLootItem_t = void(__stdcall *)(uint8_t slotByte);
|
||||
|
||||
State s_state = State::Idle;
|
||||
uint64_t s_targetGuid = 0;
|
||||
uint32_t s_desiredItemID = 0;
|
||||
int s_ticksSinceTransition = 0;
|
||||
|
||||
uint64_t ReadLootTargetGuid() {
|
||||
const uint32_t lo = *reinterpret_cast<const uint32_t *>(Offsets::VAR_LOOT_GUID_LO);
|
||||
const uint32_t hi = *reinterpret_cast<const uint32_t *>(Offsets::VAR_LOOT_GUID_HI);
|
||||
return static_cast<uint64_t>(hi) << 32 | lo;
|
||||
}
|
||||
|
||||
// Walks the currently-open loot window's slots looking for `itemID`.
|
||||
// Returns the wire-protocol slot byte the server expects in
|
||||
// `CMSG_LOOT_ITEM`, or `-1` if no slot matches. First match wins; if
|
||||
// the same itemID appears twice, only the first is reachable through
|
||||
// this function (caller can issue a second call after the first
|
||||
// completes).
|
||||
int FindWireSlotForItemID(uint32_t itemID) {
|
||||
for (int slot = 0; slot < Offsets::LOOT_MAX_SLOTS; ++slot) {
|
||||
auto *entry = reinterpret_cast<const uint8_t *>(
|
||||
Offsets::VAR_LOOT_SLOTS + slot * Offsets::LOOT_SLOT_STRIDE);
|
||||
const uint32_t id = *reinterpret_cast<const uint32_t *>(entry);
|
||||
if (id == 0)
|
||||
continue;
|
||||
if (id == itemID)
|
||||
return static_cast<int>(*(entry + Offsets::OFF_LOOT_SLOT_WIRE_INDEX));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool TakeItem(uint32_t itemID) {
|
||||
const int slotByte = FindWireSlotForItemID(itemID);
|
||||
if (slotByte < 0)
|
||||
return false;
|
||||
auto Send = reinterpret_cast<SendLootItem_t>(Offsets::FUN_CMSG_LOOT_ITEM);
|
||||
Send(static_cast<uint8_t>(slotByte));
|
||||
return true;
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
s_state = State::Idle;
|
||||
s_targetGuid = 0;
|
||||
s_desiredItemID = 0;
|
||||
}
|
||||
|
||||
void OnTick() {
|
||||
if (s_state == State::Idle)
|
||||
return;
|
||||
++s_ticksSinceTransition;
|
||||
if (s_state == State::WaitingOpen) {
|
||||
if (ReadLootTargetGuid() == s_targetGuid) {
|
||||
TakeItem(s_desiredItemID);
|
||||
Reset();
|
||||
return;
|
||||
}
|
||||
if (s_ticksSinceTransition >= kStepTimeoutTicks)
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
|
||||
// `C_Loot.LootUnitItem(guid, itemID)` — takes a specific item from a
|
||||
// specific corpse. If the corpse's loot window is already open, the
|
||||
// item is dispatched synchronously and the function returns `true` if
|
||||
// the slot was found, `false` if not. Otherwise the corpse's window is
|
||||
// opened asynchronously, and once it populates the item is sent
|
||||
// automatically. The async path returns `true` once the request is
|
||||
// queued; there's no "did it succeed" event — observe the player's
|
||||
// inventory or `LOOT_OPENED`/`ITEM_PUSH` events to confirm.
|
||||
//
|
||||
// Returns `false` immediately if:
|
||||
// - args are missing or invalid types,
|
||||
// - another `LootUnitItem` is mid-flight,
|
||||
// - the world isn't loaded,
|
||||
// - the target GUID doesn't resolve to a visible unit,
|
||||
// - (sync path only) no slot in the currently-open window holds
|
||||
// the requested itemID.
|
||||
//
|
||||
// The CMSG_LOOT_ITEM send bypasses the BoP-confirmation dialog the
|
||||
// vanilla UI normally shows. Server still enforces all real
|
||||
// permissions (loot rights, distance, master-loot rules); the
|
||||
// client-side BoP-prompt is purely a courtesy that callers of this
|
||||
// programmatic API have already opted out of by virtue of asking.
|
||||
int __fastcall Script_LootUnitItem(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: C_Loot.LootUnitItem(guid, itemID)");
|
||||
return 0;
|
||||
}
|
||||
uint64_t guid = 0;
|
||||
if (!Guid::Parse(Game::Lua::ToString(L, 1), &guid) || guid == 0) {
|
||||
Game::Lua::Error(L, "Usage: C_Loot.LootUnitItem(guid, itemID) — invalid guid");
|
||||
return 0;
|
||||
}
|
||||
const uint32_t itemID = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
|
||||
if (s_state != State::Idle) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (*reinterpret_cast<void *volatile *>(Offsets::VAR_LOCAL_PLAYER_PTR) == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Fast path: window already open for the requested target — dispatch
|
||||
// CMSG_LOOT_ITEM synchronously. No waiting on `WorldTick`, no state
|
||||
// transition needed.
|
||||
if (ReadLootTargetGuid() == guid) {
|
||||
Game::Lua::PushBool(L, TakeItem(itemID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto Resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
void *player = Resolve("player");
|
||||
if (player == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto ObjectPtr = reinterpret_cast<ClntObjMgrObjectPtr_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *target = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
if (target == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto LootUnit = reinterpret_cast<LootUnit_t>(Offsets::FUN_CMSG_LOOT_UNIT);
|
||||
LootUnit(player, target, /*useDistanceCheck=*/0);
|
||||
|
||||
s_targetGuid = guid;
|
||||
s_desiredItemID = itemID;
|
||||
s_state = State::WaitingOpen;
|
||||
s_ticksSinceTransition = 0;
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Game::Lua::RegisterTableFunction("C_Loot", "LootUnitItem",
|
||||
&Script_LootUnitItem);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace Loot::UnitItem
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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/>.
|
||||
|
||||
// `GetInboxItemLink(messageIndex[, attachmentIndex])` — returns
|
||||
// `(link, itemID)` for the item attached to inbox message
|
||||
// `messageIndex` (1-based). `link` is a basic itemID-only hyperlink
|
||||
// (see note below). Vanilla supports one attachment per message; the
|
||||
// optional 1-based `attachmentIndex` exists for modern-signature
|
||||
// parity and is no-op for any value other than 1.
|
||||
//
|
||||
// `itemID` as a second return is a ClassicAPI extension on top of
|
||||
// modern's link-only signature — saves callers a `string.match` to
|
||||
// parse it back out of the link.
|
||||
//
|
||||
// **Basic link only** (`|cff...|Hitem:N:0:0:0|h[Name]|h|r`). Vanilla
|
||||
// inbox entries DO store per-instance modifiers inline at `+0x12C..`
|
||||
// (enchant, suffix factor, random property — same fields
|
||||
// `Script_GameTooltip_SetInboxItem` copies onto the tooltip), but
|
||||
// 3.3.5's `GetInboxItemLink` ignores them and returns the itemID-only
|
||||
// link too (`FUN_0061E290(itemID)`). We match modern behavior — per-
|
||||
// instance data only fully manifests when the player takes the item
|
||||
// and the engine spawns a real CGItem; until then the link is
|
||||
// definitionally itemID-bound.
|
||||
//
|
||||
// Returns nothing on out-of-range messageIndex, empty message slot,
|
||||
// no attachment on the message, or attachmentIndex ≠ 1.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Link.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Mail::InboxItemLink {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_GetInboxItemLink(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: GetInboxItemLink(messageIndex[, attachmentIndex])");
|
||||
return 0;
|
||||
}
|
||||
const int oneBasedMsg = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (oneBasedMsg < 1)
|
||||
return 0;
|
||||
if (Game::Lua::IsNumber(L, 2)) {
|
||||
const int attach = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
if (attach != 1)
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int msgIdx = oneBasedMsg - 1;
|
||||
const int count = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_INBOX_COUNT));
|
||||
if (msgIdx >= count)
|
||||
return 0;
|
||||
|
||||
// `VAR_INBOX_ENTRIES` is a pointer slot, not the array itself —
|
||||
// one extra deref to reach the entry-pointer array.
|
||||
auto **entries = *reinterpret_cast<const uint8_t ***>(
|
||||
Offsets::VAR_INBOX_ENTRIES);
|
||||
if (entries == nullptr)
|
||||
return 0;
|
||||
const uint8_t *entry = entries[msgIdx];
|
||||
if (entry == nullptr)
|
||||
return 0;
|
||||
|
||||
const uint32_t itemID = *reinterpret_cast<const uint32_t *>(
|
||||
entry + Offsets::OFF_INBOX_ENTRY_ITEM_ID);
|
||||
if (itemID == 0)
|
||||
return 0;
|
||||
|
||||
char buf[256];
|
||||
if (!Item::Link::BasicFromItemID(itemID, buf, sizeof buf))
|
||||
return 0;
|
||||
Game::Lua::PushString(L, buf);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(itemID));
|
||||
return 2;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetInboxItemLink",
|
||||
&Script_GetInboxItemLink);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Mail::InboxItemLink
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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/>.
|
||||
|
||||
// `GetSendMailItemLink([attachmentIndex])` — returns
|
||||
// `(link, itemID)` for the item currently attached to the outgoing-
|
||||
// mail slot. `link` is the fully-decorated per-instance hyperlink
|
||||
// (enchant / random suffix preserved). Modern WoW expanded mail to up
|
||||
// to 12 attachments and added the index arg; vanilla 1.12 allows
|
||||
// exactly one, so we accept the arg for signature parity but treat
|
||||
// only index 1 as meaningful (any other index returns nothing).
|
||||
//
|
||||
// `itemID` as a second return is a ClassicAPI extension on top of
|
||||
// modern's link-only signature — saves callers a `string.match` to
|
||||
// parse it back out of the link.
|
||||
//
|
||||
// The attached item still exists as a live CGItem in the player's
|
||||
// inventory until the mail is actually sent — the engine stores its
|
||||
// GUID at `VAR_SEND_MAIL_ITEM_GUID_LO/HI`. We resolve the GUID via
|
||||
// `FUN_GET_OBJECT_BY_GUID(OBJECT_TYPE_ITEM, ...)` and feed the
|
||||
// resulting CGItem to the standard `Item::Link::FromCGItem` builder,
|
||||
// so enchant / random-suffix / unique-ID decoration matches what
|
||||
// `GetContainerItemLink` would return for the same item back in
|
||||
// the player's bag.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Link.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Mail::SendMailItemLink {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetObjectByGuid_t = void *(__fastcall *)(uint32_t typeMask, const char *debugMsg,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
int debugCode);
|
||||
|
||||
int __fastcall Script_GetSendMailItemLink(void *L) {
|
||||
// Optional 1-based attachment index. Anything other than 1 is a
|
||||
// no-op (vanilla is single-slot). Missing arg = treat as index 1.
|
||||
if (Game::Lua::IsNumber(L, 1)) {
|
||||
const int idx = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (idx != 1)
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t guidLo = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SEND_MAIL_ITEM_GUID_LO));
|
||||
const uint32_t guidHi = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SEND_MAIL_ITEM_GUID_HI));
|
||||
if (guidLo == 0 && guidHi == 0)
|
||||
return 0;
|
||||
|
||||
auto fn = reinterpret_cast<GetObjectByGuid_t>(Offsets::FUN_GET_OBJECT_BY_GUID);
|
||||
auto *cgItem = static_cast<const uint8_t *>(
|
||||
fn(Offsets::OBJECT_TYPE_ITEM, nullptr, guidLo, guidHi, 0));
|
||||
if (cgItem == nullptr)
|
||||
return 0;
|
||||
|
||||
const char *link = Item::Link::FromCGItem(cgItem);
|
||||
if (link == nullptr || *link == '\0')
|
||||
return 0;
|
||||
const int itemID = Item::ID::FromCGItem(cgItem);
|
||||
Game::Lua::PushString(L, link);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(itemID));
|
||||
return 2;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetSendMailItemLink",
|
||||
&Script_GetSendMailItemLink);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Mail::SendMailItemLink
|
||||
+4
-7
@@ -31,12 +31,10 @@ static const Event::Custom::AutoReserve _reserveQuestDataLoadResult{kQuestDataLo
|
||||
// `ret 8` cleans both args. We encode the questID into `userData` at
|
||||
// request time so the callback knows which quest completed.
|
||||
static void __stdcall QuestLoadCallback(void *userData, int success) {
|
||||
const int slot = Event::Custom::Lookup(kQuestDataLoadResult);
|
||||
if (slot < 0)
|
||||
return;
|
||||
const auto questID =
|
||||
static_cast<int>(reinterpret_cast<uintptr_t>(userData));
|
||||
Event::Custom::Fire(slot, "%d%d", questID, static_cast<int>(success != 0));
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kQuestDataLoadResult),
|
||||
questID, success != 0);
|
||||
}
|
||||
|
||||
static void RequestAndMaybeNotify(uint32_t questID) {
|
||||
@@ -47,9 +45,8 @@ static void RequestAndMaybeNotify(uint32_t questID) {
|
||||
// invoking our callback. Fire the event ourselves so addons get the
|
||||
// notification regardless of cache state, matching modern semantics.
|
||||
if (wasCached) {
|
||||
const int slot = Event::Custom::Lookup(kQuestDataLoadResult);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%d%d", static_cast<int>(questID), 1);
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kQuestDataLoadResult),
|
||||
static_cast<int>(questID), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,11 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "spell/MacroPrimarySpell.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace Spell::AtCursor {
|
||||
|
||||
@@ -170,6 +172,28 @@ void RegisterLuaFunctions() {
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// Macro-parser pattern: tag macros that call
|
||||
// `C_Spell.CastAtCursor(<spellID>)` with the spellID, so action-bar
|
||||
// UIs highlight the slot the same way they do for `/cast SpellName`.
|
||||
// The Lua call only accepts a numeric spellID, so the extractor just
|
||||
// reads a numeric literal — no name resolution needed.
|
||||
int ExtractCastAtCursorArg(const char *p, const char *end) {
|
||||
while (p < end && (*p == ' ' || *p == '\t'))
|
||||
++p;
|
||||
int value = 0;
|
||||
const char *digitStart = p;
|
||||
while (p < end && *p >= '0' && *p <= '9') {
|
||||
if (value > 100000000) // sanity cap before overflow
|
||||
return 0;
|
||||
value = value * 10 + (*p - '0');
|
||||
++p;
|
||||
}
|
||||
return (p > digitStart && value > 0) ? value : 0;
|
||||
}
|
||||
|
||||
const Spell::MacroPrimarySpell::PatternAutoRegister _patreg{
|
||||
"C_Spell.CastAtCursor(", &ExtractCastAtCursorArg};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Spell::AtCursor
|
||||
|
||||
+27
-130
@@ -42,6 +42,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "spell/Arg.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "spell/MacroPrimarySpell.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -208,141 +209,37 @@ void RegisterLuaFunctions() {
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Macro parser hook
|
||||
// Macro parser pattern
|
||||
//
|
||||
// Without this hook, a macro whose only cast statement is
|
||||
// `CastSpellNoToggle("Shoot")` doesn't get tagged in the spell-state
|
||||
// cache with the resolved spellID, because the engine's parser
|
||||
// (`FUN_004EFE00`) only knows to look for `/cast` slash-aliases and
|
||||
// `CastSpellByName("...")`. `IsAutoRepeatAction(macroSlot)` then has
|
||||
// nothing to match against the active auto-repeat spell, so the slot
|
||||
// fails to highlight in action-bar UIs (pfUI etc.).
|
||||
//
|
||||
// We hook the parser, let the original run, and if it didn't recognize
|
||||
// any pattern (cache field stays 0), scan the body ourselves for our
|
||||
// function name and use the engine's own name→spellID resolver to
|
||||
// fill in the field.
|
||||
// Tags macros containing `CastSpellNoToggle("Name")` with the
|
||||
// resolved spellID, so `IsAutoRepeatAction(macroSlot)` reports
|
||||
// correctly and action-bar UIs (pfUI etc.) highlight the slot for
|
||||
// auto-repeat spells. `Spell::MacroPrimarySpell` owns the parser
|
||||
// hook and the line walk; we just declare our `(prefix, extractor)`.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
using MacroParse_t = void(__fastcall *)(int macroEntry);
|
||||
MacroParse_t MacroParse_o = nullptr;
|
||||
|
||||
|
||||
// Scan a macro body for `CastSpellNoToggle("<name>")` and return the
|
||||
// resolved spellID, or 0 if no recognized invocation is present.
|
||||
// Mirrors the engine parser's "first hit wins, line-at-a-time" walk
|
||||
// order so behavior is consistent with how `/cast` and
|
||||
// `CastSpellByName` are matched.
|
||||
int ScanMacroBodyForOurPattern(const char *body) {
|
||||
static constexpr char kPattern[] = "CastSpellNoToggle(";
|
||||
static constexpr size_t kPatternLen = sizeof(kPattern) - 1;
|
||||
|
||||
const char *line = body;
|
||||
while (*line != '\0') {
|
||||
const char *eol = line;
|
||||
while (*eol != '\0' && *eol != '\n')
|
||||
++eol;
|
||||
|
||||
const char *p = line;
|
||||
const char *end = eol;
|
||||
while (p + kPatternLen <= end) {
|
||||
if (std::memcmp(p, kPattern, kPatternLen) == 0) {
|
||||
p += kPatternLen;
|
||||
while (p < end && (*p == ' ' || *p == '\t'))
|
||||
++p;
|
||||
if (p < end && *p == '"') {
|
||||
++p;
|
||||
const char *q = p;
|
||||
while (q < end && *q != '"')
|
||||
++q;
|
||||
if (q < end) {
|
||||
const size_t n = static_cast<size_t>(q - p);
|
||||
if (n > 0 && n < 128) {
|
||||
char name[128];
|
||||
std::memcpy(name, p, n);
|
||||
name[n] = '\0';
|
||||
const int spellID = Spell::Arg::NameToSpellID(name);
|
||||
if (spellID > 0)
|
||||
return spellID;
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // matched our prefix on this line; don't double-match
|
||||
}
|
||||
++p;
|
||||
}
|
||||
|
||||
line = (*eol == '\n') ? eol + 1 : eol;
|
||||
}
|
||||
return 0;
|
||||
int ExtractCastSpellNoToggleArg(const char *p, const char *end) {
|
||||
while (p < end && (*p == ' ' || *p == '\t'))
|
||||
++p;
|
||||
if (p >= end || *p != '"')
|
||||
return 0;
|
||||
++p;
|
||||
const char *q = p;
|
||||
while (q < end && *q != '"')
|
||||
++q;
|
||||
if (q >= end)
|
||||
return 0;
|
||||
const size_t n = static_cast<size_t>(q - p);
|
||||
if (n == 0 || n >= 128)
|
||||
return 0;
|
||||
char name[128];
|
||||
std::memcpy(name, p, n);
|
||||
name[n] = '\0';
|
||||
return Spell::Arg::NameToSpellID(name);
|
||||
}
|
||||
|
||||
// The vanilla parser's per-line stack buffer is 256 bytes. When a
|
||||
// macro line exceeds that, the parser's local at `[EBP-4]` (the saved
|
||||
// entry pointer) and the saved return address get clobbered with body
|
||||
// bytes — observed in a crash with `entry@[EBP-4]` reading as ASCII
|
||||
// `"onTi"` (fragment of `"expirationTime"` from a long `/run` line).
|
||||
// The bound-check on the line-read function reads correctly in
|
||||
// Ghidra, so the overflow path is somewhere we couldn't isolate from
|
||||
// static analysis — we settle for a conservative pre-check that
|
||||
// skips the original entirely when any line is long enough to risk
|
||||
// the bug.
|
||||
//
|
||||
// 240 (not 256) leaves a safety margin: the parser writes a NUL
|
||||
// terminator and may walk one or two bytes past in its loop logic,
|
||||
// so we keep clear of the boundary. Macros that hit this branch
|
||||
// can't have a `/cast` line anyway (`/cast SpellName(Rank N)` is
|
||||
// well under 100 bytes), so losing the engine's `/cast` /
|
||||
// `CastSpellByName` tagging is no loss. We still run our own
|
||||
// `CastSpellNoToggle` pattern scan since it bound-checks its own
|
||||
// extraction.
|
||||
bool BodyHasLongLine(const char *body) {
|
||||
if (body == nullptr)
|
||||
return false;
|
||||
constexpr int kMaxSafeLineBytes = 240;
|
||||
int lineLen = 0;
|
||||
for (; *body != '\0'; ++body) {
|
||||
if (*body == '\n') {
|
||||
lineLen = 0;
|
||||
} else if (++lineLen > kMaxSafeLineBytes) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void __fastcall MacroParse_h(int macroEntry) {
|
||||
const char *body = reinterpret_cast<const char *>(
|
||||
macroEntry + Offsets::OFF_MACRO_BODY);
|
||||
int *cacheField = reinterpret_cast<int *>(
|
||||
macroEntry + Offsets::OFF_MACRO_PRIMARY_SPELL);
|
||||
|
||||
if (BodyHasLongLine(body)) {
|
||||
// Skip the engine parser entirely — it overflows on lines
|
||||
// longer than its 256-byte buffer. Mark the cache as "no
|
||||
// recognized cast" (0); our own scan below may still tag it
|
||||
// if it's a `CastSpellNoToggle("...")` macro.
|
||||
*cacheField = 0;
|
||||
const int spellID = ScanMacroBodyForOurPattern(body);
|
||||
if (spellID > 0)
|
||||
*cacheField = spellID;
|
||||
return;
|
||||
}
|
||||
|
||||
MacroParse_o(macroEntry);
|
||||
|
||||
if (*cacheField != 0)
|
||||
return;
|
||||
|
||||
const int spellID = ScanMacroBodyForOurPattern(body);
|
||||
if (spellID > 0)
|
||||
*cacheField = spellID;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookreg{
|
||||
Offsets::FUN_MACRO_PARSE_PRIMARY_SPELL,
|
||||
reinterpret_cast<void *>(&MacroParse_h),
|
||||
reinterpret_cast<void **>(&MacroParse_o)};
|
||||
const Spell::MacroPrimarySpell::PatternAutoRegister _patreg{
|
||||
"CastSpellNoToggle(", &ExtractCastSpellNoToggleArg};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// 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/>.
|
||||
|
||||
// Owns the hook on `FUN_MACRO_PARSE_PRIMARY_SPELL` and dispatches to
|
||||
// feature-module-registered `(prefix, extractor)` pairs. The engine's
|
||||
// parser only recognizes `/cast SpellName` and `CastSpellByName(...)`
|
||||
// — addons that define new cast wrappers (`CastSpellNoToggle`,
|
||||
// `C_Spell.CastAtCursor`, etc.) need their macro slots tagged with
|
||||
// the resolved spellID too, otherwise action-bar UIs lose
|
||||
// auto-repeat highlight and cooldown overlay.
|
||||
//
|
||||
// Each feature module owns an extractor that knows how to read the
|
||||
// arg shape for its specific function (quoted string, numeric
|
||||
// literal, etc.). Patterns auto-register at static-init via
|
||||
// `PatternAutoRegister`. The hook walks each macro line ONCE,
|
||||
// checking every registered prefix at every position; on a prefix
|
||||
// match it calls the extractor to resolve a spellID.
|
||||
//
|
||||
// Long-line overflow guard: the vanilla parser has a 256-byte
|
||||
// per-line stack buffer that overflows on `/run` lines longer than
|
||||
// that. We pre-check and skip the original entirely when any line
|
||||
// exceeds the safe threshold — the original's tagging is lost in
|
||||
// that case (no `/cast` on a 240+ byte line anyway) but our
|
||||
// patterns still run since they bound-check their own extraction.
|
||||
|
||||
#include "MacroPrimarySpell.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Spell::MacroPrimarySpell {
|
||||
|
||||
namespace {
|
||||
|
||||
PatternAutoRegister *g_patternHead = nullptr;
|
||||
|
||||
using MacroParse_t = void(__fastcall *)(int macroEntry);
|
||||
MacroParse_t MacroParse_o = nullptr;
|
||||
|
||||
// Vanilla's parser uses a 256-byte stack buffer per line; bytes past
|
||||
// the buffer clobber the saved entry pointer at `[EBP-4]` and the
|
||||
// return address. 240 leaves a safety margin.
|
||||
constexpr int kMaxSafeLineBytes = 240;
|
||||
|
||||
bool BodyHasLongLine(const char *body) {
|
||||
if (body == nullptr)
|
||||
return false;
|
||||
int lineLen = 0;
|
||||
for (; *body != '\0'; ++body) {
|
||||
if (*body == '\n') {
|
||||
lineLen = 0;
|
||||
} else if (++lineLen > kMaxSafeLineBytes) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Walks each line once. At every byte position, checks every
|
||||
// registered prefix; on first match, invokes the extractor. First
|
||||
// extractor that returns a positive spellID wins for the whole macro
|
||||
// — matches the engine parser's "first hit, line-at-a-time" walk.
|
||||
int ScanBody(const char *body) {
|
||||
if (body == nullptr)
|
||||
return 0;
|
||||
const char *line = body;
|
||||
while (*line != '\0') {
|
||||
const char *eol = line;
|
||||
while (*eol != '\0' && *eol != '\n')
|
||||
++eol;
|
||||
|
||||
for (const char *p = line; p < eol; ++p) {
|
||||
for (auto *node = g_patternHead; node != nullptr; node = node->next) {
|
||||
if (p + node->prefixLen > eol)
|
||||
continue;
|
||||
if (std::memcmp(p, node->prefix, node->prefixLen) != 0)
|
||||
continue;
|
||||
const int spellID = node->extract(p + node->prefixLen, eol);
|
||||
if (spellID > 0)
|
||||
return spellID;
|
||||
// Prefix matched but extraction failed (malformed
|
||||
// args). Don't keep checking other prefixes at this
|
||||
// position — they couldn't possibly also match here.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
line = (*eol == '\n') ? eol + 1 : eol;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void __fastcall MacroParse_h(int macroEntry) {
|
||||
const char *body = reinterpret_cast<const char *>(
|
||||
macroEntry + Offsets::OFF_MACRO_BODY);
|
||||
int *cacheField = reinterpret_cast<int *>(
|
||||
macroEntry + Offsets::OFF_MACRO_PRIMARY_SPELL);
|
||||
|
||||
if (BodyHasLongLine(body)) {
|
||||
*cacheField = 0;
|
||||
const int spellID = ScanBody(body);
|
||||
if (spellID > 0)
|
||||
*cacheField = spellID;
|
||||
return;
|
||||
}
|
||||
|
||||
MacroParse_o(macroEntry);
|
||||
|
||||
if (*cacheField != 0)
|
||||
return;
|
||||
|
||||
const int spellID = ScanBody(body);
|
||||
if (spellID > 0)
|
||||
*cacheField = spellID;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookreg{
|
||||
Offsets::FUN_MACRO_PARSE_PRIMARY_SPELL,
|
||||
reinterpret_cast<void *>(&MacroParse_h),
|
||||
reinterpret_cast<void **>(&MacroParse_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
PatternAutoRegister::PatternAutoRegister(const char *p, ArgExtractor e)
|
||||
: prefix(p),
|
||||
prefixLen(static_cast<unsigned>(std::strlen(p))),
|
||||
extract(e),
|
||||
next(g_patternHead) {
|
||||
g_patternHead = this;
|
||||
}
|
||||
|
||||
} // namespace Spell::MacroPrimarySpell
|
||||
@@ -0,0 +1,44 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Spell::MacroPrimarySpell {
|
||||
|
||||
// Parses the argument(s) immediately after a known function-call
|
||||
// prefix. `p` points to the first byte past the closing `(` of the
|
||||
// prefix (so for `CastSpellNoToggle("Shoot")`, `p` would be at the
|
||||
// `"`). `end` is end-of-line. Returns the resolved spellID, or 0 if
|
||||
// extraction failed (malformed args, unknown spell, etc.).
|
||||
using ArgExtractor = int(*)(const char *p, const char *end);
|
||||
|
||||
// File-scope static auto-registers a `(prefix, extractor)` pair. Each
|
||||
// feature module that wants its Lua wrapper to tag macros declares:
|
||||
//
|
||||
// static const Spell::MacroPrimarySpell::PatternAutoRegister _r{
|
||||
// "CastSpellNoToggle(", &MyExtractor};
|
||||
//
|
||||
// The macro-parser hook walks every line once, checks each registered
|
||||
// prefix at each position, and on match invokes the extractor to
|
||||
// resolve a spellID. The resolved ID is written to the macro's
|
||||
// primary-spell cache field — drives auto-repeat highlight, cooldown
|
||||
// overlay, etc. in action-bar UIs (pfUI).
|
||||
struct PatternAutoRegister {
|
||||
PatternAutoRegister(const char *prefix, ArgExtractor extract);
|
||||
const char *prefix;
|
||||
unsigned prefixLen;
|
||||
ArgExtractor extract;
|
||||
PatternAutoRegister *next;
|
||||
};
|
||||
|
||||
} // namespace Spell::MacroPrimarySpell
|
||||
Reference in New Issue
Block a user