mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 04:47:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aac41faab9 | ||
|
|
f119f448ed | ||
|
|
44943bfab1 | ||
|
|
73d2c2f320 | ||
|
|
762968d24d | ||
|
|
d2672d818d | ||
|
|
17f44f93f8 | ||
|
|
73d90107b0 | ||
|
|
ed389591ac | ||
|
|
1867262cd9 | ||
|
|
9b21f983b3 | ||
|
|
3b729f08cb | ||
|
|
cdb3faa982 | ||
|
|
bb79311556 | ||
|
|
e881e18720 | ||
|
|
ba4520493a | ||
|
|
dc80ade21b | ||
|
|
6addd6d5b1 |
@@ -3,6 +3,7 @@
|
||||
## Notes: Library to emulate aspects of the modern WoW API on 1.12.1 / Lua 5.0.
|
||||
## Version: DEV
|
||||
## Author: Brues
|
||||
## X-Website: https://github.com/brues-code/ClassicAPI
|
||||
|
||||
locales\locales.xml
|
||||
|
||||
@@ -14,6 +15,7 @@ Util\Constants.lua
|
||||
Util\String.lua
|
||||
Util\TableUtil.lua
|
||||
Util\MathUtil.lua
|
||||
Util\PixelUtil.lua
|
||||
Util\FrameUtil.lua
|
||||
Util\FormattingUtil.lua
|
||||
Util\TimeUtil.lua
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
-- Backport of FrameXML/PixelUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- PixelUtil snaps frame sizes/points to whole physical pixels so
|
||||
-- borders and textures stay crisp at fractional UI scales. The table
|
||||
-- ports verbatim — its only math deps (Round, Lerp,
|
||||
-- ClampedPercentageBetween) all live in MathUtil, which loads first.
|
||||
--
|
||||
-- The one piece vanilla lacks is the native `GetPhysicalScreenSize()`
|
||||
-- (added in 7.0). The ClassicAPI DLL provides it (see
|
||||
-- src/screen/Info.cpp) by reading the engine's `gxResolution` CVar, so
|
||||
-- it's a real global registered at engine boot — no Lua polyfill
|
||||
-- needed here.
|
||||
|
||||
PixelUtil = {};
|
||||
|
||||
function PixelUtil.GetPixelToUIUnitFactor()
|
||||
local physicalWidth, physicalHeight = GetPhysicalScreenSize();
|
||||
return 768.0 / physicalHeight;
|
||||
end
|
||||
|
||||
function PixelUtil.GetNearestPixelSize(uiUnitSize, layoutScale, minPixels)
|
||||
if uiUnitSize == 0 and (not minPixels or minPixels == 0) then
|
||||
return 0;
|
||||
end
|
||||
|
||||
local uiUnitFactor = PixelUtil.GetPixelToUIUnitFactor();
|
||||
local numPixels = Round((uiUnitSize * layoutScale) / uiUnitFactor);
|
||||
if minPixels then
|
||||
if uiUnitSize < 0.0 then
|
||||
if numPixels > -minPixels then
|
||||
numPixels = -minPixels;
|
||||
end
|
||||
else
|
||||
if numPixels < minPixels then
|
||||
numPixels = minPixels;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return numPixels * uiUnitFactor / layoutScale;
|
||||
end
|
||||
|
||||
function PixelUtil.ConvertPixelsToUI(desiredPixels, layoutScale)
|
||||
return PixelUtil.GetNearestPixelSize(desiredPixels, layoutScale);
|
||||
end
|
||||
|
||||
function PixelUtil.ConvertPixelsToUIForRegion(desiredPixels, region)
|
||||
return PixelUtil.GetNearestPixelSize(desiredPixels, region:GetEffectiveScale());
|
||||
end
|
||||
|
||||
function PixelUtil.SetWidth(region, width, minPixels)
|
||||
region:SetWidth(PixelUtil.GetNearestPixelSize(width, region:GetEffectiveScale(), minPixels));
|
||||
end
|
||||
|
||||
function PixelUtil.SetHeight(region, height, minPixels)
|
||||
region:SetHeight(PixelUtil.GetNearestPixelSize(height, region:GetEffectiveScale(), minPixels));
|
||||
end
|
||||
|
||||
function PixelUtil.SetSize(region, width, height, minWidthPixels, minHeightPixels)
|
||||
PixelUtil.SetWidth(region, width, minWidthPixels);
|
||||
PixelUtil.SetHeight(region, height, minHeightPixels);
|
||||
end
|
||||
|
||||
function PixelUtil.SetPoint(region, point, relativeTo, relativePoint, offsetX, offsetY, minOffsetXPixels, minOffsetYPixels)
|
||||
region:SetPoint(point, relativeTo, relativePoint,
|
||||
PixelUtil.GetNearestPixelSize(offsetX, region:GetEffectiveScale(), minOffsetXPixels),
|
||||
PixelUtil.GetNearestPixelSize(offsetY, region:GetEffectiveScale(), minOffsetYPixels)
|
||||
);
|
||||
end
|
||||
|
||||
function PixelUtil.SetStatusBarValue(statusBar, value)
|
||||
local width = statusBar:GetWidth();
|
||||
if width and width > 0.0 then
|
||||
local min, max = statusBar:GetMinMaxValues();
|
||||
local percent = ClampedPercentageBetween(value, min, max);
|
||||
if percent == 0.0 or percent == 1.0 then
|
||||
statusBar:SetValue(value);
|
||||
else
|
||||
local numPixels = PixelUtil.GetNearestPixelSize(statusBar:GetWidth() * percent, statusBar:GetEffectiveScale());
|
||||
local roundedValue = Lerp(min, max, numPixels / width);
|
||||
statusBar:SetValue(roundedValue);
|
||||
end
|
||||
else
|
||||
statusBar:SetValue(value);
|
||||
end
|
||||
end
|
||||
@@ -32,8 +32,12 @@ function PlayerUtil.GetCurrentSpeed()
|
||||
return GetUnitSpeed("player") / 7 * 100
|
||||
end
|
||||
|
||||
local playerGuid
|
||||
function GetPlayerGuid()
|
||||
return UnitGUID("player");
|
||||
if not playerGuid then
|
||||
playerGuid = UnitGUID("player")
|
||||
end
|
||||
return playerGuid
|
||||
end
|
||||
|
||||
function IsPlayerGuid(guid)
|
||||
@@ -43,4 +47,4 @@ end
|
||||
function GetNameAndServerNameFromGUID(unitGUID)
|
||||
local _, _, _, _, _, name = GetPlayerInfoByGUID(unitGUID);
|
||||
return name, GetRealmName()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
-- Backport of FrameXML/TimeUtil.lua (feasible subset) to vanilla 1.12 /
|
||||
-- Lua 5.0.
|
||||
--
|
||||
-- Ports the deprecated-but-widely-used duration formatters (SecondsToTime,
|
||||
-- SecondsToClock, SecondsToTimeAbbrev) plus the unit constants/helpers. The
|
||||
-- SecondsFormatterMixin (the modern replacement) is omitted — it's a heavy
|
||||
-- mixin class little addon code targets on classic.
|
||||
--
|
||||
-- 5.0 notes: `%` -> math.mod, `s:format()` -> string.format. The FrameXML
|
||||
-- format-string globals these functions consume (DAYS_ABBR, D_HOURS,
|
||||
-- HOURS_MINUTES_SECONDS, the *_ONELETTER_ABBR set, …) are defined in the
|
||||
-- locale layer (locales/enUS.lua), which loads first.
|
||||
|
||||
SECONDS_PER_MIN = 60;
|
||||
SECONDS_PER_HOUR = 60 * SECONDS_PER_MIN;
|
||||
SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
|
||||
@@ -52,67 +39,3 @@ function SecondsToClock(seconds, displayZeroHours)
|
||||
return string.format(MINUTES_SECONDS, units.minutes, units.seconds);
|
||||
end
|
||||
end
|
||||
|
||||
-- 1.12's native unit strings are bare labels ("Hr", "Min") with no `%d`,
|
||||
-- unlike modern's "%d Hr"; insert the count correctly for either shape.
|
||||
local function FormatUnit(fmt, count)
|
||||
if string.find(fmt, "%%d") then
|
||||
return string.format(fmt, count);
|
||||
end
|
||||
return count .. " " .. fmt;
|
||||
end
|
||||
|
||||
function SecondsToTime(seconds, noSeconds, notAbbreviated, maxCount, roundUp)
|
||||
local time = "";
|
||||
local count = 0;
|
||||
local tempTime;
|
||||
seconds = roundUp and math.ceil(seconds) or math.floor(seconds);
|
||||
maxCount = maxCount or 2;
|
||||
|
||||
-- With a single term, use a higher 1.5x threshold; with 2+, 1.0x.
|
||||
local threshold = (maxCount > 1) and 1.0 or 1.5;
|
||||
|
||||
if seconds >= SECONDS_PER_DAY * threshold then
|
||||
count = count + 1;
|
||||
tempTime = (count == maxCount and roundUp) and math.ceil(seconds / SECONDS_PER_DAY)
|
||||
or math.floor(seconds / SECONDS_PER_DAY);
|
||||
time = FormatUnit(notAbbreviated and D_DAYS or DAYS_ABBR, tempTime);
|
||||
seconds = math.mod(seconds, SECONDS_PER_DAY);
|
||||
end
|
||||
if count < maxCount and seconds >= SECONDS_PER_HOUR * threshold then
|
||||
count = count + 1;
|
||||
if time ~= "" then time = time .. TIME_UNIT_DELIMITER; end
|
||||
tempTime = (count == maxCount and roundUp) and math.ceil(seconds / SECONDS_PER_HOUR)
|
||||
or math.floor(seconds / SECONDS_PER_HOUR);
|
||||
time = time .. FormatUnit(notAbbreviated and D_HOURS or HOURS_ABBR, tempTime);
|
||||
seconds = math.mod(seconds, SECONDS_PER_HOUR);
|
||||
end
|
||||
if count < maxCount and seconds >= SECONDS_PER_MIN * threshold then
|
||||
count = count + 1;
|
||||
if time ~= "" then time = time .. TIME_UNIT_DELIMITER; end
|
||||
tempTime = (count == maxCount and roundUp) and math.ceil(seconds / SECONDS_PER_MIN)
|
||||
or math.floor(seconds / SECONDS_PER_MIN);
|
||||
time = time .. FormatUnit(notAbbreviated and D_MINUTES or MINUTES_ABBR, tempTime);
|
||||
seconds = math.mod(seconds, SECONDS_PER_MIN);
|
||||
end
|
||||
if count < maxCount and seconds > 0 and not noSeconds then
|
||||
if time ~= "" then time = time .. TIME_UNIT_DELIMITER; end
|
||||
time = time .. FormatUnit(notAbbreviated and D_SECONDS or SECONDS_ABBR, seconds);
|
||||
end
|
||||
return time;
|
||||
end
|
||||
|
||||
-- Returns (formatString, value) for the single largest applicable unit.
|
||||
function SecondsToTimeAbbrev(seconds, thresholdOverride)
|
||||
local threshold = thresholdOverride or 1.5;
|
||||
if seconds >= SECONDS_PER_DAY * threshold then
|
||||
return DAY_ONELETTER_ABBR, math.ceil(seconds / SECONDS_PER_DAY);
|
||||
end
|
||||
if seconds >= SECONDS_PER_HOUR * threshold then
|
||||
return HOUR_ONELETTER_ABBR, math.ceil(seconds / SECONDS_PER_HOUR);
|
||||
end
|
||||
if seconds >= SECONDS_PER_MIN * threshold then
|
||||
return MINUTE_ONELETTER_ABBR, math.ceil(seconds / SECONDS_PER_MIN);
|
||||
end
|
||||
return SECOND_ONELETTER_ABBR, seconds;
|
||||
end
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
BINDING_NAME_AOELOOT = "AoE Loot"
|
||||
BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "g";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
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";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
|
||||
ITEM_MISSING = "%s missing";
|
||||
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "s";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
@@ -15,25 +23,3 @@ SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "g";
|
||||
SILVER_AMOUNT_SYMBOL = "s";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
TIME_UNIT_DELIMITER = " ";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
DAYS_ABBR = "%d |4Day:Days;";
|
||||
HOURS_ABBR = "%d |4Hr:Hr;";
|
||||
MINUTES_ABBR = "%d |4Min:Min;";
|
||||
SECONDS_ABBR = "%d |4Sec:Sec;";
|
||||
D_DAYS = "%d |4Day:Days;";
|
||||
D_HOURS = "%d |4Hour:Hours;";
|
||||
D_MINUTES = "%d |4Minute:Minutes;";
|
||||
D_SECONDS = "%d |4Second:Seconds;";
|
||||
DAY_ONELETTER_ABBR = "%d d";
|
||||
HOUR_ONELETTER_ABBR = "%d h";
|
||||
MINUTE_ONELETTER_ABBR = "%d m";
|
||||
SECOND_ONELETTER_ABBR = "%d s";
|
||||
|
||||
@@ -26,6 +26,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [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` |
|
||||
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID` |
|
||||
| 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` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid` |
|
||||
@@ -33,21 +34,23 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID` |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura` |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [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` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.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.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [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` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastingInfo`, `CastSpellNoToggle`, `ChannelInfo`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingInfo`, `UnitChannelInfo` |
|
||||
| [Screen](docs/API.md#screen) | `GetPhysicalScreenSize` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [Table](docs/API.md#table) | `table.wipe` |
|
||||
@@ -88,6 +91,18 @@ functions, just behavior the stock 1.12 engine didn't have. See the
|
||||
| `CastSpellByName("<spellID>")` | Same — numeric strings resolve through the engine's name resolver |
|
||||
| `CastSpellNoToggle("<name>")` in a macro | Engine's macro parser now recognizes it as a primary-spell line, so the macro slot in an addon like pfUI highlights when its spell is auto-repeating or its self-aura is active |
|
||||
|
||||
### Console commands
|
||||
|
||||
Registered on the engine's developer console (the `~` console available
|
||||
when launching with `-console`), not as Lua functions. See the
|
||||
[Console section in the Lua reference](docs/API.md#console) for details.
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `ExportInterfaceFiles code` | Extracts Blizzard's UI source (`.lua`/`.xml`/`.toc`/`.xsd`) from the MPQs to `BlizzardInterfaceCode\` |
|
||||
| `ExportInterfaceFiles art` | Extracts Blizzard's UI art (`.blp`/`.tga`) from the MPQs to `BlizzardInterfaceArt\` |
|
||||
| `ExportDBCFiles` | Extracts the client's `.dbc` tables from the MPQs to `DBFilesClient\` |
|
||||
|
||||
### Events
|
||||
|
||||
| Event | Payload |
|
||||
|
||||
+423
-47
@@ -33,6 +33,10 @@ build instructions.
|
||||
- [Combat](#combat)
|
||||
- [`InCombatLockdown()`](#incombatlockdown)
|
||||
|
||||
- [Console](#console)
|
||||
- [`ExportInterfaceFiles art|code` (console command)](#exportinterfacefiles-artcode-console-command)
|
||||
- [`ExportDBCFiles` (console command)](#exportdbcfiles-console-command)
|
||||
|
||||
- [Container](#container)
|
||||
- [`C_Container.GetContainerItemID(bagIndex, slotIndex)`](#c_containergetcontaineritemidbagindex-slotindex)
|
||||
- [`GetItemCooldown(itemInfo)` / `C_Container.GetItemCooldown(itemID)`](#getitemcooldowniteminfo--c_containergetitemcooldownitemid)
|
||||
@@ -54,6 +58,11 @@ build instructions.
|
||||
- [`coroutine.wrap(fn)`](#coroutinewrapfn)
|
||||
- [Async pattern (RunAsync + C_Timer.After)](#async-pattern-runasync--c_timerafter)
|
||||
|
||||
- [Creature](#creature)
|
||||
- [`C_CreatureInfo.GetCreatureID(guid)`](#c_creatureinfogetcreatureidguid)
|
||||
- [`C_CreatureInfo.GetCreatureInfoByID(creatureID)`](#c_creatureinfogetcreatureinfobyidcreatureid)
|
||||
- [`C_CreatureInfo.RequestLoadCreatureByID(creatureID)`](#c_creatureinforequestloadcreaturebyidcreatureid)
|
||||
|
||||
- [CVar](#cvar)
|
||||
- [`C_CVar.GetCVarBool(cvar)`](#c_cvargetcvarboolcvar)
|
||||
|
||||
@@ -122,6 +131,10 @@ build instructions.
|
||||
- [`C_FriendList.SendWhoQueryByName(name)`](#c_friendlistsendwhoquerybynamename)
|
||||
- [`C_FriendList.IsWhoQueryPending()`](#c_friendlistiswhoquerypending)
|
||||
|
||||
- [GameObject](#gameobject)
|
||||
- [`C_GameObjectInfo.GetGameObjectInfoByID(gameObjectID)`](#c_gameobjectinfogetgameobjectinfobyidgameobjectid)
|
||||
- [`C_GameObjectInfo.RequestLoadGameObjectByID(gameObjectID)`](#c_gameobjectinforequestloadgameobjectbyidgameobjectid)
|
||||
|
||||
- [GameTooltip](#gametooltip)
|
||||
- [`GameTooltip:SetSpellByID(spellID)`](#gametooltipsetspellbyidspellid)
|
||||
- [`GameTooltip:SetTalentByID(talentID)`](#gametooltipsettalentbyidtalentid)
|
||||
@@ -201,6 +214,7 @@ build instructions.
|
||||
- [`C_Item.UnlockAllItems()`](#c_itemunlockallitems)
|
||||
- [`C_Item.UnlockItem(itemLocation)`](#c_itemunlockitemitemlocation)
|
||||
- [`C_Item.UseAtCursor(itemInfo)`](#c_itemuseatcursoriteminfo)
|
||||
- [`C_Item.UseAtUnit(itemInfo, unit)`](#c_itemuseatunititeminfo-unit)
|
||||
- [`C_Item.UseItemByName(itemInfo [, unit])`](#c_itemuseitembynameiteminfo--unit)
|
||||
- [`Get*ItemID` — companions to the engine's `Get*ItemLink` family](#getitemid--companions-to-the-engines-getitemlink-family)
|
||||
- [`GetAverageItemLevel()`](#getaverageitemlevel)
|
||||
@@ -252,7 +266,6 @@ build instructions.
|
||||
- [`UnitNameFromGUID(guid)`](#unitnamefromguidguid)
|
||||
- [`C_PlayerCache.GetPlayerInfoByName(name)`](#c_playercachegetplayerinfobynamename)
|
||||
- [`C_PlayerInfo.GUIDIsPlayer(guid)` / `GUIDIsCreature` / `GUIDIsPet` / `GUIDIsGameObject`](#c_playerinfoguidisplayerguid--guidiscreature--guidispet--guidisgameobject)
|
||||
- [`C_CreatureInfo.GetCreatureID(guid)`](#c_creatureinfogetcreatureidguid)
|
||||
- [`C_PlayerCache.RememberPlayer(guid, name, classToken)`](#c_playercacherememberplayerguid-name-classtoken)
|
||||
- [`C_PlayerCache.SetEnabled(enabled)`](#c_playercachesetenabledenabled)
|
||||
- [`C_PlayerCache.IsEnabled()`](#c_playercacheisenabled)
|
||||
@@ -269,6 +282,9 @@ build instructions.
|
||||
- [`C_QuestLog.IsQuestDataCachedByID(questID)`](#c_questlogisquestdatacachedbyidquestid)
|
||||
- [`GetQuestLogLeaderBoardID(objectiveIndex [, questIndex])`](#getquestlogleaderboardidobjectiveindex--questindex)
|
||||
|
||||
- [Screen](#screen)
|
||||
- [`GetPhysicalScreenSize()`](#getphysicalscreensize)
|
||||
|
||||
- [Spell](#spell)
|
||||
- [`C_Spell.DoesSpellExist(spellID)`](#c_spelldoesspellexistspellid)
|
||||
- [`C_Spell.GetSchoolString(schoolMask)`](#c_spellgetschoolstringschoolmask)
|
||||
@@ -301,9 +317,10 @@ build instructions.
|
||||
- [`GetSpellSchool(spellID)`](#getspellschoolspellid)
|
||||
- [`CastSpellNoToggle(name | spellID)`](#castspellnotogglename--spellid)
|
||||
- [`C_Spell.CastAtCursor(spellID)`](#c_spellcastatcursorspellid)
|
||||
- [`C_Spell.CastAtUnit(spellID, unit)`](#c_spellcastatunitspellid-unit)
|
||||
- [`C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`](#c_spellcancelspellbyidspellid--cancelspellbynamename)
|
||||
- [`UnitCastingInfo(unit)` / `CastingInfo()`](#unitcastinginfounit--castinginfo)
|
||||
- [`UnitChannelInfo(unit)` / `ChannelInfo()`](#unitchannelinfounit--channelinfo)
|
||||
- [`C_Spell.UnitCastingInfo(unit)` / `C_Spell.CastingInfo()`](#c_spellunitcastinginfounit--c_spellcastinginfo)
|
||||
- [`C_Spell.UnitChannelInfo(unit)` / `C_Spell.ChannelInfo()`](#c_spellunitchannelinfounit--c_spellchannelinfo)
|
||||
|
||||
- [SpellBook](#spellbook)
|
||||
- [`FindSpellBookSlotByID(spellID)`](#findspellbookslotbyidspellid)
|
||||
@@ -831,6 +848,76 @@ if UnitAffectingCombat("player") then
|
||||
end
|
||||
```
|
||||
|
||||
## Console
|
||||
|
||||
### `ExportInterfaceFiles art|code` (console command)
|
||||
|
||||
A **developer-console command** (not a Lua function) — type it into the
|
||||
`~` console, which is available when the client is launched with
|
||||
`-console`. It extracts Blizzard's stock UI files out of the game's MPQ
|
||||
archives onto disk, mirroring the same-named command from later clients
|
||||
(4.3.4). Useful for reading the FrameXML / GlueXML source you're
|
||||
backporting addons against, or pulling out the default art.
|
||||
|
||||
- `ExportInterfaceFiles code` — writes `.lua` / `.xml` / `.toc` /
|
||||
`.xsd` files to `BlizzardInterfaceCode\…`
|
||||
- `ExportInterfaceFiles art` — writes `.blp` / `.tga` files to
|
||||
`BlizzardInterfaceArt\…`
|
||||
|
||||
Files are written relative to the client's working directory (next to
|
||||
`WoW.exe`), preserving their `Interface\…` subtree under the
|
||||
destination folder — e.g. `Interface\FrameXML\FrameXML.toc` becomes
|
||||
`BlizzardInterfaceCode\FrameXML\FrameXML.toc`. On completion the command
|
||||
prints a `wrote N file(s)` line back to the console.
|
||||
|
||||
```
|
||||
> ExportInterfaceFiles code
|
||||
ExportInterfaceFiles: wrote 1234 code file(s) to BlizzardInterfaceCode\
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- It enumerates the mounted MPQs' `(listfile)`, so it surfaces whatever
|
||||
the archives actually ship — including Blizzard's own UI addons under
|
||||
`Interface\AddOns\Blizzard_*` (AuctionUI, TalentUI, TradeSkillUI,
|
||||
etc.), which are part of the stock UI source. Files present in
|
||||
multiple archives (base + patches) are written once.
|
||||
- Your own loose, on-disk addons are *not* exported — they aren't in
|
||||
any archive's listfile, so they're never enumerated.
|
||||
- It runs synchronously and briefly freezes the client while it walks
|
||||
`Interface\` — expected for a one-shot extraction.
|
||||
|
||||
### `ExportDBCFiles` (console command)
|
||||
|
||||
A ClassicAPI-original companion to `ExportInterfaceFiles` that dumps
|
||||
every `.dbc` table the client loads to `DBFilesClient\` under the working
|
||||
directory (next to `WoW.exe`). No subcommand — there's only one thing to
|
||||
export.
|
||||
|
||||
```
|
||||
> ExportDBCFiles
|
||||
ExportDBCFiles: wrote 155 .dbc file(s) to DBFilesClient\
|
||||
```
|
||||
|
||||
Unlike the MPQ `(listfile)` alone (which omits ~18 of the tables the
|
||||
client actually loads — mostly cosmetic animation/sound/lookup DBCs that
|
||||
Blizzard left out of the index), this unions two sources for a complete
|
||||
set:
|
||||
|
||||
1. the **MPQ `(listfile)`** under `DBFilesClient\` — catches files that
|
||||
ship in the archives without a loader (e.g. `wowerror_strings.dbc`);
|
||||
2. a **`.text` scan for the DBC path-getter pattern**
|
||||
(`mov eax, &"DBFilesClient\X.dbc"; ret`) — the authoritative list of
|
||||
what this build loads. The master DBC init (`FUN_0053f8b0`) is
|
||||
straight-line with no iterable name table, so the per-DBC getters
|
||||
*are* the registry; scanning for them is the same technique that
|
||||
built [docs/DBCs.md](DBCs.md).
|
||||
|
||||
Results are deduped case-insensitively. Handy for a local DBC dump to
|
||||
inspect schemas/values directly — e.g. verifying a record's column
|
||||
layout against a known row, the way `C_Item.GetEnchantInfo`'s effect
|
||||
columns were confirmed — without a standalone MPQ extraction tool.
|
||||
|
||||
## Container
|
||||
|
||||
### `C_Container.GetContainerItemID(bagIndex, slotIndex)`
|
||||
@@ -1382,6 +1469,114 @@ 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.
|
||||
|
||||
## Creature
|
||||
|
||||
### `C_CreatureInfo.GetCreatureID(guid)`
|
||||
|
||||
Extracts the creature template / NPC ID from a unit GUID. Vanilla
|
||||
1.12 packs the entry ID directly into bits 24-47 of the 64-bit
|
||||
GUID for the types that carry one; this function does the shift
|
||||
and mask so addons don't have to.
|
||||
|
||||
```lua
|
||||
C_CreatureInfo.GetCreatureID(UnitGUID("target")) -- 1842 for Hogger
|
||||
C_CreatureInfo.GetCreatureID(UnitGUID("pet")) -- pet's creature template ID
|
||||
C_CreatureInfo.GetCreatureID(UnitGUID("player")) -- nil (no entry on players)
|
||||
```
|
||||
|
||||
Accepts creature GUIDs (`0xF130xxxx…`) and pet GUIDs
|
||||
(`0xF140xxxx…`). Returns `nil` for:
|
||||
- non-string input or malformed GUIDs
|
||||
- player GUIDs — the low 32 bits hold a player ID, not a template
|
||||
- game-object / dynamic-object / corpse / item GUIDs — modern's
|
||||
`C_CreatureInfo` doesn't surface entry IDs for these even though
|
||||
the bits are in the same range; addons that need them can shift
|
||||
the raw GUID themselves (`(guid >> 24) & 0xFFFFFF`)
|
||||
- entry IDs of 0 — the engine never assigns 0; treated as "no info"
|
||||
|
||||
The standard 16-digit (`"0xHHHHHHHHLLLLLLLL"`) and 8-digit
|
||||
(`"0xLLLLLLLL"`) GUID-string forms are both accepted, the same forms
|
||||
the `C_PlayerInfo.GUIDIs*` functions take.
|
||||
|
||||
The creatureID this returns is exactly what
|
||||
[`C_CreatureInfo.GetCreatureInfoByID`](#c_creatureinfogetcreatureinfobyidcreatureid)
|
||||
takes — `GetCreatureInfoByID(GetCreatureID(UnitGUID("target")))` gives
|
||||
the target's cached name/type/family/rank.
|
||||
|
||||
### `C_CreatureInfo.GetCreatureInfoByID(creatureID)`
|
||||
|
||||
Reads the client-side **creature cache** (`creaturecache.wdb`, fed by
|
||||
`SMSG_CREATURE_QUERY_RESPONSE`) for an NPC/creature by ID and returns a
|
||||
table — or `nil` if it isn't cached:
|
||||
|
||||
```lua
|
||||
local info = C_CreatureInfo.GetCreatureInfoByID(61332)
|
||||
-- {
|
||||
-- creatureID = 61332,
|
||||
-- name = "Misthoof Stag",
|
||||
-- subName = "", -- title/subtitle ("" if none)
|
||||
-- type = 1, -- CreatureType: 1=Beast, 7=Humanoid, …
|
||||
-- family = 0, -- CreatureFamily
|
||||
-- rank = 0, -- 0=normal, 1=elite, 2=rareelite, 3=worldboss, 4=rare
|
||||
-- displayID = 10957, -- model display ID
|
||||
-- }
|
||||
```
|
||||
|
||||
This is a synchronous **peek** — it returns data only for creatures
|
||||
already cached (seen this session, or loaded from `creaturecache.wdb`
|
||||
at login), and `nil` otherwise. Unlike `UnitCreatureType`/
|
||||
`UnitClassification`, which only work on a *live unit token*, this
|
||||
answers for any creatureID you have cached — even one not currently in
|
||||
the world.
|
||||
|
||||
Reads through the engine's generic cache `_GetRecord` (with no callback,
|
||||
so it's a pure lookup — no network query) and pulls the fields straight
|
||||
off the cached data block. Field offsets verified against the binary
|
||||
(rank read by the engine's classification helper at `[block+0x20]`) and
|
||||
real `creaturecache.wdb` rows (Misthoof Stag→type 1, Nordrassil
|
||||
Nymph→type 7 rank 1, Greathorn Hunter→family 26).
|
||||
|
||||
For a creature that isn't cached yet, call
|
||||
[`RequestLoadCreatureByID`](#c_creatureinforequestloadcreaturebyidcreatureid)
|
||||
first and read it once `CREATURE_DATA_LOAD_RESULT` fires.
|
||||
|
||||
### `C_CreatureInfo.RequestLoadCreatureByID(creatureID)`
|
||||
|
||||
Asynchronously fetches a creature that isn't in the cache yet — issues
|
||||
`SMSG_CREATURE_QUERY` and fires **`CREATURE_DATA_LOAD_RESULT(creatureID,
|
||||
success)`** when the response lands, at which point
|
||||
[`GetCreatureInfoByID`](#c_creatureinfogetcreatureinfobyidcreatureid)
|
||||
returns its data. The same shape as
|
||||
[`C_Item.RequestLoadItemData`](#c_itemrequestloaditemdatabyiditem--c_itemrequestloaditemdataitemlocation).
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("CREATURE_DATA_LOAD_RESULT")
|
||||
f:SetScript("OnEvent", function()
|
||||
-- 1.12 passes event args as globals: arg1 = creatureID, arg2 = success
|
||||
if arg2 then
|
||||
local info = C_CreatureInfo.GetCreatureInfoByID(arg1)
|
||||
-- info.name, info.type, info.rank, …
|
||||
end
|
||||
end)
|
||||
|
||||
C_CreatureInfo.RequestLoadCreatureByID(10184) -- Onyxia → fires the event,
|
||||
-- then GetCreatureInfoByID works
|
||||
```
|
||||
|
||||
Returns `true` if the request was accepted (`false` only on bad input
|
||||
or a full pending set). If the creature is **already** cached, the
|
||||
event fires synchronously with `success = true`. On a query that never
|
||||
resolves it fires once with `success = false` after a timeout.
|
||||
|
||||
Implementation: a shared `Cache::QueryLoad` dispatcher hooks the engine's
|
||||
generic cache response parser (`FUN_00556E20`) **once** and routes by
|
||||
cache instance — the same hook serves the gameobject cache too. The
|
||||
creature/gameobject caches use this generic parser; the item and quest
|
||||
caches have their own, so there's no hook collision. Verified in-game: `RequestLoadCreatureByID(10184)` on
|
||||
an uncached Onyxia fired the event and populated the cache (type 2
|
||||
Dragonkin, rank 3 world boss).
|
||||
|
||||
## CVar
|
||||
|
||||
### `C_CVar.GetCVarBool(cvar)`
|
||||
@@ -2809,6 +3004,48 @@ contribute to the same pending window. This is acceptable for the
|
||||
addons that need per-call tracking should manage their own ticket
|
||||
state on top.
|
||||
|
||||
## GameObject
|
||||
|
||||
The gameobject analog of [`C_CreatureInfo`](#creature) — reads the
|
||||
client-side gameobject cache (`gameobjectcache.wdb`, fed by
|
||||
`SMSG_GAMEOBJECT_QUERY_RESPONSE`) by GO entry ID.
|
||||
|
||||
### `C_GameObjectInfo.GetGameObjectInfoByID(gameObjectID)`
|
||||
|
||||
Synchronous **peek** — returns a table for a cached gameobject, `nil`
|
||||
otherwise:
|
||||
|
||||
```lua
|
||||
local info = C_GameObjectInfo.GetGameObjectInfoByID(47297)
|
||||
-- { gameObjectID = 47297, name = "Mesa Elevator", type = 11, displayID = 360 }
|
||||
```
|
||||
|
||||
`type` is the `GameObjectType` (0=door, 1=button, 3=chest, 5=generic,
|
||||
6=trap, 10=goober, …). Field offsets verified against the binary
|
||||
(name `[block+0x08]`) and real `gameobjectcache.wdb` rows (Mesa
|
||||
Elevator 47297→type 11; Windrunner 176250→type 15 displayID 7087).
|
||||
|
||||
### `C_GameObjectInfo.RequestLoadGameObjectByID(gameObjectID)`
|
||||
|
||||
Asynchronously fetches an uncached gameobject — issues
|
||||
`SMSG_GAMEOBJECT_QUERY` and fires **`GAMEOBJECT_DATA_LOAD_RESULT(
|
||||
gameObjectID, success)`** when it lands, after which
|
||||
`GetGameObjectInfoByID` returns its data. Returns `true` if accepted;
|
||||
fires synchronously on a cache hit. Same shape as
|
||||
[`C_CreatureInfo.RequestLoadCreatureByID`](#c_creatureinforequestloadcreaturebyidcreatureid),
|
||||
and rides the same shared `Cache::QueryLoad` dispatcher (the gameobject
|
||||
cache is a sibling class of the creature cache with its own
|
||||
`_GetRecord`/parser, hooked separately).
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("GAMEOBJECT_DATA_LOAD_RESULT")
|
||||
f:SetScript("OnEvent", function() -- arg1 = gameObjectID, arg2 = success
|
||||
if arg2 then local info = C_GameObjectInfo.GetGameObjectInfoByID(arg1) end
|
||||
end)
|
||||
C_GameObjectInfo.RequestLoadGameObjectByID(7000032)
|
||||
```
|
||||
|
||||
## GameTooltip
|
||||
|
||||
### `GameTooltip:SetItemByID(itemID)`
|
||||
@@ -4759,6 +4996,32 @@ Cancels placement automatically when the cursor isn't on terrain —
|
||||
the item-use packet is never sent, so an off-screen click doesn't
|
||||
waste the grenade.
|
||||
|
||||
### `C_Item.UseAtUnit(itemInfo, unit)`
|
||||
|
||||
Unit-position analog of
|
||||
[`C_Item.UseAtCursor`](#c_itemuseatcursoriteminfo): uses `itemInfo` at
|
||||
`unit`'s feet rather than the cursor. ClassicAPI's `[@unit]` for
|
||||
ground-target on-use items. `itemInfo` accepts the same forms as
|
||||
`UseAtCursor` (itemID, bare `"item:N"`, full chat link, localized
|
||||
name); `unit` is any unit token (`"player"`, `"target"`,
|
||||
`"mouseover"`, `"party1"`, …).
|
||||
|
||||
```lua
|
||||
C_Item.UseAtUnit(4068, "target") -- Iron Grenade at the target's feet
|
||||
C_Item.UseAtUnit("Iron Grenade", "player")
|
||||
```
|
||||
|
||||
Same chain as `UseAtCursor` (`Item::Location::FindByArgInBags` +
|
||||
`FUN_ITEM_USE`) but committing the placement at the unit's world
|
||||
position via
|
||||
[`Spell::AtCursor::CommitAtCoords`](#c_spellcastatunitspellid-unit)
|
||||
instead of the cursor raycast. Returns `true` when the placement
|
||||
landed at the unit; `false` for non-ground-target items (the item
|
||||
still fires with any implicit target), unparseable input,
|
||||
item-not-in-bags, or an unresolvable unit. The unit is resolved
|
||||
before the item fires, so an absent unit fails without consuming the
|
||||
item.
|
||||
|
||||
### `C_Item.UseItemByName(itemInfo [, unit])`
|
||||
|
||||
Finds the first item in the player's bags matching `itemInfo` and
|
||||
@@ -6175,33 +6438,6 @@ Accepts either the 16-digit `"0xHHHHHHHHLLLLLLLL"` form or the
|
||||
Malformed input returns `false` rather than raising — matching
|
||||
modern's tolerance for stale GUIDs from addon-side caches.
|
||||
|
||||
### `C_CreatureInfo.GetCreatureID(guid)`
|
||||
|
||||
Extracts the creature template / NPC ID from a unit GUID. Vanilla
|
||||
1.12 packs the entry ID directly into bits 24-47 of the 64-bit
|
||||
GUID for the types that carry one; this function does the shift
|
||||
and mask so addons don't have to.
|
||||
|
||||
```lua
|
||||
C_CreatureInfo.GetCreatureID(UnitGUID("target")) -- 1842 for Hogger
|
||||
C_CreatureInfo.GetCreatureID(UnitGUID("pet")) -- pet's creature template ID
|
||||
C_CreatureInfo.GetCreatureID(UnitGUID("player")) -- nil (no entry on players)
|
||||
```
|
||||
|
||||
Accepts creature GUIDs (`0xF130xxxx…`) and pet GUIDs
|
||||
(`0xF140xxxx…`). Returns `nil` for:
|
||||
- non-string input or malformed GUIDs
|
||||
- player GUIDs — the low 32 bits hold a player ID, not a template
|
||||
- game-object / dynamic-object / corpse / item GUIDs — modern's
|
||||
`C_CreatureInfo` doesn't surface entry IDs for these even though
|
||||
the bits are in the same range; addons that need them can shift
|
||||
the raw GUID themselves (`(guid >> 24) & 0xFFFFFF`)
|
||||
- entry IDs of 0 — the engine never assigns 0; treated as "no info"
|
||||
|
||||
The standard 16-digit (`"0xHHHHHHHHLLLLLLLL"`) and 8-digit
|
||||
(`"0xLLLLLLLL"`) GUID-string forms are both accepted, same as
|
||||
`C_PlayerInfo.GUIDIs*` above.
|
||||
|
||||
### `C_PlayerCache.SetEnabled(enabled)`
|
||||
|
||||
Opts into (or out of) the persistent name cache. `enabled` is a
|
||||
@@ -6581,6 +6817,40 @@ A separate function keeps the existing call wire-compatible.
|
||||
> first, skip zero slots, then the item array. 1-based `objectiveIndex`
|
||||
> counts only non-empty slots.
|
||||
|
||||
## Screen
|
||||
|
||||
### `GetPhysicalScreenSize()`
|
||||
|
||||
Returns `(widthPixels, heightPixels)` — the display's physical
|
||||
resolution in pixels. Modern WoW added this as a native in 7.0
|
||||
(Legion); FrameXML's `PixelUtil` builds its entire pixel↔UI-unit
|
||||
conversion on it (`768.0 / physicalHeight`), which is what lets addons
|
||||
snap frames to whole pixels for crisp borders at fractional UI scales.
|
||||
|
||||
```lua
|
||||
local w, h = GetPhysicalScreenSize() -- e.g. 1920, 1080
|
||||
```
|
||||
|
||||
Vanilla 1.12 has no such native — and no pre-parsed pixel-dimension
|
||||
global. `GetScreenWidth()`/`GetScreenHeight()` return UI-space units
|
||||
(derived from the UIParent aspect ratio at `[UIParent + 0x7c]`), not
|
||||
pixels. The engine only ever holds the current mode as the
|
||||
`gxResolution` CVar string (`"WIDTHxHEIGHT"`); both `SetScreenResolution`
|
||||
(`0x0048bfd0`) and the display-init path sscanf it on demand rather
|
||||
than caching dimensions anywhere. So we read the `gxResolution` CVar
|
||||
directly through the engine's by-name lookup (`FUN_FIND_CVAR`, value at
|
||||
`+0x20`) and parse it — the same source of truth the engine uses.
|
||||
|
||||
Returns `1024, 768` (→ factor 1.0, making PixelUtil a 1:1 no-op) when
|
||||
the CVar is missing or unparseable. Caveat: in windowed mode
|
||||
`gxResolution` reflects the configured render resolution, which may
|
||||
differ from the OS window's client size — vanilla exposes no separate
|
||||
window-pixel query.
|
||||
|
||||
The companion `PixelUtil` table (`PixelUtil.SetPoint`/`SetSize`/
|
||||
`SetStatusBarValue`/…) is provided Lua-side by the ClassicAPI addon and
|
||||
consumes this function.
|
||||
|
||||
## Spell
|
||||
|
||||
> **All `C_Spell.*` functions in this section accept any spell
|
||||
@@ -7325,6 +7595,70 @@ addons reading positions 4..8 by index still work.
|
||||
Equivalent to the extension of `GetWeaponEnchantInfo` introduced
|
||||
in 3.x.
|
||||
|
||||
### `C_Item.GetEnchantInfo(enchantID)`
|
||||
|
||||
Resolves an item-enchantment ID — the `enchantID` returned by
|
||||
[`C_Item.GetWeaponEnchantInfo`](#c_itemgetweaponenchantinfo) for a
|
||||
weapon's temporary enchant, and the same IDs item permanent enchants
|
||||
use — into a table:
|
||||
|
||||
```lua
|
||||
local info = C_Item.GetEnchantInfo(enchantID)
|
||||
-- info.enchantID = <id>
|
||||
-- info.name = "Crusader" -- localized display name
|
||||
-- info.effects = { {type=1, amount=0, arg=20007} }
|
||||
-- info.spellID = 20007 -- spell-type enchants only
|
||||
```
|
||||
|
||||
```lua
|
||||
-- A proc/equip enchant → chain its spellID into C_Spell:
|
||||
local info = C_Item.GetEnchantInfo(1900) -- "Crusader"
|
||||
if info.spellID then
|
||||
print(C_Spell.GetSpellDescription(info.spellID)) -- the proc's text
|
||||
end
|
||||
```
|
||||
|
||||
`effects` is an array of the record's non-empty effect slots, each
|
||||
`{ type, amount, arg }` where `type` is the standard
|
||||
`ITEM_ENCHANTMENT_TYPE`:
|
||||
|
||||
| type | meaning | carries |
|
||||
|------|---------|---------|
|
||||
| 1 | combat-proc spell | `arg` = spellID |
|
||||
| 2 | weapon damage | `amount` = +damage |
|
||||
| 3 | equip spell / aura | `arg` = spellID |
|
||||
| 4 | resistance / armor | `amount` = +value |
|
||||
| 5 | stat | `arg` = stat index, `amount` = value |
|
||||
| 6 | totem | — |
|
||||
| 7 | use spell | `arg` = spellID |
|
||||
|
||||
For spell types (1/3/7) `arg` is a spellID feedable into
|
||||
[`C_Spell.GetSpellInfo`](#getspellinfospellid--getspellinfoslot-booktype) /
|
||||
`GetSpellDescription`; the first such id is also surfaced at top level
|
||||
as `spellID` for convenience (absent for non-spell enchants like
|
||||
sharpening stones).
|
||||
|
||||
Returns `nil` for a non-numeric / non-positive id, an out-of-range
|
||||
id, or a record with no name.
|
||||
|
||||
Reads `SpellItemEnchantment.dbc` (records `0x00C0D7D8`, count
|
||||
`0x00C0D7DC`) — the 24-column table every enchant ID indexes:
|
||||
`Type[3]@+0x04`, `Amount[3]@+0x10`, `EffectArg[3]@+0x28`,
|
||||
`Name[8]@+0x34` (locale-indexed). The layout was verified by parsing
|
||||
the on-disk DBC against known records (Crusader 1900 → type 1, arg
|
||||
20007; Sharpened +3 → type 2, amount 3). Fully resident from boot, so
|
||||
it answers for any enchant ID with no caching or round-trip.
|
||||
|
||||
Lives in `C_Item` (not `C_Spell`) because the id originates from
|
||||
`C_Item.GetWeaponEnchantInfo` and the concept is an item enchantment —
|
||||
`SpellItemEnchantment` is just the DBC's internal name (enchants are
|
||||
*implemented* via spell effects).
|
||||
|
||||
> **Not derivable: the source item.** The enchant record holds no
|
||||
> back-reference to the item that applied it, and vanilla has no
|
||||
> client-side reverse index (enchantID → item). Finding it would need
|
||||
> an external scraped DB (pfQuest/Questie-style).
|
||||
|
||||
### `IsHarmfulSpell(spell)` / `IsHelpfulSpell(spell)`
|
||||
|
||||
Classify a spell as offensive (`IsHarmfulSpell`) or non-offensive
|
||||
@@ -7520,6 +7854,36 @@ The companion item version is
|
||||
[`C_Item.UseAtCursor`](#c_itemuseatcursoriteminfo) — same chain via
|
||||
the item-use path for grenades / on-use ground-target items.
|
||||
|
||||
### `C_Spell.CastAtUnit(spellID, unit)`
|
||||
|
||||
Casts a ground-target spell at `unit`'s feet, bypassing the AoE
|
||||
reticle click. ClassicAPI's analog of modern's
|
||||
`/cast [@unit] Flamestrike` — `CastAtUnit(spellID, "player")` drops it
|
||||
at the player's position, `"target"` at the target's, and so on for
|
||||
any unit token (`"mouseover"`, `"party1"`, `"raid7"`, …). Returns
|
||||
`true` when the placement landed at the unit; `false` for
|
||||
non-ground-target spells (cast still fires normally), unknown
|
||||
spellIDs, or a unit with no resolvable position.
|
||||
|
||||
```lua
|
||||
C_Spell.CastAtUnit(2120, "target") -- Flamestrike at the target's feet
|
||||
C_Spell.CastAtUnit(10, "player") -- Blizzard centered on yourself
|
||||
```
|
||||
|
||||
Same two-stage shape as
|
||||
[`C_Spell.CastAtCursor`](#c_spellcastatcursorspellid), but instead of
|
||||
the cursor raycast, step 2 reads the unit's world position from its
|
||||
`GetPosition` virtual (the same one `CheckInteractDistance` /
|
||||
`UnitInRange` use) and commits placement there via
|
||||
`FUN_COMMIT_PLACEMENT_COORDS`. The unit is resolved *before* the cast
|
||||
is dispatched, so an absent/invalid unit fails cleanly without
|
||||
leaving the engine armed in placement mode. A genuinely unrecognized
|
||||
unit-token string raises the engine's standard "Unknown unit" error,
|
||||
matching `UnitHealth("garbage")`.
|
||||
|
||||
The companion item version is
|
||||
[`C_Item.UseAtUnit`](#c_itemuseatunititeminfo-unit).
|
||||
|
||||
### `C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`
|
||||
|
||||
Cancel a buff on the player by spellID or by spell name. Modern WoW's
|
||||
@@ -7555,21 +7919,31 @@ pass the plain name. Multi-rank buffs match on first-found; if you
|
||||
have multiple ranks of the same buff active (unusual but possible with
|
||||
e.g. paladin blessings before talent merge), the first slot wins.
|
||||
|
||||
### `UnitCastingInfo(unit)` / `CastingInfo()`
|
||||
### `C_Spell.UnitCastingInfo(unit)` / `C_Spell.CastingInfo()`
|
||||
|
||||
Returns the unit's in-progress **regular cast** (not a channel), or
|
||||
`nil` if it isn't casting. `CastingInfo()` is `UnitCastingInfo("player")`
|
||||
without the unit-token lookup.
|
||||
`nil` if it isn't casting. `C_Spell.CastingInfo()` is
|
||||
`C_Spell.UnitCastingInfo("player")` without the unit-token lookup.
|
||||
|
||||
> **Under `C_Spell`, not the global `UnitCastingInfo`.** Modern WoW
|
||||
> exposes these as globals, but a global here would clobber addons that
|
||||
> ship their own vanilla cast-tracking via the `_G.UnitCastingInfo or
|
||||
> <fallback>` idiom — e.g. ShaguTweaks' `libcast` scrapes the combat
|
||||
> log for **remote/enemy** casts the 1.12 engine never exposes. If we
|
||||
> occupied the global, those addons would adopt our player-only version
|
||||
> and lose their (superior, for that case) fallback. So we cede the
|
||||
> global names and register under `C_Spell` instead. Same for
|
||||
> `UnitChannelInfo`/`ChannelInfo`.
|
||||
|
||||
```
|
||||
name, displayName, textureID, startTimeMs, endTimeMs, isTradeskill,
|
||||
castID, notInterruptible, castingSpellID, castBarID, delayTimeMs
|
||||
= UnitCastingInfo(unit)
|
||||
= C_Spell.UnitCastingInfo(unit)
|
||||
```
|
||||
|
||||
```lua
|
||||
-- mid-cast:
|
||||
UnitCastingInfo("player")
|
||||
C_Spell.UnitCastingInfo("player")
|
||||
-- "Conjure Food", "Conjure Food", "Interface\\Icons\\INV_Misc_Food_...",
|
||||
-- 907928268, 907931268, false, nil, false, 28612, nil, 0
|
||||
```
|
||||
@@ -7580,11 +7954,11 @@ so progress is `(GetTime()*1000 - startTimeMs) / (endTimeMs - startTimeMs)`.
|
||||
> **Local player only.** Vanilla 1.12 stores no cast times anywhere
|
||||
> readable (the cast bar is Lua-driven off `SPELLCAST_START`), and it
|
||||
> tracks regular casts for nobody but you — other units' casts exist
|
||||
> only as transient animation visuals. So `UnitCastingInfo(otherUnit)`
|
||||
> only as transient animation visuals. So `C_Spell.UnitCastingInfo(otherUnit)`
|
||||
> returns `nil`. We self-track the player's cast on a per-frame tick
|
||||
> (`VAR_CURRENT_CAST_SPELL`), stamping `startTimeMs = now` and
|
||||
> `endTimeMs = now + base cast time`. Instant casts return `nil` (no
|
||||
> cast bar). See [`UnitChannelInfo`](#unitchannelinfounit--channelinfo)
|
||||
> cast bar). See [`C_Spell.UnitChannelInfo`](#c_spellunitchannelinfounit--c_spellchannelinfo)
|
||||
> for channels, which *are* visible on other units.
|
||||
>
|
||||
> `isTradeskill` is real — the spell's `SPELL_ATTR_TRADESPELL` flag, so
|
||||
@@ -7598,30 +7972,32 @@ so progress is `(GetTime()*1000 - startTimeMs) / (endTimeMs - startTimeMs)`.
|
||||
> `castID`/`castBarID` = `nil`, `notInterruptible` = `false`,
|
||||
> `delayTimeMs` = `0`.
|
||||
|
||||
### `UnitChannelInfo(unit)` / `ChannelInfo()`
|
||||
### `C_Spell.UnitChannelInfo(unit)` / `C_Spell.ChannelInfo()`
|
||||
|
||||
Returns the unit's in-progress **channel**, or `nil` if it isn't
|
||||
channeling. `ChannelInfo()` is `UnitChannelInfo("player")` without the
|
||||
token lookup.
|
||||
channeling. `C_Spell.ChannelInfo()` is `C_Spell.UnitChannelInfo("player")`
|
||||
without the token lookup. (Under `C_Spell` rather than the global name
|
||||
for the same reason as
|
||||
[`C_Spell.UnitCastingInfo`](#c_spellunitcastinginfounit--c_spellcastinginfo).)
|
||||
|
||||
```
|
||||
name, displayName, textureID, startTimeMs, endTimeMs, isTradeskill,
|
||||
notInterruptible, spellID = UnitChannelInfo(unit)
|
||||
notInterruptible, spellID = C_Spell.UnitChannelInfo(unit)
|
||||
```
|
||||
|
||||
```lua
|
||||
ChannelInfo()
|
||||
C_Spell.ChannelInfo()
|
||||
-- "Blizzard", "Blizzard", "Interface\\Icons\\Spell_Frost_IceStorm",
|
||||
-- 907894314, 907902314, false, false, 10187
|
||||
```
|
||||
|
||||
Unlike casts, the channeled spellID **is** broadcast for every unit
|
||||
(`UNIT_FIELD_CHANNEL_SPELL`), so `UnitChannelInfo("target")` works on
|
||||
other units — returning `name`/`displayName`/`textureID`/`spellID` with
|
||||
**`nil` start/end times** (we only track the local player's channel
|
||||
(`UNIT_FIELD_CHANNEL_SPELL`), so `C_Spell.UnitChannelInfo("target")`
|
||||
works on other units — returning `name`/`displayName`/`textureID`/`spellID`
|
||||
with **`nil` start/end times** (we only track the local player's channel
|
||||
start). For the player, the full timing is present
|
||||
(`endTimeMs - startTimeMs` = channel duration). Same placeholder fields
|
||||
as `UnitCastingInfo`.
|
||||
as `C_Spell.UnitCastingInfo`.
|
||||
|
||||
## SpellBook
|
||||
|
||||
@@ -7868,7 +8244,7 @@ This is distinct from spell channeling: the function fires for
|
||||
*participants* who clicked the portal, not the warlock who cast
|
||||
Ritual of Summoning. Vanilla has no Lua surface for this state —
|
||||
the engine's `SPELLCAST_CHANNEL_*` events don't fire and
|
||||
`CastingInfo()` returns nothing — so addons that want to react to
|
||||
`C_Spell.CastingInfo()` returns nothing — so addons that want to react to
|
||||
the player being committed to a ritual (e.g. suppress autorun
|
||||
toggles, hide nameplate clicks, warn before movement) have no other
|
||||
way to detect it.
|
||||
|
||||
@@ -236,6 +236,25 @@ void PushLocalizedFormatInt(void *L, const char *globalName,
|
||||
}
|
||||
} // namespace Lua
|
||||
|
||||
namespace Console {
|
||||
namespace {
|
||||
using RegisterCommand_t = int(__fastcall *)(const char *name, void *handler,
|
||||
int category, const char *description);
|
||||
using Write_t = void(__fastcall *)(const char *line, int colorFlag);
|
||||
} // namespace
|
||||
|
||||
void RegisterCommand(const char *name, CommandHandler handler, int category,
|
||||
const char *description) {
|
||||
auto fn = reinterpret_cast<RegisterCommand_t>(Offsets::FUN_CONSOLE_COMMAND_REGISTER);
|
||||
fn(name, reinterpret_cast<void *>(handler), category, description);
|
||||
}
|
||||
|
||||
void Write(const char *line) {
|
||||
auto fn = reinterpret_cast<Write_t>(Offsets::FUN_CONSOLE_WRITE);
|
||||
fn(line, 0);
|
||||
}
|
||||
} // namespace Console
|
||||
|
||||
namespace {
|
||||
ModuleAutoRegister *g_moduleHead = nullptr;
|
||||
GlueModuleAutoRegister *g_glueModuleHead = nullptr;
|
||||
|
||||
+44
@@ -242,6 +242,50 @@ void PushLocalizedFormatInt(void *L, const char *globalName,
|
||||
const char *fallback, int n);
|
||||
} // namespace Lua
|
||||
|
||||
// Developer-console command system — the `~` console available when the
|
||||
// client is launched with `-console`. Vanilla maintains a
|
||||
// `TSExplicitList<CONSOLECOMMAND>`; these wrap the engine's registrar
|
||||
// and output paths. See the "Console commands & Interface file export"
|
||||
// section in CLAUDE.md for the underlying offsets and ABI.
|
||||
namespace Console {
|
||||
|
||||
// Console-command handler ABI: `args` is the command-line text after
|
||||
// the command name (empty string when invoked bare). The engine
|
||||
// ignores the return value; return 1 by convention.
|
||||
using CommandHandler = int(__fastcall *)(void *unused, const char *args);
|
||||
|
||||
// Help-grouping categories — the engine's `help` enumerates these nine
|
||||
// (in this order). Cosmetic: the value only affects which group `help`
|
||||
// lists the command under; the engine doesn't validate it. Values are
|
||||
// sequential table indices (graphics=1 confirmed via FUN_0066F6C0, the
|
||||
// rest follow the displayed order).
|
||||
enum Category {
|
||||
CATEGORY_DEBUG = 0,
|
||||
CATEGORY_GRAPHICS = 1,
|
||||
CATEGORY_CONSOLE = 2,
|
||||
CATEGORY_COMBAT = 3,
|
||||
CATEGORY_GAME = 4,
|
||||
CATEGORY_DEFAULT = 5,
|
||||
CATEGORY_NET = 6,
|
||||
CATEGORY_SOUND = 7,
|
||||
CATEGORY_GM = 8,
|
||||
};
|
||||
|
||||
// Registers a console command. `name` and `description` are stored BY
|
||||
// POINTER (the engine doesn't copy them), so pass string literals or
|
||||
// otherwise process-lifetime storage. Dedup-safe: re-registering the
|
||||
// same name is a harmless no-op, so it's fine to call from a hook that
|
||||
// fires more than once. Safe any time after engine boot — the command
|
||||
// table self-initializes on first use. `description` may be nullptr.
|
||||
void RegisterCommand(const char *name, CommandHandler handler, int category,
|
||||
const char *description);
|
||||
|
||||
// Writes a line to the console output buffer. No-ops cleanly when the
|
||||
// console isn't active, so it's always safe to call.
|
||||
void Write(const char *line);
|
||||
|
||||
} // namespace Console
|
||||
|
||||
// Self-registration for API modules. Each module .cpp declares a file-scope
|
||||
// `static const Game::ModuleAutoRegister _r{&RegisterLuaFunctions};`, which
|
||||
// chains itself onto a global list at DLL-load time. `RunModuleRegistrations`
|
||||
|
||||
+183
-9
@@ -1982,6 +1982,68 @@ enum Offsets {
|
||||
FUN_DBCACHE_QUEST_GET_RECORD = 0x00562A40,
|
||||
VAR_QUEST_LOG_SELECTED_QUEST_ID = 0x00BB7480,
|
||||
|
||||
// Creature cache (creaturecache.wdb, SMSG_CREATURE_QUERY_RESPONSE).
|
||||
// One instance of the generic client query-cache class; constructed
|
||||
// by the cache factory (FUN_00554b40 -> FUN_00556060 with sig
|
||||
// 'BOMW', "creaturecache.wdb", recordSize 0x60).
|
||||
VAR_CREATURE_CACHE = 0x00C0E354,
|
||||
// Creature-cache `_GetRecord` — `__thiscall(cache, id, const u64 *guid,
|
||||
// void *callback, void *userData, char dedup)`. Returns the data
|
||||
// block (entry+0x18) when loaded; with `callback==0` it only peeks
|
||||
// (no query), with a non-null callback it fires the network query on
|
||||
// a miss. Class-specific (reads the loaded flag at entry+0x50);
|
||||
// gameobjects are a sibling class with a different layout and their
|
||||
// own `_GetRecord` (FUN_GAMEOBJECT_GET_RECORD). Verified via
|
||||
// FUN_00604600 (`FUN_00556aa0(creatureID, &guid, cb, 0, 0)`).
|
||||
FUN_CREATURE_GET_RECORD = 0x00556AA0,
|
||||
// Creature-cache response parser — `__thiscall(cache, packetReader,
|
||||
// flag)`. Fills the entry data block, sets the loaded flag (entry+0x50),
|
||||
// and walks the entry's pending-callback list. The creature SMSG
|
||||
// handler `FUN_005550E0` calls it with ecx=the creature cache.
|
||||
// `Cache::QueryLoad` hooks it to fire CREATURE_DATA_LOAD_RESULT. The
|
||||
// item/quest caches have their own parsers, so it's conflict-free.
|
||||
FUN_CREATURE_QUERY_RESPONSE = 0x00556E20,
|
||||
// Creature-query record field offsets, within the data block returned
|
||||
// by FUN_CREATURE_GET_RECORD. Anchored by rank@+0x20 (read by the
|
||||
// classification helper FUN_00605620 as `[block+0x20]`) and the
|
||||
// wire/WDB field order (Name[4], SubName, typeFlags, type, family,
|
||||
// rank, unk, petSpellDataId, displayId), cross-checked against real
|
||||
// creaturecache.wdb rows (Misthoof Stag 61332 -> type 1; Nordrassil
|
||||
// Nymph 61582 -> type 7, rank 1). Strings are char* (the fixed 0x60
|
||||
// block holds pointers, not the WDB's inline strings).
|
||||
OFF_CREATURE_NAME = 0x00, // char *Name[4]; [0] is the primary name
|
||||
OFF_CREATURE_SUBNAME = 0x10, // char *SubName (title); "" if none
|
||||
OFF_CREATURE_TYPE = 0x18, // CreatureType (1=Beast, 7=Humanoid, ...)
|
||||
OFF_CREATURE_FAMILY = 0x1C, // CreatureFamily
|
||||
OFF_CREATURE_RANK = 0x20, // 0=normal,1=elite,2=rareelite,3=worldboss,4=rare
|
||||
OFF_CREATURE_DISPLAYID = 0x2C, // creature model display ID
|
||||
|
||||
// GameObject cache (gameobjectcache.wdb, SMSG_GAMEOBJECT_QUERY_RESPONSE).
|
||||
// Sibling cache class to creature (constructed by FUN_00554B90 ->
|
||||
// FUN_00557bd0, sig 'BOGW', recordSize 0x5E) — same struct shape but a
|
||||
// different vtable (0x0080913C) and a LARGER entry (loaded flag at
|
||||
// entry+0x98, not +0x50), so it has its own `_GetRecord` and parser.
|
||||
VAR_GAMEOBJECT_CACHE = 0x00C0E318,
|
||||
// GO `_GetRecord`, same signature as the creature one. Verified via
|
||||
// FUN_0062dc10 (`FUN_00558560(goEntry, &guid, 0,0,0)` then reads the
|
||||
// GO name at `[block+0x08]`).
|
||||
FUN_GAMEOBJECT_GET_RECORD = 0x00558560,
|
||||
// GO response parser — `__thiscall(cache, packetReader, flag)`. The GO
|
||||
// SMSG handler `FUN_00555100` calls it with ecx=the GO cache. Distinct
|
||||
// from the creature parser (sets loaded flag at entry+0x98), so
|
||||
// `Cache::QueryLoad` hooks it separately to fire GAMEOBJECT_DATA_LOAD_RESULT.
|
||||
FUN_GAMEOBJECT_QUERY_RESPONSE = 0x005588E0,
|
||||
// GO-query record field offsets, within the data block returned by
|
||||
// FUN_GAMEOBJECT_GET_RECORD. Wire/WDB order: type, displayId, Name[4],
|
||||
// then a 24-int data[] array. name@+0x08 verified (FUN_0062dc10 reads
|
||||
// `[block+0x08]` as the name); type@+0x00 / displayId@+0x04 follow from
|
||||
// the field order, cross-checked against real gameobjectcache.wdb rows
|
||||
// (Windrunner type 15 displayId 7087; Mesa Elevator 47297 type 11
|
||||
// displayId 360).
|
||||
OFF_GAMEOBJECT_TYPE = 0x00, // GameObjectType (0=door,3=chest,…)
|
||||
OFF_GAMEOBJECT_DISPLAYID = 0x04, // model display ID
|
||||
OFF_GAMEOBJECT_NAME = 0x08, // char *Name[4]; [0] is the primary name
|
||||
|
||||
// Action bar slot table — `uint[120]` (max slot index `< 0x78`).
|
||||
// Each entry is a packed action descriptor; top 4 bits are the type
|
||||
// tag, low 28 bits are the payload. Tags:
|
||||
@@ -2428,6 +2490,29 @@ enum Offsets {
|
||||
VAR_SPELLMECHANIC_COUNT = 0x00C0D7C8, // max mechanic ID
|
||||
OFF_SPELLMECHANIC_NAME = 0x04, // char *name[9], locale-indexed
|
||||
|
||||
// SpellItemEnchantment.dbc — the table a weapon/item enchant ID
|
||||
// (e.g. the enchantID from `C_Item.GetWeaponEnchantInfo`) indexes.
|
||||
// Records-ptr at instance +0x08, count at +0x0C (instance 0x00C0D7D0).
|
||||
// 24-DWORD records (recordSize 0x60). Layout fully verified by
|
||||
// parsing the on-disk SpellItemEnchantment.dbc against known records
|
||||
// (Crusader 1900: Type[0]=1, Arg[0]=20007 "Holy Strength";
|
||||
// Sharpened+3 id 13: Type[0]=2, Amount[0]=3; Reinforced Armor+8 id 15:
|
||||
// Type[0]=4, Amount[0]=8). OFF_..._NAME (+0x34) is cross-confirmed by
|
||||
// two engine readers (FUN_00496170 / FUN_00495d60) that push
|
||||
// `[rec + 0x34 + VAR_LOCALE_INDEX*4]` as the display name.
|
||||
// Type[i] : ITEM_ENCHANTMENT_TYPE — 1=combat-proc spell,
|
||||
// 2=damage, 3=equip spell, 4=resistance, 5=stat,
|
||||
// 6=totem, 7=use spell. 0 = empty slot.
|
||||
// Amount[i] : magnitude for damage/resist/stat types (min; vanilla
|
||||
// min==max). 0 for spell types (value lives in the spell).
|
||||
// Arg[i] : spellID for spell types (1/3/7); stat index for STAT.
|
||||
VAR_SPELLITEMENCHANT_RECORDS = 0x00C0D7D8, // SpellItemEnchantmentRecord *records[enchantID]
|
||||
VAR_SPELLITEMENCHANT_COUNT = 0x00C0D7DC, // max enchant ID
|
||||
OFF_SPELLITEMENCHANT_TYPE = 0x04, // int Type[3]
|
||||
OFF_SPELLITEMENCHANT_AMOUNT = 0x10, // int Amount[3] (EffectPointsMin)
|
||||
OFF_SPELLITEMENCHANT_ARG = 0x28, // int EffectArg[3]
|
||||
OFF_SPELLITEMENCHANT_NAME = 0x34, // char *name[8], locale-indexed
|
||||
|
||||
// Spell.dbc School field — 0-based integer at record +0x04.
|
||||
// Verified empirically against Fireball (133) → School=2 (Fire)
|
||||
// and Frostbolt (116) → School=4 (Frost) on Octo 1.12.1. Vanilla
|
||||
@@ -2820,8 +2905,12 @@ enum Offsets {
|
||||
// trailing zeros, hands the buffer to the caller. Caller frees
|
||||
// via `FUN_STORM_SMEM_FREE`.
|
||||
//
|
||||
// __cdecl int FileRead(
|
||||
// int unused, // always 0 from engine call sites
|
||||
// __stdcall int FileRead( // callee cleans 28 bytes (RET 0x1C);
|
||||
// // NOT __cdecl — see src/addons/Embedded.cpp
|
||||
// int archive, // 0 = merged VFS; or an archive
|
||||
// // handle to read from one MPQ
|
||||
// // (e.g. FUN_00648FB0 reads its
|
||||
// // "(listfile)" this way)
|
||||
// const char *path, // "Interface\AddOns\X\Y.lua"
|
||||
// void **outBuf, // Storm-allocated content
|
||||
// size_t *outSize, // optional, may be NULL
|
||||
@@ -2836,6 +2925,85 @@ enum Offsets {
|
||||
// normal `SMemFree` works without us tracking lifetime.
|
||||
FUN_FILE_READ = 0x00648620,
|
||||
|
||||
// MPQ file enumerator — walks every mounted archive's `(listfile)`
|
||||
// and invokes a callback for each entry whose path begins with a
|
||||
// given prefix. This is the engine's flat, recursive name walk (the
|
||||
// `SFileEnumFiles` analog), NOT a per-directory find. It's the
|
||||
// function the macro-icon loader `FUN_LOAD_MACRO_ICONS` uses to
|
||||
// surface MPQ-baked icons under `Interface\Icons\`.
|
||||
//
|
||||
// void __fastcall MpqEnumFiles(
|
||||
// int archiveSelector, // ecx — archive-set selector;
|
||||
// // the loader passes 6.
|
||||
// // Covers the primary
|
||||
// // mounted archives plus
|
||||
// // one selected extra.
|
||||
// const char *pathPrefix, // edx — e.g. "Interface\" —
|
||||
// // only listfile entries
|
||||
// // under this prefix are
|
||||
// // reported.
|
||||
// FileCb callback, // per-file callback (below)
|
||||
// void *userParam); // loader passes 0
|
||||
//
|
||||
// The callback is `int __fastcall(const char *fullPath /*ecx*/)`;
|
||||
// `fullPath` is the entry's full archive-relative path (e.g.
|
||||
// "Interface\FrameXML\Foo.lua"). Returning 0 STOPS enumeration —
|
||||
// return non-zero to keep going (note: the OPPOSITE of the disk
|
||||
// find at `FindFirstFileW`). The continue/stop sense is verified in
|
||||
// the per-archive walker `FUN_00648FB0`, whose loop does
|
||||
// `if (... && callback() == 0) break;`.
|
||||
//
|
||||
// Internally: `FUN_00401470` loops the archive-handle array
|
||||
// (`DAT_008826BC`, count `DAT_00882740`) calling the per-archive
|
||||
// listfile walker `FUN_00648FB0`, which reads each archive's
|
||||
// `(listfile)` via `FUN_FILE_READ`, splits on CR/LF, prefix-filters
|
||||
// via a trampoline, and dispatches the callback. Used by
|
||||
// `Interface::Export` (`ExportInterfaceFiles`).
|
||||
//
|
||||
// NB: the sibling `FUN_0042AD10` is a DISK-only walker (it resolves
|
||||
// to kernel32 `FindFirstFileW` and never sees MPQ contents) — don't
|
||||
// confuse the two. The macro `Icons.cpp` callback labels have the
|
||||
// disk/MPQ roles swapped; this entry is the authoritative note.
|
||||
FUN_MPQ_ENUM_FILES = 0x00401470,
|
||||
|
||||
// Console-command registrar — the vanilla equivalent of 4.3.4's
|
||||
// `FUN_00654c90`. Registers a developer-console command (the `~`
|
||||
// console you get when launching with `-console`).
|
||||
//
|
||||
// int __fastcall RegisterConsoleCommand(
|
||||
// const char *name, // ecx — command name; stored BY
|
||||
// // POINTER (no copy), so must
|
||||
// // have static lifetime
|
||||
// Handler handler, // edx — see below
|
||||
// int category, // help-grouping category id
|
||||
// const char *description);// help text, or NULL
|
||||
// Returns 1 on success, 0 if a command with that name already
|
||||
// exists (dedup-safe — repeat calls are harmless no-ops). The
|
||||
// command node is never freed for a static-literal name, so we
|
||||
// don't need SStrDup the way the event table does.
|
||||
//
|
||||
// The handler is `int __fastcall(void *unused /*ecx*/, const char
|
||||
// *args /*edx*/)`; `args` is the text after the command name
|
||||
// (`*args == '\0'` when invoked bare). Return 1. Categories seen in
|
||||
// the binary: graphics=1, help=2, misc=5 (we use 5, matching
|
||||
// 4.3.4's `ExportInterfaceFiles`); the value is cosmetic (only
|
||||
// affects `help` grouping) and unvalidated by the registrar.
|
||||
//
|
||||
// Self-initializes the command hash table on first use, so it is
|
||||
// safe to call any time after the engine's own console subsystem
|
||||
// boots (well before our glue / load-script hooks fire). Verified
|
||||
// via the bulk graphics registrar `FUN_0066F6C0` and the `help`
|
||||
// command's own registration in `FUN_006400E0`.
|
||||
FUN_CONSOLE_COMMAND_REGISTER = 0x0063F9E0,
|
||||
|
||||
// `ConsoleWrite(const char *line /*ecx*/, int colorFlag /*edx*/)` —
|
||||
// appends a line to the developer console's output buffer. No-ops
|
||||
// cleanly when the console isn't active (gated on the console-init
|
||||
// globals `DAT_00C4EC1C` / `DAT_00C4EC34`), so it's safe to call
|
||||
// unconditionally from a console-command handler. `colorFlag` 0 =
|
||||
// default. Used by `Interface::Export` for "wrote N files" feedback.
|
||||
FUN_CONSOLE_WRITE = 0x0063CB50,
|
||||
|
||||
// TOC parser — `__fastcall(this=addonName)`. Already documented in
|
||||
// CLAUDE.md under "AddOn registry & hot-reload". Dedup-safe: if
|
||||
// the addon is already in the hash table, returns immediately
|
||||
@@ -3379,13 +3547,19 @@ enum Offsets {
|
||||
// Bits 4-7 — WASD movement-key state. Each `MoveForwardStart`
|
||||
// / `StrafeLeftStart` / etc. pushes one of these as the bit
|
||||
// mask into the engine's button-press handler. Bit `0x1000` is
|
||||
// the autorun-active flag (set by `ToggleAutoRun` and the
|
||||
// both-mouse-buttons combo). `INPUT_FLAGS_MOVING_ANY` is the
|
||||
// engine's own "is the user currently inputting translational
|
||||
// movement" mask, used to drive `PLAYER_STARTED_MOVING` —
|
||||
// matches modern's "STOPPED fires on key release, even
|
||||
// mid-air" semantics because no physics/airborne bit is
|
||||
// involved.
|
||||
// the autorun-active flag (set by `ToggleAutoRun`).
|
||||
// `INPUT_FLAGS_MOVING_ANY` is the engine's own "is the user
|
||||
// currently inputting translational movement via keys/autorun"
|
||||
// mask, used to drive `PLAYER_STARTED_MOVING` — matches modern's
|
||||
// "STOPPED fires on key release, even mid-air" semantics because
|
||||
// no physics/airborne bit is involved.
|
||||
//
|
||||
// NOTE: the "hold both mouse buttons to run forward" gesture is
|
||||
// NOT in this mask. The button handler (`FUN_00514840`) drives
|
||||
// that forward move directly via `FUN_005103e0` and explicitly
|
||||
// *clears* the autorun bit when both buttons go down. Detect it
|
||||
// from `INPUT_FLAG_MOUSELOOK & INPUT_FLAG_FREE_LOOK` both held
|
||||
// instead — see `Player::InputEvents`.
|
||||
INPUT_FLAG_MOVE_FORWARD = 0x10,
|
||||
INPUT_FLAG_MOVE_BACKWARD = 0x20,
|
||||
INPUT_FLAG_STRAFE_LEFT = 0x40,
|
||||
|
||||
Vendored
+224
@@ -0,0 +1,224 @@
|
||||
// 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 "cache/QueryLoad.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Cache::QueryLoad {
|
||||
|
||||
namespace {
|
||||
|
||||
// A cache class's `_GetRecord`. callback=nullptr → pure peek (returns the
|
||||
// loaded data block or nullptr, no query). Non-null callback → also fires
|
||||
// the network query on a miss and stores `(callback, userData)` on the
|
||||
// entry's pending list. dedup=1 so repeat fetches for the same id before
|
||||
// the response lands don't queue the callback twice.
|
||||
using GetCacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, char dedup);
|
||||
|
||||
// A cache class's response parser, which we hook (e.g.
|
||||
// FUN_CREATURE_QUERY_RESPONSE / FUN_GAMEOBJECT_QUERY_RESPONSE).
|
||||
using CacheResponse_t = void(__thiscall *)(void *cache, void *packetReader, int flag);
|
||||
|
||||
// Engine callback shape (__stdcall, ret 8) — see Item::Data. We only pass
|
||||
// it to make `_GetRecord` send the query (the trigger is callback != 0);
|
||||
// completion is observed via the response hook + tick sweep.
|
||||
void __stdcall NoOpCallback(void * /*userData*/, int /*success*/) {}
|
||||
|
||||
const uint8_t *CacheFetch(uintptr_t getRecord, void *cache, uint32_t id, void *callback) {
|
||||
auto fn = reinterpret_cast<GetCacheRecord_t>(getRecord);
|
||||
const uint64_t zeroGuid = 0;
|
||||
void *userData =
|
||||
callback ? reinterpret_cast<void *>(static_cast<uintptr_t>(id)) : nullptr;
|
||||
return fn(cache, id, &zeroGuid, callback, userData, 1);
|
||||
}
|
||||
|
||||
struct Pending {
|
||||
uint32_t id;
|
||||
int ticksWaiting;
|
||||
};
|
||||
|
||||
constexpr int kMaxCaches = 8;
|
||||
constexpr int kMaxPending = 256;
|
||||
constexpr int kMaxWaitTicks = 1200; // give up and fire failure after this many WorldTicks
|
||||
|
||||
struct Registered {
|
||||
void *cache;
|
||||
const char *eventName;
|
||||
uintptr_t getRecord; // this cache class's _GetRecord
|
||||
Pending pending[kMaxPending];
|
||||
int pendingCount;
|
||||
};
|
||||
|
||||
Registered g_caches[kMaxCaches];
|
||||
int g_cacheCount = 0;
|
||||
|
||||
Registered *Find(void *cache) {
|
||||
for (int i = 0; i < g_cacheCount; ++i) {
|
||||
if (g_caches[i].cache == cache)
|
||||
return &g_caches[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Fire(const Registered *r, uint32_t id, bool success) {
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(r->eventName),
|
||||
static_cast<int>(id), success);
|
||||
}
|
||||
|
||||
int FindPending(const Registered *r, uint32_t id) {
|
||||
for (int i = 0; i < r->pendingCount; ++i) {
|
||||
if (r->pending[i].id == id)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void RemovePendingAt(Registered *r, int idx) {
|
||||
--r->pendingCount;
|
||||
if (idx != r->pendingCount)
|
||||
r->pending[idx] = r->pending[r->pendingCount];
|
||||
}
|
||||
|
||||
bool IsLoaded(const Registered *r, uint32_t id) {
|
||||
return CacheFetch(r->getRecord, r->cache, id, nullptr) != nullptr;
|
||||
}
|
||||
|
||||
// Fire + remove any pending id whose entry is now loaded.
|
||||
void Sweep(Registered *r) {
|
||||
for (int i = 0; i < r->pendingCount;) {
|
||||
if (IsLoaded(r, r->pending[i].id)) {
|
||||
const uint32_t id = r->pending[i].id;
|
||||
RemovePendingAt(r, i);
|
||||
Fire(r, id, true);
|
||||
continue;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
// Shared post-response dispatch: a parser for `cache` just ran, so sweep
|
||||
// that cache's pending set (no-op for caches we don't track).
|
||||
void DispatchSweep(void *cache) {
|
||||
Registered *r = Find(cache);
|
||||
if (r != nullptr)
|
||||
Sweep(r);
|
||||
}
|
||||
|
||||
// One response-handler hook per cache class. Each calls its own original
|
||||
// trampoline (the parser fills the entry + walks the engine's own
|
||||
// callback list), then sweeps via the shared dispatch. `__thiscall`
|
||||
// mimicked as `__fastcall(ecx, edx_unused, …)`.
|
||||
using Trampoline_t = void(__fastcall *)(void *cache, void *edx, void *packetReader, int flag);
|
||||
|
||||
CacheResponse_t g_origCreatureParser = nullptr;
|
||||
void __fastcall CreatureParser_h(void *cache, void * /*edx*/, void *packetReader, int flag) {
|
||||
reinterpret_cast<Trampoline_t>(g_origCreatureParser)(cache, nullptr, packetReader, flag);
|
||||
DispatchSweep(cache);
|
||||
}
|
||||
|
||||
CacheResponse_t g_origGameObjectParser = nullptr;
|
||||
void __fastcall GameObjectParser_h(void *cache, void * /*edx*/, void *packetReader, int flag) {
|
||||
reinterpret_cast<Trampoline_t>(g_origGameObjectParser)(cache, nullptr, packetReader, flag);
|
||||
DispatchSweep(cache);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookCreature{
|
||||
Offsets::FUN_CREATURE_QUERY_RESPONSE, reinterpret_cast<void *>(&CreatureParser_h),
|
||||
reinterpret_cast<void **>(&g_origCreatureParser)};
|
||||
const Game::HookAutoRegister _hookGameObject{
|
||||
Offsets::FUN_GAMEOBJECT_QUERY_RESPONSE, reinterpret_cast<void *>(&GameObjectParser_h),
|
||||
reinterpret_cast<void **>(&g_origGameObjectParser)};
|
||||
|
||||
// Timeout + safety net. The hooks cover the happy path; this fires
|
||||
// failure for ids that never resolve after `kMaxWaitTicks`, and
|
||||
// re-checks loaded as a backstop for fills observed off the hook path.
|
||||
void OnWorldTick() {
|
||||
for (int c = 0; c < g_cacheCount; ++c) {
|
||||
Registered *r = &g_caches[c];
|
||||
for (int i = 0; i < r->pendingCount;) {
|
||||
if (IsLoaded(r, r->pending[i].id)) {
|
||||
const uint32_t id = r->pending[i].id;
|
||||
RemovePendingAt(r, i);
|
||||
Fire(r, id, true);
|
||||
continue;
|
||||
}
|
||||
if (r->pending[i].ticksWaiting >= kMaxWaitTicks) {
|
||||
const uint32_t id = r->pending[i].id;
|
||||
RemovePendingAt(r, i);
|
||||
Fire(r, id, false);
|
||||
continue;
|
||||
}
|
||||
++r->pending[i].ticksWaiting;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
} // namespace
|
||||
|
||||
void Register(void *cacheInstance, const char *eventName, uintptr_t getRecordAddr) {
|
||||
if (cacheInstance == nullptr || eventName == nullptr || getRecordAddr == 0)
|
||||
return;
|
||||
if (Find(cacheInstance) != nullptr) // idempotent (safe across /reload)
|
||||
return;
|
||||
if (g_cacheCount >= kMaxCaches)
|
||||
return;
|
||||
g_caches[g_cacheCount].cache = cacheInstance;
|
||||
g_caches[g_cacheCount].eventName = eventName;
|
||||
g_caches[g_cacheCount].getRecord = getRecordAddr;
|
||||
g_caches[g_cacheCount].pendingCount = 0;
|
||||
++g_cacheCount;
|
||||
}
|
||||
|
||||
bool IsCached(void *cacheInstance, uint32_t id) {
|
||||
const Registered *r = Find(cacheInstance);
|
||||
return r != nullptr && IsLoaded(r, id);
|
||||
}
|
||||
|
||||
bool RequestLoad(void *cacheInstance, uint32_t id) {
|
||||
Registered *r = Find(cacheInstance);
|
||||
if (r == nullptr || id == 0)
|
||||
return false;
|
||||
|
||||
// Already cached — fire success synchronously (modern RequestLoad
|
||||
// contract: the completion event fires even on a cache hit).
|
||||
if (IsLoaded(r, id)) {
|
||||
Fire(r, id, true);
|
||||
return true;
|
||||
}
|
||||
// Already in flight — one completion event per id.
|
||||
if (FindPending(r, id) >= 0)
|
||||
return true;
|
||||
if (r->pendingCount >= kMaxPending)
|
||||
return false;
|
||||
|
||||
// Send the query (non-null callback is the trigger), then track until
|
||||
// the response hook / tick fires the event.
|
||||
CacheFetch(r->getRecord, cacheInstance, id, reinterpret_cast<void *>(&NoOpCallback));
|
||||
r->pending[r->pendingCount].id = id;
|
||||
r->pending[r->pendingCount].ticksWaiting = 0;
|
||||
++r->pendingCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Cache::QueryLoad
|
||||
Vendored
+65
@@ -0,0 +1,65 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Shared async-load machinery for the client query caches that aren't
|
||||
// item/quest — creature, gameobject, and (potentially) the other
|
||||
// SMSG_*_QUERY caches. Modeled on `Item::Data`'s pending/timeout/event
|
||||
// logic, generalized so several caches share one set of code.
|
||||
//
|
||||
// These caches are sibling classes: they share the record/entry hash-map
|
||||
// shape but differ in `_GetRecord` and response-parser per class (the
|
||||
// creature class reads its loaded flag at entry+0x50, the gameobject
|
||||
// class at +0x98, etc.). So a module registers BOTH its cache's
|
||||
// `_GetRecord` address and the dispatcher routes completion by cache
|
||||
// instance. The dispatcher hooks each class's response parser (MinHook
|
||||
// allows only one hook per target, but the parsers are distinct
|
||||
// functions, so hooking creature's and gameobject's separately is fine);
|
||||
// item/quest have their own parsers and aren't touched.
|
||||
//
|
||||
// static const char *kEvent = "CREATURE_DATA_LOAD_RESULT";
|
||||
// static const Event::Custom::AutoReserve _r{kEvent}; // reserve the event
|
||||
// ...
|
||||
// Cache::QueryLoad::Register(cacheInstance, kEvent,
|
||||
// Offsets::FUN_CREATURE_GET_RECORD); // at init
|
||||
// Cache::QueryLoad::RequestLoad(cacheInstance, id); // fire a load
|
||||
//
|
||||
// The completion event fires as `(id: number, success: bool)` — success
|
||||
// when the cache fills, failure on timeout.
|
||||
|
||||
namespace Cache::QueryLoad {
|
||||
|
||||
// Register a query cache for async-load tracking. `cacheInstance` is the
|
||||
// cache global (e.g. `Offsets::VAR_CREATURE_CACHE` cast to void*);
|
||||
// `eventName` is a Custom event the caller has reserved via
|
||||
// `Event::Custom::AutoReserve`, fired `(id, success)` on completion;
|
||||
// `getRecordAddr` is the cache class's `_GetRecord` (e.g.
|
||||
// `Offsets::FUN_CREATURE_GET_RECORD`). `cacheInstance`/`eventName` must
|
||||
// outlive the process (pass static values). Idempotent — calling again
|
||||
// for the same cache is a no-op (safe across /reload).
|
||||
void Register(void *cacheInstance, const char *eventName, uintptr_t getRecordAddr);
|
||||
|
||||
// True if `id` is already in the cache (pure peek, no network query).
|
||||
bool IsCached(void *cacheInstance, uint32_t id);
|
||||
|
||||
// Issue an async load for `id`. If already cached, fires the completion
|
||||
// event with success synchronously. Otherwise sends the query and tracks
|
||||
// the id until the response lands (or it times out). Returns false only
|
||||
// if the cache isn't registered or its pending set is full; true
|
||||
// otherwise (already-cached, already-pending, or newly-tracked).
|
||||
bool RequestLoad(void *cacheInstance, uint32_t id);
|
||||
|
||||
} // namespace Cache::QueryLoad
|
||||
@@ -0,0 +1,138 @@
|
||||
// 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_CreatureInfo.GetCreatureInfoByID(creatureID)` -> table | nil
|
||||
//
|
||||
// Reads the client-side creature cache (creaturecache.wdb, fed by
|
||||
// SMSG_CREATURE_QUERY_RESPONSE) for a creature/NPC by ID and returns:
|
||||
//
|
||||
// {
|
||||
// creatureID = <id>,
|
||||
// name = "Misthoof Stag", -- primary name
|
||||
// subName = "", -- title/subtitle ("" if none)
|
||||
// type = 1, -- CreatureType (1=Beast, 7=Humanoid, …)
|
||||
// family = 0, -- CreatureFamily
|
||||
// rank = 0, -- 0=normal,1=elite,2=rareelite,3=worldboss,4=rare
|
||||
// displayID = 10957, -- model display ID
|
||||
// }
|
||||
//
|
||||
// `GetCreatureInfoByID` is a synchronous *peek*: returns the data only
|
||||
// if the creature is already cached (seen this session, or loaded from
|
||||
// creaturecache.wdb at login); nil otherwise. `RequestLoadCreatureByID`
|
||||
// fetches an uncached creature — it issues the query and fires
|
||||
// `CREATURE_DATA_LOAD_RESULT(creatureID, success)` when the response
|
||||
// lands, via the shared `Cache::QueryLoad` dispatcher.
|
||||
//
|
||||
// Field offsets verified against the binary (rank read by the engine's
|
||||
// classification helper as `[block+0x20]`) and real creaturecache.wdb
|
||||
// rows. See Offsets.h `OFF_CREATURE_*`.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cache/QueryLoad.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Creature::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
// Completion event for RequestLoadCreatureByID. Reserved at static-init
|
||||
// so the engine allocates it in its event table; fired (creatureID,
|
||||
// success) by the Cache::QueryLoad dispatcher.
|
||||
constexpr const char *kCreatureDataLoadResult = "CREATURE_DATA_LOAD_RESULT";
|
||||
const Event::Custom::AutoReserve _reserveLoadResult{kCreatureDataLoadResult};
|
||||
|
||||
// Generic cache `_GetRecord`. With callback=nullptr it only peeks the
|
||||
// hash table (no network query) and returns the data block (entry+0x18)
|
||||
// if the creature is loaded, else nullptr.
|
||||
using GetCacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, char dedup);
|
||||
|
||||
const uint8_t *PeekCreature(uint32_t creatureID) {
|
||||
auto fn = reinterpret_cast<GetCacheRecord_t>(Offsets::FUN_CREATURE_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, creatureID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
int __fastcall Script_GetCreatureInfoByID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int creatureID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (creatureID <= 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *rec = PeekCreature(static_cast<uint32_t>(creatureID));
|
||||
if (rec == nullptr)
|
||||
return 0; // not cached -> nil
|
||||
|
||||
const char *name = *reinterpret_cast<const char *const *>(rec + Offsets::OFF_CREATURE_NAME);
|
||||
const char *subName =
|
||||
*reinterpret_cast<const char *const *>(rec + Offsets::OFF_CREATURE_SUBNAME);
|
||||
const uint32_t type = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_TYPE);
|
||||
const uint32_t family = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_FAMILY);
|
||||
const uint32_t rank = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_RANK);
|
||||
const uint32_t displayID =
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_DISPLAYID);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "creatureID", creatureID);
|
||||
Game::Lua::SetFieldString(L, "name", name); // NULL coerced to ""
|
||||
Game::Lua::SetFieldString(L, "subName", subName);
|
||||
Game::Lua::SetFieldNumber(L, "type", type);
|
||||
Game::Lua::SetFieldNumber(L, "family", family);
|
||||
Game::Lua::SetFieldNumber(L, "rank", rank);
|
||||
Game::Lua::SetFieldNumber(L, "displayID", displayID);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.RequestLoadCreatureByID(creatureID)` -> bool. Async
|
||||
// fetch for an uncached creature: issues SMSG_CREATURE_QUERY and fires
|
||||
// `CREATURE_DATA_LOAD_RESULT(creatureID, success)` when the response
|
||||
// lands (or on timeout). Fires success synchronously if already cached.
|
||||
// Returns true if the request was accepted (false for bad input or a
|
||||
// full pending set) — mirrors C_Item.RequestLoadItemData(ByID).
|
||||
int __fastcall Script_RequestLoadCreatureByID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int creatureID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (creatureID <= 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE);
|
||||
const bool accepted =
|
||||
Cache::QueryLoad::RequestLoad(cache, static_cast<uint32_t>(creatureID));
|
||||
Game::Lua::PushBool(L, accepted);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureInfoByID",
|
||||
&Script_GetCreatureInfoByID);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "RequestLoadCreatureByID",
|
||||
&Script_RequestLoadCreatureByID);
|
||||
Cache::QueryLoad::Register(reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE),
|
||||
kCreatureDataLoadResult, Offsets::FUN_CREATURE_GET_RECORD);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Creature::Info
|
||||
@@ -0,0 +1,117 @@
|
||||
// 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_GameObjectInfo.GetGameObjectInfoByID(gameObjectID)` -> table | nil
|
||||
// `C_GameObjectInfo.RequestLoadGameObjectByID(gameObjectID)` -> bool
|
||||
//
|
||||
// The gameobject analog of `C_CreatureInfo`. Reads the client-side
|
||||
// gameobject cache (gameobjectcache.wdb, fed by
|
||||
// SMSG_GAMEOBJECT_QUERY_RESPONSE) by GO entry ID:
|
||||
//
|
||||
// {
|
||||
// gameObjectID = <id>,
|
||||
// name = "Mesa Elevator",
|
||||
// type = 11, -- GameObjectType (0=door,3=chest,…)
|
||||
// displayID = 360, -- model display ID
|
||||
// }
|
||||
//
|
||||
// `GetGameObjectInfoByID` is a synchronous peek (nil if not cached);
|
||||
// `RequestLoadGameObjectByID` fetches an uncached GO and fires
|
||||
// `GAMEOBJECT_DATA_LOAD_RESULT(gameObjectID, success)` when it lands.
|
||||
//
|
||||
// The GO cache is a sibling class to the creature cache (its own
|
||||
// `_GetRecord` and response parser), but it rides the same shared
|
||||
// `Cache::QueryLoad` dispatcher. Field offsets: name@+0x08 verified
|
||||
// (FUN_0062dc10 reads `[block+0x08]`), type@+0x00 / displayID@+0x04 from
|
||||
// the wire/WDB field order. See Offsets.h `OFF_GAMEOBJECT_*`.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cache/QueryLoad.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace GameObject::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kGameObjectDataLoadResult = "GAMEOBJECT_DATA_LOAD_RESULT";
|
||||
const Event::Custom::AutoReserve _reserveLoadResult{kGameObjectDataLoadResult};
|
||||
|
||||
using GetCacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, char dedup);
|
||||
|
||||
// Peek the GO cache (no network query).
|
||||
const uint8_t *PeekGameObject(uint32_t goID) {
|
||||
auto fn = reinterpret_cast<GetCacheRecord_t>(Offsets::FUN_GAMEOBJECT_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_GAMEOBJECT_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, goID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
int __fastcall Script_GetGameObjectInfoByID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int goID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (goID <= 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *rec = PeekGameObject(static_cast<uint32_t>(goID));
|
||||
if (rec == nullptr)
|
||||
return 0; // not cached -> nil
|
||||
|
||||
const uint32_t type = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_GAMEOBJECT_TYPE);
|
||||
const uint32_t displayID =
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_GAMEOBJECT_DISPLAYID);
|
||||
const char *name = *reinterpret_cast<const char *const *>(rec + Offsets::OFF_GAMEOBJECT_NAME);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "gameObjectID", goID);
|
||||
Game::Lua::SetFieldString(L, "name", name); // NULL coerced to ""
|
||||
Game::Lua::SetFieldNumber(L, "type", type);
|
||||
Game::Lua::SetFieldNumber(L, "displayID", displayID);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_RequestLoadGameObjectByID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int goID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (goID <= 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_GAMEOBJECT_CACHE);
|
||||
const bool accepted = Cache::QueryLoad::RequestLoad(cache, static_cast<uint32_t>(goID));
|
||||
Game::Lua::PushBool(L, accepted);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_GameObjectInfo", "GetGameObjectInfoByID",
|
||||
&Script_GetGameObjectInfoByID);
|
||||
Game::Lua::RegisterTableFunction("C_GameObjectInfo", "RequestLoadGameObjectByID",
|
||||
&Script_RequestLoadGameObjectByID);
|
||||
Cache::QueryLoad::Register(reinterpret_cast<void *>(Offsets::VAR_GAMEOBJECT_CACHE),
|
||||
kGameObjectDataLoadResult, Offsets::FUN_GAMEOBJECT_GET_RECORD);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace GameObject::Info
|
||||
@@ -0,0 +1,409 @@
|
||||
// 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/>.
|
||||
|
||||
// `ExportInterfaceFiles art|code` — backport of the 4.3.4 developer-
|
||||
// console command of the same name. Registered as a real console
|
||||
// command (the `~` console available when the client is launched with
|
||||
// `-console`), exactly like retail. Dumps Blizzard's UI files out of
|
||||
// the mounted MPQ archives onto disk:
|
||||
//
|
||||
// code → .lua / .xml / .toc / .xsd → BlizzardInterfaceCode\...
|
||||
// art → .blp / .tga → BlizzardInterfaceArt\...
|
||||
//
|
||||
// `ExportDBCFiles` — ClassicAPI-original companion that dumps every
|
||||
// `.dbc` table the client loads to `DBFilesClient\`. It unions the MPQ
|
||||
// `(listfile)` with a scan of `.text` for the DBC path-getter pattern
|
||||
// (`mov eax, &"DBFilesClient\X.dbc"; ret`) — the latter is the
|
||||
// authoritative "what does this build load" list, since the master DBC
|
||||
// init is straight-line (no name table), and it catches the ~18 DBCs the
|
||||
// listfile doesn't index. See `ScanPathGetters`.
|
||||
//
|
||||
// Files land relative to the client's working directory, mirroring the
|
||||
// retail command's `BlizzardInterface{Code,Art}\<relative-path>`
|
||||
// layout. On completion it writes a "wrote N files" line back to the
|
||||
// console.
|
||||
//
|
||||
// **How it works (vanilla 1.12 / Octo):**
|
||||
//
|
||||
// Like 4.3.4, we enumerate the mounted archives' `(listfile)` — the
|
||||
// flat, full-path file index — rather than walking directories. The
|
||||
// vanilla primitive is `FUN_MPQ_ENUM_FILES` (`0x00401470`, the same
|
||||
// MPQ walk the macro-icon loader uses): it loops the archive handles
|
||||
// and dispatches a callback per listfile entry under a path prefix
|
||||
// (here `"Interface\"`). We collect every matching full path (deduped
|
||||
// across archives), then read each via `FUN_FILE_READ` and write it to
|
||||
// disk with Win32.
|
||||
//
|
||||
// Don't confuse this with `FUN_0042AD10`, the disk-only
|
||||
// `FindFirstFileW` walker — pointing the export at that yields zero
|
||||
// MPQ files. See the `FUN_MPQ_ENUM_FILES` note in Offsets.h.
|
||||
//
|
||||
// We export the full `Interface\` tree, matching 4.3.4 (extension
|
||||
// filter only, no path exclusion). That INCLUDES `Interface\AddOns\` —
|
||||
// the listfile only indexes MPQ-baked files, so those AddOns entries
|
||||
// are Blizzard's own UI addons (Blizzard_AuctionUI, Blizzard_TalentUI,
|
||||
// …), which are part of the stock UI source. The player's loose,
|
||||
// on-disk addons live in no archive's listfile, so they're never
|
||||
// enumerated and never re-exported.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace Interface::Export {
|
||||
|
||||
namespace {
|
||||
|
||||
// Engine's file-content reader (see Offsets::FUN_FILE_READ). __stdcall
|
||||
// (callee cleans 28 bytes via RET 0x1C — NOT __cdecl; getting this
|
||||
// wrong corrupts the caller's stack). Allocates a Storm buffer; free
|
||||
// with SMemFree.
|
||||
using FileRead_t = int(__stdcall *)(int unused, const char *path, void **outBuf,
|
||||
unsigned int *outSize, unsigned int extraBytes,
|
||||
int flag1, int flag2);
|
||||
using SMemFree_t = void(__stdcall *)(void *ptr, const char *file, int line, int flags);
|
||||
|
||||
// MPQ enumerator (see Offsets::FUN_MPQ_ENUM_FILES). The callback
|
||||
// receives each listfile entry's full path in ecx; returning 0 STOPS
|
||||
// enumeration, so we return 1 to keep going.
|
||||
using MpqEnumCb_t = int(__fastcall *)(const char *fullPath);
|
||||
using MpqEnumFiles_t = void(__fastcall *)(int archiveSelector, const char *pathPrefix,
|
||||
MpqEnumCb_t callback, void *userParam);
|
||||
|
||||
// The archive-set selector the engine's own macro-icon loader passes to
|
||||
// FUN_00401470 — covers the primary mounted archives plus the selected
|
||||
// extra. We mirror it.
|
||||
constexpr int kArchiveSelector = 6;
|
||||
|
||||
// Case-insensitive ASCII equality.
|
||||
bool EqualsCI(const char *a, const char *b) {
|
||||
for (;; ++a, ++b) {
|
||||
char ca = *a, cb = *b;
|
||||
if (ca >= 'A' && ca <= 'Z') ca = static_cast<char>(ca + ('a' - 'A'));
|
||||
if (cb >= 'A' && cb <= 'Z') cb = static_cast<char>(cb + ('a' - 'A'));
|
||||
if (ca != cb)
|
||||
return false;
|
||||
if (ca == '\0')
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool HasExtension(const char *name, const char *const *exts, int count) {
|
||||
const char *dot = nullptr;
|
||||
for (const char *p = name; *p; ++p) {
|
||||
if (*p == '.')
|
||||
dot = p;
|
||||
}
|
||||
if (dot == nullptr)
|
||||
return false;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (EqualsCI(dot + 1, exts[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create every parent directory of `relPath` (a backslash-separated
|
||||
// path, e.g. "BlizzardInterfaceCode\FrameXML\Foo.lua"). Each component
|
||||
// is created relative to the working directory; existing dirs are a
|
||||
// no-op.
|
||||
void EnsureParentDirs(const std::string &relPath) {
|
||||
std::string accum;
|
||||
for (size_t i = 0; i < relPath.size(); ++i) {
|
||||
const char c = relPath[i];
|
||||
if (c == '\\' || c == '/') {
|
||||
if (!accum.empty())
|
||||
CreateDirectoryA(accum.c_str(), nullptr);
|
||||
accum.push_back('\\');
|
||||
} else {
|
||||
accum.push_back(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool WriteFileToDisk(const std::string &relPath, const void *data, unsigned int size) {
|
||||
EnsureParentDirs(relPath);
|
||||
HANDLE h = CreateFileA(relPath.c_str(), GENERIC_WRITE, 0, nullptr,
|
||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (h == INVALID_HANDLE_VALUE)
|
||||
return false;
|
||||
bool ok = true;
|
||||
if (size != 0) {
|
||||
DWORD written = 0;
|
||||
ok = WriteFile(h, data, size, &written, nullptr) != 0 && written == size;
|
||||
}
|
||||
CloseHandle(h);
|
||||
return ok;
|
||||
}
|
||||
|
||||
const char *const kCodeExts[] = {"lua", "xml", "toc", "xsd"};
|
||||
const char *const kArtExts[] = {"blp", "tga"};
|
||||
const char *const kDbcExts[] = {"dbc"};
|
||||
|
||||
enum Mode { MODE_CODE, MODE_ART };
|
||||
|
||||
std::string LowerCopy(const char *s) {
|
||||
std::string out;
|
||||
for (const char *p = s; *p; ++p) {
|
||||
char c = *p;
|
||||
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c + ('a' - 'A'));
|
||||
out.push_back(c);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Collector for the MPQ enumeration callback (and the path-getter scan).
|
||||
// File-static because FUN_00401470 hands the callback only the path
|
||||
// string; enumeration is synchronous and single-threaded, so a
|
||||
// file-static sink is safe.
|
||||
struct Collector {
|
||||
const char *const *exts = nullptr;
|
||||
int extCount = 0;
|
||||
std::vector<std::string> files;
|
||||
std::unordered_set<std::string> seen; // lowercased keys — dedup across sources
|
||||
|
||||
// Case-insensitive dedup so the listfile and the path-getter scan
|
||||
// (which may differ in casing) never write the same DBC twice.
|
||||
void Add(const char *path) {
|
||||
if (seen.insert(LowerCopy(path)).second)
|
||||
files.emplace_back(path);
|
||||
}
|
||||
};
|
||||
Collector *g_collector = nullptr;
|
||||
|
||||
// MPQ enumeration callback — one call per listfile entry under the
|
||||
// active prefix. Keep going (return 1) regardless.
|
||||
int __fastcall CollectCb(const char *fullPath) {
|
||||
Collector *c = g_collector;
|
||||
if (c != nullptr && fullPath != nullptr &&
|
||||
HasExtension(fullPath, c->exts, c->extCount))
|
||||
c->Add(fullPath);
|
||||
return 1; // 0 would stop enumeration
|
||||
}
|
||||
|
||||
// --- DBC path-getter scan ---------------------------------------------
|
||||
//
|
||||
// The DBC loaders aren't driven by any iterable name table — the master
|
||||
// init (`FUN_0053f8b0`) is straight-line, calling ~150 per-DBC loaders in
|
||||
// sequence, each of which calls its own one-instruction path-getter:
|
||||
//
|
||||
// B8 <&"DBFilesClient\Foo.dbc"> mov eax, <strVA>
|
||||
// C3 ret
|
||||
//
|
||||
// So the authoritative, dynamic list of "what DBCs does this build load"
|
||||
// is the set of those getters. We recover it the same way docs/DBCs.md
|
||||
// did — scan `.text` for the `B8 imm32 C3` pattern and keep matches whose
|
||||
// immediate points at a `.dbc` string. This catches every loaded table,
|
||||
// including the ~18 the MPQ `(listfile)` doesn't index.
|
||||
|
||||
// Build-specific bounds (this 1.12.1 client). `.text` per CLAUDE.md; the
|
||||
// image range bounds candidate string pointers so the suffix check never
|
||||
// dereferences unmapped memory. Getter strings live in `.data` (e.g.
|
||||
// Spell.dbc at 0x00859E30), comfortably inside the image.
|
||||
constexpr uintptr_t kTextStart = 0x00401000;
|
||||
constexpr uintptr_t kTextEnd = 0x007FEDAC;
|
||||
constexpr uintptr_t kImageStart = 0x00400000;
|
||||
constexpr uintptr_t kImageEnd = 0x00D26000;
|
||||
|
||||
// True if `s` (bounded, never reading past the image) names a ".dbc" file.
|
||||
bool LooksLikeDbcString(const uint8_t *s) {
|
||||
const uint8_t *limit = reinterpret_cast<const uint8_t *>(kImageEnd);
|
||||
size_t n = 0;
|
||||
while (n < 260 && s + n < limit && s[n] != '\0')
|
||||
++n;
|
||||
if (s + n >= limit || s[n] != '\0' || n < 4) // not terminated, or too short
|
||||
return false;
|
||||
return EqualsCI(reinterpret_cast<const char *>(s + n - 4), ".dbc");
|
||||
}
|
||||
|
||||
// Scan .text for the DBC path-getter pattern and add each referenced
|
||||
// ".dbc" path to the collector. Synchronous, reads only our own image.
|
||||
void ScanPathGetters(Collector &c) {
|
||||
const uint8_t *p = reinterpret_cast<const uint8_t *>(kTextStart);
|
||||
const uint8_t *end = reinterpret_cast<const uint8_t *>(kTextEnd) - 6;
|
||||
for (; p <= end; ++p) {
|
||||
if (p[0] != 0xB8 || p[5] != 0xC3) // mov eax, imm32 ; ret
|
||||
continue;
|
||||
uint32_t imm;
|
||||
std::memcpy(&imm, p + 1, sizeof(imm));
|
||||
if (imm < kImageStart || imm >= kImageEnd)
|
||||
continue;
|
||||
const uint8_t *s = reinterpret_cast<const uint8_t *>(imm);
|
||||
if (LooksLikeDbcString(s))
|
||||
c.Add(reinterpret_cast<const char *>(s));
|
||||
}
|
||||
}
|
||||
|
||||
// Read each collected source path from the MPQs and write it under
|
||||
// `dstRoot`, re-rooted by stripping the leading `prefixLen` chars (the
|
||||
// "Interface\" / "DBFilesClient\" prefix). Returns the number written.
|
||||
int WriteFiles(const std::vector<std::string> &files, size_t prefixLen,
|
||||
const char *dstRoot) {
|
||||
auto FileRead = reinterpret_cast<FileRead_t>(Offsets::FUN_FILE_READ);
|
||||
auto SMemFree = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
|
||||
int written = 0;
|
||||
for (const std::string &src : files) {
|
||||
void *buf = nullptr;
|
||||
unsigned int size = 0;
|
||||
if (FileRead(0, src.c_str(), &buf, &size, 0, 1, 0) == 0 || buf == nullptr)
|
||||
continue;
|
||||
|
||||
// Re-root: "<prefix>Sub\Foo.ext" -> "<dstRoot>\Sub\Foo.ext".
|
||||
std::string dst = dstRoot;
|
||||
dst.push_back('\\');
|
||||
dst.append(src, prefixLen, std::string::npos);
|
||||
|
||||
if (WriteFileToDisk(dst, buf, size))
|
||||
++written;
|
||||
|
||||
SMemFree(buf, __FILE__, __LINE__, 0);
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
// Enumerate every MPQ file under `pathPrefix` whose extension matches
|
||||
// one of `exts`, read each, and write it under `dstRoot`. Returns the
|
||||
// number of files successfully written.
|
||||
int ExportTree(const char *pathPrefix, const char *const *exts, int extCount,
|
||||
const char *dstRoot) {
|
||||
auto MpqEnumFiles = reinterpret_cast<MpqEnumFiles_t>(Offsets::FUN_MPQ_ENUM_FILES);
|
||||
|
||||
Collector collector;
|
||||
collector.exts = exts;
|
||||
collector.extCount = extCount;
|
||||
|
||||
g_collector = &collector;
|
||||
MpqEnumFiles(kArchiveSelector, pathPrefix, &CollectCb, nullptr);
|
||||
g_collector = nullptr;
|
||||
|
||||
return WriteFiles(collector.files, std::strlen(pathPrefix), dstRoot);
|
||||
}
|
||||
|
||||
// Export the DBC tables. Unions two sources, deduped case-insensitively:
|
||||
// 1. the MPQ (listfile) under DBFilesClient\ (catches files that ship
|
||||
// in the archives without a loader, e.g. wowerror_strings.dbc), and
|
||||
// 2. the .text path-getter scan (the authoritative set the client
|
||||
// actually loads — catches the ~18 DBCs absent from the listfile).
|
||||
// Every DBC path is "DBFilesClient\Name.dbc", so re-rooting strips that
|
||||
// 14-char prefix and writes under `dstRoot`.
|
||||
int ExportDBC(const char *dstRoot) {
|
||||
auto MpqEnumFiles = reinterpret_cast<MpqEnumFiles_t>(Offsets::FUN_MPQ_ENUM_FILES);
|
||||
|
||||
Collector collector;
|
||||
collector.exts = kDbcExts;
|
||||
collector.extCount = 1;
|
||||
|
||||
g_collector = &collector;
|
||||
MpqEnumFiles(kArchiveSelector, "DBFilesClient\\", &CollectCb, nullptr);
|
||||
g_collector = nullptr;
|
||||
|
||||
ScanPathGetters(collector);
|
||||
|
||||
return WriteFiles(collector.files, std::strlen("DBFilesClient\\"), dstRoot);
|
||||
}
|
||||
|
||||
// Console-command handler ABI: edx = the argument text after the
|
||||
// command name ("" when bare). Mirrors 4.3.4's `art`/`code` dispatch.
|
||||
bool FirstTokenEquals(const char *s, const char *token) {
|
||||
size_t i = 0;
|
||||
for (; token[i] != '\0'; ++i) {
|
||||
char a = s[i];
|
||||
if (a >= 'A' && a <= 'Z') a = static_cast<char>(a + ('a' - 'A'));
|
||||
if (a != token[i])
|
||||
return false;
|
||||
}
|
||||
const char end = s[i];
|
||||
return end == '\0' || end == ' ' || end == '\t';
|
||||
}
|
||||
|
||||
int __fastcall Console_ExportInterfaceFiles(void * /*unused*/, const char *args) {
|
||||
const char *p = (args != nullptr) ? args : "";
|
||||
while (*p == ' ' || *p == '\t')
|
||||
++p;
|
||||
|
||||
Mode mode;
|
||||
if (FirstTokenEquals(p, "art"))
|
||||
mode = MODE_ART;
|
||||
else if (FirstTokenEquals(p, "code"))
|
||||
mode = MODE_CODE;
|
||||
else {
|
||||
Game::Console::Write("Usage: ExportInterfaceFiles art|code");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const int written = ExportTree(
|
||||
"Interface\\",
|
||||
(mode == MODE_ART) ? kArtExts : kCodeExts,
|
||||
(mode == MODE_ART) ? 2 : 4,
|
||||
(mode == MODE_ART) ? "BlizzardInterfaceArt" : "BlizzardInterfaceCode");
|
||||
|
||||
char msg[160];
|
||||
snprintf(msg, sizeof(msg), "ExportInterfaceFiles: wrote %d %s file(s) to %s\\",
|
||||
written, (mode == MODE_ART) ? "art" : "code",
|
||||
(mode == MODE_ART) ? "BlizzardInterfaceArt" : "BlizzardInterfaceCode");
|
||||
Game::Console::Write(msg);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `ExportDBCFiles` — ClassicAPI-original companion to ExportInterfaceFiles.
|
||||
// Dumps every `.dbc` table the client loads to `DBFilesClient\` under the
|
||||
// working directory. No subcommand — there's only one thing to export.
|
||||
// Unions the MPQ (listfile) with a `.text` path-getter scan so it gets
|
||||
// the complete set the client loads, including DBCs the listfile omits.
|
||||
int __fastcall Console_ExportDBCFiles(void * /*unused*/, const char * /*args*/) {
|
||||
const int written = ExportDBC("DBFilesClient");
|
||||
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"ExportDBCFiles: wrote %d .dbc file(s) to DBFilesClient\\", written);
|
||||
Game::Console::Write(msg);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Register the console command. A console command is process-global
|
||||
// (not Lua-state-bound) and persists for the whole session, so the
|
||||
// single registration at glue boot — the earliest hook, firing at
|
||||
// launch before any console can be opened — covers both the login
|
||||
// console and in-world. The glue hook re-fires on each world→glue
|
||||
// return (logout), but the engine's registrar dedups by name (and we
|
||||
// pass the same string literal, so its pointer-equality fast path
|
||||
// catches the repeat), making a second call a no-op. The command name
|
||||
// and help string are stored by pointer, so they must be static —
|
||||
// string literals satisfy that for the DLL's lifetime.
|
||||
void EnsureRegistered() {
|
||||
Game::Console::RegisterCommand(
|
||||
"ExportInterfaceFiles", &Console_ExportInterfaceFiles,
|
||||
Game::Console::CATEGORY_DEBUG,
|
||||
"Extracts Blizzard's UI files from the MPQs to disk. "
|
||||
"Usage: ExportInterfaceFiles art|code");
|
||||
Game::Console::RegisterCommand(
|
||||
"ExportDBCFiles", &Console_ExportDBCFiles,
|
||||
Game::Console::CATEGORY_DEBUG,
|
||||
"Extracts the client's .dbc tables from the MPQs to disk. "
|
||||
"Usage: ExportDBCFiles");
|
||||
}
|
||||
|
||||
const Game::GlueModuleAutoRegister _autoreg{&EnsureRegistered};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Interface::Export
|
||||
@@ -0,0 +1,139 @@
|
||||
// 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.GetEnchantInfo(enchantID)` -> table | nil
|
||||
//
|
||||
// Resolves an item-enchantment ID — the kind `C_Item.GetWeaponEnchantInfo`
|
||||
// returns for a weapon's temporary enchant (poisons, oils, sharpening
|
||||
// stones, shaman imbues, …), and the same IDs item permanent enchants
|
||||
// use — into its `SpellItemEnchantment.dbc` record and returns a table:
|
||||
//
|
||||
// {
|
||||
// enchantID = <id>,
|
||||
// name = "<localized display name>", -- "Crusader", "+8 Spirit"
|
||||
// effects = { -- 1..3 active effect slots
|
||||
// { type = <ITEM_ENCHANTMENT_TYPE>, amount = <int>, arg = <int> },
|
||||
// ...
|
||||
// },
|
||||
// spellID = <int>, -- present only for spell-type enchants (proc/
|
||||
// -- equip/use): the EffectArg of the first such
|
||||
// -- effect, e.g. Crusader -> 20007 "Holy Strength".
|
||||
// }
|
||||
//
|
||||
// `effects[i].type` is the standard ITEM_ENCHANTMENT_TYPE:
|
||||
// 1 = combat-proc spell (arg = spellID)
|
||||
// 2 = weapon damage (amount = +dmg)
|
||||
// 3 = equip spell / aura (arg = spellID)
|
||||
// 4 = resistance / armor (amount = +value)
|
||||
// 5 = stat (arg = stat index, amount = value)
|
||||
// 6 = totem, 7 = use spell
|
||||
// For spell types (1/3/7) `arg` is a spellID you can feed straight into
|
||||
// `C_Spell.GetSpellInfo` / `GetSpellDescription`; `spellID` is surfaced
|
||||
// at top level as a convenience for that common case.
|
||||
//
|
||||
// Returns nil for a non-numeric / non-positive id, an out-of-range id,
|
||||
// or a record with no name. Lives in C_Item (not C_Spell) because the
|
||||
// id originates from `C_Item.GetWeaponEnchantInfo` and the concept is an
|
||||
// item enchantment — `SpellItemEnchantment` is just the DBC's internal
|
||||
// name (enchants are *implemented* via spell effects).
|
||||
//
|
||||
// Not derivable: the source item. The record holds no back-reference to
|
||||
// the item that applied the enchant, and vanilla has no client-side
|
||||
// reverse index (enchantID -> item).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Item::EnchantInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kNumEffects = 3;
|
||||
constexpr int kNumNameLocales = 8; // Name[8]; field after is the locale mask
|
||||
|
||||
// Spell-type enchants whose EffectArg is a spellID (proc / equip / use).
|
||||
bool IsSpellType(int type) { return type == 1 || type == 3 || type == 7; }
|
||||
|
||||
int __fastcall Script_C_Item_GetEnchantInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int enchantID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (enchantID <= 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELLITEMENCHANT_RECORDS,
|
||||
Offsets::VAR_SPELLITEMENCHANT_COUNT,
|
||||
static_cast<uint32_t>(enchantID));
|
||||
if (rec == nullptr)
|
||||
return 0; // OOR id or unpopulated slot -> nil
|
||||
|
||||
// Localized display name (8-slot locale array at +0x34). Clamp the
|
||||
// locale index to the array bounds — index 8 would read the locale
|
||||
// mask column that follows. enUS (0) fallback for empty slots.
|
||||
auto names = reinterpret_cast<const char *const *>(rec + Offsets::OFF_SPELLITEMENCHANT_NAME);
|
||||
int locale = *reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
if (locale < 0 || locale >= kNumNameLocales)
|
||||
locale = 0;
|
||||
const char *name = names[locale];
|
||||
if (name == nullptr || name[0] == '\0')
|
||||
name = names[0]; // enUS fallback
|
||||
if (name == nullptr || name[0] == '\0')
|
||||
return 0; // unnamed record -> nil
|
||||
|
||||
auto type = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELLITEMENCHANT_TYPE);
|
||||
auto amount = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELLITEMENCHANT_AMOUNT);
|
||||
auto arg = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELLITEMENCHANT_ARG);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "enchantID", enchantID);
|
||||
Game::Lua::SetFieldString(L, "name", name);
|
||||
|
||||
// effects = { {type,amount,arg}, ... } — only non-empty slots.
|
||||
Game::Lua::PushString(L, "effects");
|
||||
Game::Lua::NewTable(L);
|
||||
int n = 0;
|
||||
int spellID = 0;
|
||||
for (int i = 0; i < kNumEffects; ++i) {
|
||||
if (type[i] == 0)
|
||||
continue;
|
||||
if (spellID == 0 && IsSpellType(type[i]) && arg[i] != 0)
|
||||
spellID = arg[i];
|
||||
++n;
|
||||
Game::Lua::PushNumber(L, n); // 1-based array index
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "type", type[i]);
|
||||
Game::Lua::SetFieldNumber(L, "amount", amount[i]);
|
||||
Game::Lua::SetFieldNumber(L, "arg", arg[i]);
|
||||
Game::Lua::SetTable(L, -3); // effects[n] = entry
|
||||
}
|
||||
Game::Lua::SetTable(L, -3); // result.effects = effects
|
||||
|
||||
if (spellID != 0)
|
||||
Game::Lua::SetFieldNumber(L, "spellID", spellID);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetEnchantInfo",
|
||||
&Script_C_Item_GetEnchantInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Item::EnchantInfo
|
||||
@@ -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/>.
|
||||
|
||||
// `C_Item.UseAtUnit(itemInfo, unit)` — use the named item at the given
|
||||
// unit's feet. For items whose on-use spell is ground-target (Iron
|
||||
// Grenade, demolition charges, etc.), bypasses the manual click on the
|
||||
// AoE reticle. `UseAtUnit(itemInfo, "target")` drops the reticle at
|
||||
// the target's position.
|
||||
//
|
||||
// Unit-position analog of `C_Item.UseAtCursor` — same bag-walk +
|
||||
// `FUN_ITEM_USE` dispatch, but the placement is committed at the
|
||||
// unit's world position (via `Spell::AtCursor::CommitAtCoords`)
|
||||
// instead of the cursor raycast.
|
||||
//
|
||||
// Returns `true` when the placement landed at the unit; `false` for
|
||||
// non-ground-target items (the item still fires, no implicit target),
|
||||
// unparseable input, item-not-in-bags, or an unresolvable unit.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Location.h"
|
||||
#include "spell/AtCursor.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Item::UseAtUnit {
|
||||
|
||||
namespace {
|
||||
|
||||
using UseItem_t = unsigned(__thiscall *)(const void *item, const uint64_t *targetGuid,
|
||||
int flag);
|
||||
|
||||
int __fastcall Script_C_Item_UseAtUnit(void *L) {
|
||||
const auto arg = Item::Arg::Resolve(L, 1);
|
||||
if (arg.itemID <= 0 && arg.name == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 2);
|
||||
|
||||
// Resolve the unit position first — fail fast before firing the
|
||||
// item if the unit is absent. (FindByArgInBags below stomps the
|
||||
// Lua stack, so read the token argument before that runs.)
|
||||
float pos[3];
|
||||
if (!Unit::Position::ReadToken(token, pos)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Item::Location::ByGUIDResult found;
|
||||
if (!Item::Location::FindByArgInBags(L, arg, &found)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint64_t zeroTarget = 0;
|
||||
auto useItem = reinterpret_cast<UseItem_t>(Offsets::FUN_ITEM_USE);
|
||||
useItem(found.item, &zeroTarget, 0);
|
||||
|
||||
const bool placed = Spell::AtCursor::CommitAtCoords(pos);
|
||||
Game::Lua::PushBool(L, placed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Item", "UseAtUnit",
|
||||
&Script_C_Item_UseAtUnit);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Item::UseAtUnit
|
||||
@@ -111,10 +111,21 @@ void OnWorldTick() {
|
||||
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
ctrl + Offsets::OFF_UI_INPUT_FLAGS);
|
||||
const bool moving = (flags & Offsets::INPUT_FLAGS_MOVING_ANY) != 0;
|
||||
const bool freeLookHeld = (flags & Offsets::INPUT_FLAG_FREE_LOOK) != 0;
|
||||
const bool mouselookHeld = (flags & Offsets::INPUT_FLAG_MOUSELOOK) != 0;
|
||||
|
||||
// MOVING — translational input. The WASD / autorun keys set the
|
||||
// INPUT_FLAGS_MOVING_ANY bits directly. The "hold both mouse
|
||||
// buttons to run forward" gesture does NOT: the engine drives that
|
||||
// forward move at the movement layer (FUN_005103e0 from the button
|
||||
// handler) and actually *clears* the autorun bit on the both-button
|
||||
// press, so no MOVING_ANY bit is ever set. Detect it from the
|
||||
// bit combo instead — both mouselook (RMB) and free-look (LMB)
|
||||
// held means the engine is mouse-steering the character forward.
|
||||
const bool mouseSteerMoving = mouselookHeld && freeLookHeld;
|
||||
const bool moving =
|
||||
(flags & Offsets::INPUT_FLAGS_MOVING_ANY) != 0 || mouseSteerMoving;
|
||||
|
||||
// TURNING — latched bit-AND-rotation signal:
|
||||
// STARTED fires when mouselook bit is held AND the body yaw
|
||||
// changes (the first actual drag after pressing RMB).
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// 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/>.
|
||||
|
||||
// `GetPhysicalScreenSize()` → (widthPixels, heightPixels) — the
|
||||
// display's physical resolution in pixels. Modern WoW added this as a
|
||||
// native in 7.0 (Legion); FrameXML's `PixelUtil` hangs its entire
|
||||
// pixel↔UI-unit conversion off it (`768.0 / physicalHeight`), which is
|
||||
// what lets addons snap frames to whole pixels.
|
||||
//
|
||||
// Vanilla 1.12 has no such native and no pre-parsed pixel-dimension
|
||||
// global — the engine only ever holds the current mode as the
|
||||
// `gxResolution` CVar string ("WIDTHxHEIGHT"). `Script_GetScreenWidth`
|
||||
// / `GetScreenHeight` return UI-space units (derived from the UIParent
|
||||
// aspect ratio at `[UIParent + 0x7c]`), not pixels, so they can't
|
||||
// serve. `SetScreenResolution` (`FUN_0048bfd0`) and the display-init
|
||||
// path both sscanf the `gxResolution` value on demand rather than
|
||||
// caching width/height anywhere — so the CVar string is the
|
||||
// authoritative current-resolution source, and we parse it the same
|
||||
// way the engine does.
|
||||
//
|
||||
// Returns 1024x768 (→ factor 1.0, i.e. 1 pixel == 1 UI unit, making
|
||||
// PixelUtil a no-op pass-through) when the CVar is missing or
|
||||
// unparseable. Caveat: in windowed mode `gxResolution` reflects the
|
||||
// configured render resolution, which may differ from the OS window's
|
||||
// client size — vanilla exposes no separate window-pixel query.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Screen::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
using FindCVar_t = const uint8_t *(__fastcall *)(const char *name);
|
||||
|
||||
// Parses "WIDTHxHEIGHT" (e.g. "1920x1080"). Tolerant of the separator
|
||||
// (any run of non-digits between the two numbers) so an uppercase 'X'
|
||||
// or stray spaces don't break it. Returns false unless both numbers
|
||||
// are present and positive.
|
||||
bool ParseResolution(const char *s, int *outW, int *outH) {
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
|
||||
const char *p = s;
|
||||
int w = 0;
|
||||
bool gotW = false;
|
||||
while (*p >= '0' && *p <= '9') {
|
||||
w = w * 10 + (*p - '0');
|
||||
++p;
|
||||
gotW = true;
|
||||
}
|
||||
if (!gotW)
|
||||
return false;
|
||||
|
||||
while (*p != '\0' && (*p < '0' || *p > '9')) // skip the 'x' separator
|
||||
++p;
|
||||
|
||||
int h = 0;
|
||||
bool gotH = false;
|
||||
while (*p >= '0' && *p <= '9') {
|
||||
h = h * 10 + (*p - '0');
|
||||
++p;
|
||||
gotH = true;
|
||||
}
|
||||
if (!gotH || w <= 0 || h <= 0)
|
||||
return false;
|
||||
|
||||
*outW = w;
|
||||
*outH = h;
|
||||
return true;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetPhysicalScreenSize(void *L) {
|
||||
int width = 1024;
|
||||
int height = 768; // fallback → 768/768 = factor 1.0 (no-op snapping)
|
||||
|
||||
auto findCVar = reinterpret_cast<FindCVar_t>(Offsets::FUN_FIND_CVAR);
|
||||
const uint8_t *cvar = findCVar("gxResolution");
|
||||
if (cvar != nullptr) {
|
||||
const char *value = *reinterpret_cast<const char *const *>(
|
||||
cvar + Offsets::OFF_CVAR_VALUE_STR);
|
||||
ParseResolution(value, &width, &height);
|
||||
}
|
||||
|
||||
Game::Lua::PushNumber(L, width);
|
||||
Game::Lua::PushNumber(L, height);
|
||||
return 2;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetPhysicalScreenSize",
|
||||
&Script_GetPhysicalScreenSize);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Screen::Info
|
||||
+51
-23
@@ -70,9 +70,17 @@ void CancelPlacement() {
|
||||
fn();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
void Commit(const float coords[3]) {
|
||||
auto commit = reinterpret_cast<CommitPlacementCoords_t>(
|
||||
Offsets::FUN_COMMIT_PLACEMENT_COORDS);
|
||||
commit(coords);
|
||||
}
|
||||
|
||||
bool Resolve() {
|
||||
// Shared placement-readiness gate for both the cursor and explicit-
|
||||
// coords commit paths. Decision tree (preserved from the original
|
||||
// Resolve): no placement → false (no cancel); non-ground placement →
|
||||
// cancel + false; not in world → false; otherwise ready → true.
|
||||
bool PlacementReady() {
|
||||
const uint32_t state = *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_SPELL_PLACEMENT_STATE);
|
||||
if (state == 0)
|
||||
@@ -81,12 +89,25 @@ bool Resolve() {
|
||||
if ((state & Offsets::SPELL_PLACEMENT_GROUND_MASK) == 0) {
|
||||
// Spell wants a unit / item / corpse / etc. — not ground.
|
||||
// Cancel so the cursor recovers; caller learns via the false
|
||||
// return that no cursor-placement happened.
|
||||
// return that no placement happened.
|
||||
CancelPlacement();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InWorld())
|
||||
return InWorld();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool CommitAtCoords(const float coords[3]) {
|
||||
if (!PlacementReady())
|
||||
return false;
|
||||
Commit(coords);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Resolve() {
|
||||
if (!PlacementReady())
|
||||
return false;
|
||||
|
||||
void *clickInfo = *reinterpret_cast<void *const *>(
|
||||
@@ -114,9 +135,7 @@ bool Resolve() {
|
||||
const float *coords = reinterpret_cast<const float *>(
|
||||
static_cast<const uint8_t *>(clickInfo) + Offsets::OFF_CLICK_INFO_COORDS);
|
||||
|
||||
auto commit = reinterpret_cast<CommitPlacementCoords_t>(
|
||||
Offsets::FUN_COMMIT_PLACEMENT_COORDS);
|
||||
commit(coords);
|
||||
Commit(coords);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -126,16 +145,11 @@ using NameToSlot_t = int(__fastcall *)(const char *name, void *out);
|
||||
using CastDispatch_t = void(__fastcall *)(unsigned slot, int bookType,
|
||||
unsigned targetGuidLo, float targetGuidHi);
|
||||
|
||||
int __fastcall Script_C_Spell_CastAtCursor(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (spellID <= 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool DispatchSpellCast(int spellID) {
|
||||
if (spellID <= 0)
|
||||
return false;
|
||||
|
||||
// Hand the spellID off as a numeric string — our `Spell::CastByID`
|
||||
// hook on `FUN_RESOLVE_SPELL_NAME_TO_SLOT` translates pure-numeric
|
||||
@@ -148,17 +162,31 @@ int __fastcall Script_C_Spell_CastAtCursor(void *L) {
|
||||
Offsets::FUN_RESOLVE_SPELL_NAME_TO_SLOT);
|
||||
int bookType = 0;
|
||||
const int slot = nameToSlot(numBuf, &bookType);
|
||||
if (slot < 0) {
|
||||
// Spell not in the player's spellbook — can't cast it from
|
||||
// here either way. Match `CastSpellByName`'s silent failure.
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (slot < 0)
|
||||
return false; // not in the player's spellbook
|
||||
|
||||
auto dispatch = reinterpret_cast<CastDispatch_t>(
|
||||
Offsets::FUN_SPELL_CAST_DISPATCH);
|
||||
dispatch(static_cast<unsigned>(slot), bookType,
|
||||
0, 0.0f); // no implicit target unit; engine handles placement
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_C_Spell_CastAtCursor(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
// Spell not in the player's spellbook → match `CastSpellByName`'s
|
||||
// silent failure.
|
||||
if (!DispatchSpellCast(spellID)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const bool placed = Resolve();
|
||||
Game::Lua::PushBool(L, placed);
|
||||
|
||||
@@ -30,4 +30,22 @@ namespace Spell::AtCursor {
|
||||
// click-info struct pointer.
|
||||
bool Resolve();
|
||||
|
||||
// Commits an in-flight ground-target placement at explicit world
|
||||
// coords (x, y, z) instead of the cursor raycast. Same placement
|
||||
// guards as `Resolve` (no placement active → false; non-ground
|
||||
// placement → cancel + false; not in world → false), but skips the
|
||||
// cursor entirely. Used by `C_Spell.CastAtUnit` / `C_Item.UseAtUnit`
|
||||
// to drop the reticle at a unit's feet. Returns true when the
|
||||
// placement was committed.
|
||||
bool CommitAtCoords(const float coords[3]);
|
||||
|
||||
// Resolves a numeric `spellID` to a spellbook slot and dispatches the
|
||||
// cast through the engine — the shared front half of
|
||||
// `C_Spell.CastAtCursor` / `CastAtUnit`. For a ground-target spell
|
||||
// the engine enters placement mode (commit follows via `Resolve` /
|
||||
// `CommitAtCoords`); for a normal spell it fires immediately. Returns
|
||||
// true if the cast was dispatched (spell is in the player's
|
||||
// spellbook), false otherwise.
|
||||
bool DispatchSpellCast(int spellID);
|
||||
|
||||
} // namespace Spell::AtCursor
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// 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_Spell.CastAtUnit(spellID, unit)` — cast a ground-target spell at
|
||||
// the given unit's feet, bypassing the manual click on the AoE
|
||||
// reticle. `CastAtUnit(spellID, "player")` drops it at the player's
|
||||
// position, `"target"` at the target's, etc.
|
||||
//
|
||||
// Same two-step shape as `C_Spell.CastAtCursor`, but the placement is
|
||||
// committed at the unit's world position instead of the cursor
|
||||
// raycast:
|
||||
//
|
||||
// 1. `Spell::AtCursor::DispatchSpellCast(spellID)` initiates the
|
||||
// cast; the engine enters placement mode for a ground-target
|
||||
// spell.
|
||||
// 2. Read the unit's world position (vtable `GetPosition`) and
|
||||
// `Spell::AtCursor::CommitAtCoords()` commits the placement
|
||||
// there.
|
||||
//
|
||||
// Returns `true` when the placement was committed at the unit; `false`
|
||||
// when the spell isn't ground-target (placement cancelled), the spell
|
||||
// isn't in the spellbook, or the unit has no resolvable position. The
|
||||
// cast itself still proceeds for non-ground spells, just with no
|
||||
// implicit target — matching `CastAtCursor`'s semantics.
|
||||
|
||||
#include "Game.h"
|
||||
#include "spell/AtCursor.h"
|
||||
#include "spell/MacroPrimarySpell.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
namespace Spell::AtUnit {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_C_Spell_CastAtUnit(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *token = Game::Lua::ToString(L, 2);
|
||||
|
||||
// Resolve the unit's position before dispatching — fail fast so a
|
||||
// bad/absent unit doesn't leave the engine sitting in placement
|
||||
// mode. (A genuinely unrecognized token string raises the engine's
|
||||
// standard "Unknown unit" error inside the resolver, same as
|
||||
// `UnitHealth("garbage")`.)
|
||||
float pos[3];
|
||||
if (!Unit::Position::ReadToken(token, pos)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!Spell::AtCursor::DispatchSpellCast(spellID)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const bool placed = Spell::AtCursor::CommitAtCoords(pos);
|
||||
Game::Lua::PushBool(L, placed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "CastAtUnit",
|
||||
&Script_C_Spell_CastAtUnit);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// Macro-parser pattern: tag macros calling
|
||||
// `C_Spell.CastAtUnit(<spellID>, ...)` with the spellID so action-bar
|
||||
// UIs highlight the slot. The first arg is the numeric spellID, so the
|
||||
// extractor just reads the leading numeric literal — same shape as
|
||||
// `CastAtCursor`'s.
|
||||
int ExtractCastAtUnitArg(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.CastAtUnit(", &ExtractCastAtUnitArg};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Spell::AtUnit
|
||||
+14
-6
@@ -241,7 +241,7 @@ static int __fastcall Script_CastingInfo(void *L) { return PushCastInfo(L, g_cas
|
||||
// in vanilla; any other unit returns nil.
|
||||
static int __fastcall Script_UnitCastingInfo(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitCastingInfo(\"unit\")");
|
||||
Game::Lua::Error(L, "Usage: C_Spell.UnitCastingInfo(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
if (ArgIsPlayer(L))
|
||||
@@ -258,7 +258,7 @@ static int __fastcall Script_ChannelInfo(void *L) {
|
||||
// texture (no times) for other units via the broadcast +0x228 field.
|
||||
static int __fastcall Script_UnitChannelInfo(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitChannelInfo(\"unit\")");
|
||||
Game::Lua::Error(L, "Usage: C_Spell.UnitChannelInfo(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
@@ -277,10 +277,18 @@ static int __fastcall Script_UnitChannelInfo(void *L) {
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitCastingInfo", &Script_UnitCastingInfo);
|
||||
Game::Lua::RegisterGlobalFunction("CastingInfo", &Script_CastingInfo);
|
||||
Game::Lua::RegisterGlobalFunction("UnitChannelInfo", &Script_UnitChannelInfo);
|
||||
Game::Lua::RegisterGlobalFunction("ChannelInfo", &Script_ChannelInfo);
|
||||
// Registered under C_Spell rather than as globals to avoid clobbering
|
||||
// the global `UnitCastingInfo` / `UnitChannelInfo` names. Addons that
|
||||
// ship their own vanilla cast-tracking libraries use the
|
||||
// `_G.UnitCastingInfo or <fallback>` idiom (e.g. ShaguTweaks'
|
||||
// libcast.lua scrapes the combat log for remote/enemy casts the 1.12
|
||||
// engine never exposes). Occupying the global makes them adopt our
|
||||
// player-only version and drop their superior fallback — so we cede
|
||||
// the global names and expose the functions here instead.
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "UnitCastingInfo", &Script_UnitCastingInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "CastingInfo", &Script_CastingInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "UnitChannelInfo", &Script_UnitChannelInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "ChannelInfo", &Script_ChannelInfo);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
// Shared world-position read for any CGObject-derived unit. The
|
||||
// position comes from the object's `GetPosition` virtual (vtable
|
||||
// slot 5) — the same one `CheckInteractDistance` / `UnitInRange` use.
|
||||
// Header-only so callers (UnitInRange, CastAtUnit, UseAtUnit) don't
|
||||
// each re-derive the vtable-slot math.
|
||||
|
||||
namespace Unit::Position {
|
||||
|
||||
// Resolves a unit token ("player", "target", "party1", "mouseover",
|
||||
// …) to a `CGUnit *`. Returns nullptr for a recognized-but-absent
|
||||
// token (e.g. "party5" with no such member). NOTE: the engine's
|
||||
// resolver raises a Lua error for genuinely unrecognized token
|
||||
// strings — same contract as `UnitHealth("garbage")` — so only feed
|
||||
// it real unit tokens, or strings you're willing to let error.
|
||||
inline void *ResolveToken(const char *token) {
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *);
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return fn(token);
|
||||
}
|
||||
|
||||
// Reads `obj`'s world position into `out[3]` (x, y, z). Calls
|
||||
// `obj->vtable[5](outBuf)`; the returned float* may equal `outBuf`
|
||||
// (object filled it) or point at a cached position field — either
|
||||
// way the values land in `out`. Returns false if `obj` is null or
|
||||
// the virtual yields no position (object has no known position yet).
|
||||
inline bool Read(void *obj, float out[3]) {
|
||||
if (obj == nullptr)
|
||||
return false;
|
||||
auto **vtable = *reinterpret_cast<void ***>(obj);
|
||||
using GetPosition_t = float *(__thiscall *)(void *self, float outBuf[3]);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
vtable[Offsets::OFF_CGOBJECT_VTBL_GET_POSITION / 4]);
|
||||
float *p = fn(obj, out);
|
||||
if (p == nullptr)
|
||||
return false;
|
||||
if (p != out) {
|
||||
out[0] = p[0];
|
||||
out[1] = p[1];
|
||||
out[2] = p[2];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convenience: resolve a token and read its position in one step.
|
||||
inline bool ReadToken(const char *token, float out[3]) {
|
||||
return Read(ResolveToken(token), out);
|
||||
}
|
||||
|
||||
} // namespace Unit::Position
|
||||
+5
-39
@@ -15,42 +15,17 @@
|
||||
// uses, applied with a fixed 40-yard threshold.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Unit::Range {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using GetPosition_t = float *(__thiscall *)(void *self, float outBuf[3]);
|
||||
|
||||
constexpr float kRangeYards = 40.0f;
|
||||
constexpr float kRangeYardsSq = kRangeYards * kRangeYards;
|
||||
|
||||
void *ResolveUnit(const char *token) {
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return fn(token);
|
||||
}
|
||||
|
||||
// Calls `obj->vtable[5](outBuf)`. The returned float* may equal
|
||||
// outBuf (the object filled it directly) or point at a cached
|
||||
// position field on the object — either way, deref it for x/y/z.
|
||||
// Returns nullptr if the object's vtable virtual returns null (no
|
||||
// known position for this object yet).
|
||||
float *ReadPosition(void *obj, float outBuf[3]) {
|
||||
if (obj == nullptr)
|
||||
return nullptr;
|
||||
auto **vtable = *reinterpret_cast<void ***>(obj);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
vtable[Offsets::OFF_CGOBJECT_VTBL_GET_POSITION / 4]);
|
||||
return fn(obj, outBuf);
|
||||
}
|
||||
|
||||
int __fastcall Script_UnitInRange(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitInRange(\"unit\")");
|
||||
@@ -69,19 +44,10 @@ int __fastcall Script_UnitInRange(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
void *unit = ResolveUnit(token);
|
||||
void *player = ResolveUnit("player");
|
||||
if (unit == nullptr || player == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
}
|
||||
|
||||
float unitBuf[3] = {};
|
||||
float playerBuf[3] = {};
|
||||
const float *unitPos = ReadPosition(unit, unitBuf);
|
||||
const float *playerPos = ReadPosition(player, playerBuf);
|
||||
if (unitPos == nullptr || playerPos == nullptr) {
|
||||
float unitPos[3] = {};
|
||||
float playerPos[3] = {};
|
||||
if (!Unit::Position::ReadToken(token, unitPos) ||
|
||||
!Unit::Position::ReadToken("player", playerPos)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
|
||||
Reference in New Issue
Block a user