mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 20:37:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd33288c9 | ||
|
|
bf7e476c10 | ||
|
|
8f3ed96d51 | ||
|
|
e1ffaab6b4 | ||
|
|
c13b1c8eb7 | ||
|
|
bedc5d6740 | ||
|
|
bdef428ff7 | ||
|
|
25b1a1a95c | ||
|
|
5e23332880 | ||
|
|
eb36b3e09e | ||
|
|
16452519a6 | ||
|
|
33c5f3041d | ||
|
|
e8a03d584b | ||
|
|
adba73a6d8 | ||
|
|
d09137cae0 | ||
|
|
5721fb65a7 | ||
|
|
3afd403eae | ||
|
|
e1030213d3 | ||
|
|
c1cf437bf8 | ||
|
|
07a91feff0 | ||
|
|
88ea4356a0 | ||
|
|
50909ce440 | ||
|
|
d48d003828 | ||
|
|
d51f5f72a9 | ||
|
|
04efd4caf1 | ||
|
|
329d6b75ba | ||
|
|
6a677ce447 | ||
|
|
f6f88ab4ee | ||
|
|
17c928028e | ||
|
|
a47f38417f | ||
|
|
1282e3f6db | ||
|
|
4e4fad9196 | ||
|
|
67063876da | ||
|
|
b34368ef14 | ||
|
|
4983c9fd83 | ||
|
|
9e5114141e | ||
|
|
6be25d78e0 | ||
|
|
06e68d04a8 |
@@ -12,7 +12,6 @@ Util\Print.lua
|
||||
Util\Mixin.lua
|
||||
Util\Color.lua
|
||||
Util\Constants.lua
|
||||
Util\String.lua
|
||||
Util\TableUtil.lua
|
||||
Util\MathUtil.lua
|
||||
Util\PixelUtil.lua
|
||||
@@ -23,6 +22,7 @@ Util\CallbackRegistry.lua
|
||||
Util\GlobalCallbackRegistry.lua
|
||||
Util\ColorUtil.lua
|
||||
Util\ItemLocation.lua
|
||||
Util\PlayerLocation.lua
|
||||
Util\EventUtil.lua
|
||||
Util\ItemUtil.lua
|
||||
Util\AddOnCompat.lua
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
-- - 1.12 has no concept of an item GUID and no public item-lock API,
|
||||
-- so those return nil / no-op.
|
||||
|
||||
C_Item.GetItemInfo = GetItemInfo
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Item = {}
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
-- Backport of the modern `PlayerLocation` / `PlayerLocationMixin` parameter
|
||||
-- object (Blizzard_ObjectAPI). Like `ItemLocation`, it's a lightweight
|
||||
-- descriptor that wraps one of several ways to identify a player — a GUID, a
|
||||
-- unit token, a chat line, a /who index, a battlefield-scoreboard index, etc.
|
||||
-- — so retail/Classic addon code can construct and inspect it unchanged.
|
||||
--
|
||||
-- It's pure table bookkeeping: every Create/Set/Get/Is is just fields, no
|
||||
-- engine access. The GUID / unit / chatLine / whoIndex / battlefieldScore
|
||||
-- kinds all map to things vanilla 1.12 genuinely has. The community, voice,
|
||||
-- and Battle.net kinds have no vanilla producer, but they're kept as harmless
|
||||
-- data holders so the mixin matches retail exactly for any code that builds
|
||||
-- or reads them. Only the two methods that reach into systems 1.12 lacks —
|
||||
-- `IsValid` (Club) and `IsBattleNetGUID` (account info) — are guarded so they
|
||||
-- degrade gracefully instead of erroring on the missing `C_Club` /
|
||||
-- `C_AccountInfo` namespaces.
|
||||
|
||||
local CreateFromMixins = CreateFromMixins
|
||||
|
||||
PlayerLocation = PlayerLocation or {}
|
||||
PlayerLocationMixin = PlayerLocationMixin or {}
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromGUID(guid)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetGUID(guid)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromUnit(unit)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetUnit(unit)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromChatLineID(lineID)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetChatLineID(lineID)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromBattlefieldScoreIndex(battlefieldScoreIndex)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetBattlefieldScoreIndex(battlefieldScoreIndex)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromWhoIndex(whoIndex)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetWhoIndex(whoIndex)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromVoiceID(memberID, channelID)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetVoiceID(memberID, channelID)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromBattleNetID(battleNetID)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetBattleNetID(battleNetID)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromCommunityChatData(clubID, streamID, epoch, position)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetCommunityData(clubID, streamID, epoch, position)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromCommunityInvitation(clubID, guid)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetCommunityInvitation(clubID, guid)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[public api]]
|
||||
function PlayerLocationMixin:SetGUID(guid)
|
||||
self:ClearAndSetField("guid", guid)
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsValid()
|
||||
if self:IsCommunityData() then
|
||||
-- Vanilla has no Community/Club system; if the namespace is
|
||||
-- absent a community location can never be resolved.
|
||||
if C_Club and C_Club.CanResolvePlayerLocationFromClubMessageData then
|
||||
return C_Club.CanResolvePlayerLocationFromClubMessageData(self.communityClubID, self.communityStreamID, self.communityEpoch, self.communityPosition)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsGUID()
|
||||
return self.guid ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsBattleNetGUID()
|
||||
-- No Battle.net accounts on 1.12; guarded so it returns false instead
|
||||
-- of erroring on a missing C_AccountInfo.
|
||||
if self.guid and C_AccountInfo and C_AccountInfo.IsGUIDBattleNetAccountType then
|
||||
return C_AccountInfo.IsGUIDBattleNetAccountType(self.guid)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetGUID()
|
||||
return self.guid or self.communityClubInviterGUID
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetUnit(unit)
|
||||
self:ClearAndSetField("unit", unit)
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsUnit()
|
||||
return self.unit ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetUnit()
|
||||
return self.unit
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetChatLineID(lineID)
|
||||
self:ClearAndSetField("chatLineID", lineID)
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsChatLineID()
|
||||
return self.chatLineID ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetChatLineID()
|
||||
return self.chatLineID
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetBattlefieldScoreIndex(index)
|
||||
self:ClearAndSetField("battlefieldScoreIndex", index)
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsBattlefieldScoreIndex()
|
||||
return self.battlefieldScoreIndex ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetBattlefieldScoreIndex()
|
||||
return self.battlefieldScoreIndex
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetWhoIndex(whoIndex)
|
||||
self:ClearAndSetField("whoIndex", whoIndex)
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsWhoIndex()
|
||||
return self.whoIndex ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetWhoIndex()
|
||||
return self.whoIndex
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetVoiceID(memberID, channelID)
|
||||
self:Clear()
|
||||
self.voiceMemberID = memberID
|
||||
self.voiceChannelID = channelID
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsVoiceID()
|
||||
return self.voiceMemberID ~= nil and self.voiceChannelID ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetVoiceID()
|
||||
return self.voiceMemberID, self.voiceChannelID
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetBattleNetID(battleNetID)
|
||||
self:Clear()
|
||||
self.battleNetID = battleNetID
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsBattleNetID()
|
||||
return self.battleNetID ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetBattleNetID()
|
||||
return self.battleNetID
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetCommunityData(clubID, streamID, epoch, position)
|
||||
self:Clear()
|
||||
self.communityClubID = clubID
|
||||
self.communityStreamID = streamID
|
||||
self.communityEpoch = epoch
|
||||
self.communityPosition = position
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsCommunityData()
|
||||
return self.communityClubID ~= nil and self.communityStreamID ~= nil and self.communityEpoch ~= nil and self.communityPosition ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetCommunityInvitation(clubID, guid)
|
||||
self:Clear()
|
||||
self.communityClubID = clubID
|
||||
self.communityClubInviterGUID = guid
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsCommunityInvitation()
|
||||
return self.communityClubID ~= nil and self.communityClubInviterGUID ~= nil
|
||||
end
|
||||
|
||||
--[[private api]]
|
||||
function PlayerLocationMixin:Clear()
|
||||
self.guid = nil
|
||||
self.unit = nil
|
||||
self.chatLineID = nil
|
||||
self.battlefieldScoreIndex = nil
|
||||
self.whoIndex = nil
|
||||
self.voiceMemberID = nil
|
||||
self.voiceChannelID = nil
|
||||
self.communityClubID = nil
|
||||
self.communityStreamID = nil
|
||||
self.communityEpoch = nil
|
||||
self.communityPosition = nil
|
||||
self.communityClubInviterGUID = nil
|
||||
self.battleNetID = nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:ClearAndSetField(fieldName, field)
|
||||
self:Clear()
|
||||
self[fieldName] = field
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
function string.match(s, pattern, init)
|
||||
if s == nil then return nil end
|
||||
local r1, r2, c1, c2, c3, c4, c5, c6, c7, c8, c9 =
|
||||
string.find(s, pattern, init)
|
||||
if r1 == nil then return nil end
|
||||
if c1 ~= nil then
|
||||
return c1, c2, c3, c4, c5, c6, c7, c8, c9
|
||||
end
|
||||
return string.sub(s, r1, r2)
|
||||
end
|
||||
|
||||
string.gmatch = string.gfind
|
||||
+21
-5
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(ClassicAPI LANGUAGES CXX)
|
||||
project(ClassicAPI LANGUAGES CXX RC)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -77,17 +77,20 @@ add_library(${PROJECT_NAME} SHARED
|
||||
# `CLASSIC_API_VERSION >= X` always pass during local development;
|
||||
# CI release builds overwrite it via `-DCLASSICAPI_TAG=vX.Y.Z`.
|
||||
set(CLASSICAPI_VERSION_VALUE 99999999)
|
||||
set(CLASSICAPI_VER_MAJOR 9999)
|
||||
set(CLASSICAPI_VER_MINOR 99)
|
||||
set(CLASSICAPI_VER_PATCH 99)
|
||||
|
||||
set(CLASSICAPI_TAG
|
||||
""
|
||||
CACHE STRING "Release tag like v1.2.3")
|
||||
|
||||
if(CLASSICAPI_TAG MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)$")
|
||||
set(_MAJOR "${CMAKE_MATCH_1}")
|
||||
set(_MINOR "${CMAKE_MATCH_2}")
|
||||
set(_PATCH "${CMAKE_MATCH_3}")
|
||||
set(CLASSICAPI_VER_MAJOR "${CMAKE_MATCH_1}")
|
||||
set(CLASSICAPI_VER_MINOR "${CMAKE_MATCH_2}")
|
||||
set(CLASSICAPI_VER_PATCH "${CMAKE_MATCH_3}")
|
||||
math(EXPR CLASSICAPI_VERSION_VALUE
|
||||
"${_MAJOR} * 10000 + ${_MINOR} * 100 + ${_PATCH}")
|
||||
"${CLASSICAPI_VER_MAJOR} * 10000 + ${CLASSICAPI_VER_MINOR} * 100 + ${CLASSICAPI_VER_PATCH}")
|
||||
elseif(CLASSICAPI_TAG)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
@@ -99,6 +102,19 @@ target_compile_definitions(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE CLASSICAPI_VERSION_VALUE=${CLASSICAPI_VERSION_VALUE})
|
||||
|
||||
# Stamp the PE VERSIONINFO resource from the same version components, so the
|
||||
# DLL's file/product version (Explorer -> Properties -> Details) matches the
|
||||
# CLASSIC_API_VERSION Lua global. The .rsrc section the resource lands in is
|
||||
# untouched by the .rdata/.pdata section merges below.
|
||||
set(CLASSICAPI_VER_STRING
|
||||
"${CLASSICAPI_VER_MAJOR}.${CLASSICAPI_VER_MINOR}.${CLASSICAPI_VER_PATCH}.0")
|
||||
set(CLASSICAPI_VERSION_RC "${CMAKE_BINARY_DIR}/generated/version.rc")
|
||||
configure_file(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.rc.in"
|
||||
"${CLASSICAPI_VERSION_RC}"
|
||||
@ONLY)
|
||||
target_sources(${PROJECT_NAME} PRIVATE "${CLASSICAPI_VERSION_RC}")
|
||||
|
||||
# ole32: COM (CoInitializeEx / CoCreateInstance / CoTaskMemFree) for the
|
||||
# SAPI text-to-speech backend in src/voicechat/Tts.cpp. The SAPI GUIDs are
|
||||
# instantiated in that TU via <initguid.h>, so no sapi.lib is needed.
|
||||
|
||||
@@ -19,14 +19,14 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
|-----------|-------|
|
||||
| [Action](docs/API.md#action) | `GetActionInfo` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded` |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
|
||||
| [Combat](docs/API.md#combat) | `InCombatLockdown` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
|
||||
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [Coroutine](docs/API.md#coroutine) | `coroutine.create`, `coroutine.resume`, `coroutine.yield`, `coroutine.status`, `coroutine.wrap` |
|
||||
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID` |
|
||||
| [Container](docs/API.md#container) | `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
|
||||
| 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` |
|
||||
@@ -41,28 +41,31 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `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.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `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.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `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` |
|
||||
| [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.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `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.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.PickupItem`, `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` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
|
||||
| [Lua](docs/API.md#lua) | `coroutine.create`, `coroutine.resume`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `math.fmod`, `select`, `string.gmatch`, `string.match`, `table.wipe` |
|
||||
| [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`, `C_Map.GetMapOverlays` |
|
||||
| [Map](docs/API.md#map) | `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapOverlays`, `C_Map.GetMapWorldSize` |
|
||||
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
|
||||
| [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_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
|
||||
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GetClass`, `C_PlayerInfo.GetName`, `C_PlayerInfo.GetRace`, `C_PlayerInfo.GetSex`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `C_PlayerInfo.IsConnected` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [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.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `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.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `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` |
|
||||
| [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.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `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.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `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` |
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitDistanceSquared`, `UnitGUID`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitCreatureFamilyID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterComboDuration` |
|
||||
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
|
||||
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
|
||||
@@ -114,6 +117,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
|
||||
| Event | Payload |
|
||||
|-------|---------|
|
||||
| `BAG_NEW_ITEMS_UPDATED` | *(none)* |
|
||||
| `BAG_UPDATE_DELAYED` | *(none)* |
|
||||
| `EQUIPMENT_SETS_CHANGED` | *(none)* |
|
||||
| `EQUIPMENT_SWAP_PENDING` | `setID` |
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Generated from cmake/version.rc.in by CMake configure_file().
|
||||
// Stamps the PE VERSIONINFO resource so Explorer -> Properties -> Details
|
||||
// and GetFileVersionInfo() report the same version as the CLASSIC_API_VERSION
|
||||
// Lua global. Both derive from the CLASSICAPI_TAG (vMAJOR.MINOR.PATCH) cache
|
||||
// var; an untagged dev build gets the 9999.99.99 "newer than everything"
|
||||
// sentinel.
|
||||
|
||||
#include <winver.h>
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION @CLASSICAPI_VER_MAJOR@,@CLASSICAPI_VER_MINOR@,@CLASSICAPI_VER_PATCH@,0
|
||||
PRODUCTVERSION @CLASSICAPI_VER_MAJOR@,@CLASSICAPI_VER_MINOR@,@CLASSICAPI_VER_PATCH@,0
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_DLL
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "ClassicAPI Team"
|
||||
VALUE "FileDescription", "ClassicAPI - modern WoW API backport for 1.12.1"
|
||||
VALUE "FileVersion", "@CLASSICAPI_VER_STRING@"
|
||||
VALUE "InternalName", "ClassicAPI"
|
||||
VALUE "LegalCopyright", "Licensed under GPLv3"
|
||||
VALUE "OriginalFilename", "ClassicAPI.dll"
|
||||
VALUE "ProductName", "ClassicAPI"
|
||||
VALUE "ProductVersion", "@CLASSICAPI_VER_STRING@"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
// 0x0409 = U.S. English, 1200 = Unicode codepage.
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
||||
+1297
-174
File diff suppressed because it is too large
Load Diff
+290
-7
@@ -530,14 +530,23 @@ enum Offsets {
|
||||
OFF_UNIT_FIELD_CREATEDBY = 0x20,
|
||||
|
||||
// Pet-vs-minion discriminator for an owned unit: `int __fastcall(unit)`.
|
||||
// Returns 1 for a controllable **pet** (classified by creature family
|
||||
// at descriptor +0x212 → family DBC, with a ChrRaces fallback), a
|
||||
// non-1 value otherwise. The engine's unit-title builder `FUN_0052FD30`
|
||||
// uses it as `(fn(unit) != 1) + 1` → 1 = "X's Pet" title, 2 = "X's
|
||||
// Minion" — so it's the exact signal separating pet from minion, which
|
||||
// neither the GUID (pets and guardians share the 0xF14… prefix) nor
|
||||
// UNIT_FLAG_PLAYER_CONTROLLED reliably does.
|
||||
// `0x00605570` is really the engine's **creature-type resolver** — it's
|
||||
// `Script_UnitCreatureType`'s (`0x0051A280`) inner helper and returns the
|
||||
// `CreatureType.dbc` id (1=Beast, 3=Demon, 7=Humanoid, …), via three
|
||||
// paths in order: a display-override at descriptor `+0x212`, then the
|
||||
// creature-cache type at `[unit+0xB30]+0x18`, then the player-race type
|
||||
// at `ChrRaces[[unit+0x110]+0x78]+0x24`. `FUN_UNIT_CREATURE_TYPE` is the
|
||||
// canonical name; `FUN_UNIT_PET_MINION_CLASS` is a historical alias for
|
||||
// the same address.
|
||||
//
|
||||
// The pet/minion use: the unit-title builder `FUN_0052FD30` calls it as
|
||||
// `(fn(unit) != 1) + 1` → 1 = "X's Pet" title, 2 = "X's Minion". That
|
||||
// works because it's really testing "is this a Beast (type 1)": hunter
|
||||
// pets are Beasts, warlock minions are Demons (type 3) — so `!= 1`
|
||||
// cleanly separates them, which neither the GUID (pets and guardians
|
||||
// share the 0xF14… prefix) nor UNIT_FLAG_PLAYER_CONTROLLED reliably does.
|
||||
FUN_UNIT_PET_MINION_CLASS = 0x00605570,
|
||||
FUN_UNIT_CREATURE_TYPE = 0x00605570,
|
||||
|
||||
// Party / raid roster counts and the party GUID array referenced
|
||||
// in the `FUN_TOKEN_TO_GUID` dispatch comment above. Used by
|
||||
@@ -706,11 +715,19 @@ enum Offsets {
|
||||
// +0x00 ?
|
||||
// +0x0C name C-string ptr (alias of UNIT_NAME for creatures)
|
||||
// +0x10 subName C-string ptr ("Innkeeper", "<Master Trainer>", etc.)
|
||||
// +0x1C CreatureFamily id (→ CreatureFamily.dbc), 0 = none
|
||||
//
|
||||
// Used by `Script_UnitName`'s creature path and by VanillaMinimapTracking's
|
||||
// subname-based blip filter (`src/Blips.cpp::ExtractUnitSubName`).
|
||||
OFF_UNIT_CREATURE_CACHE_ROW = 0xB30,
|
||||
OFF_CREATURE_CACHE_SUB_NAME = 0x10,
|
||||
// CreatureFamily id within the +0xB30 cache row — the value the engine's
|
||||
// `Script_UnitCreatureFamily` (`0x0051A310`) reads via its family helper
|
||||
// `FUN_006055e0` (`[unit+0xB30] ? [row+0x1C] : 0`) before mapping it
|
||||
// through `CreatureFamily.dbc` for the localized name. `UnitCreatureFamilyID`
|
||||
// returns the raw id. Same field as `OFF_CREATURE_FAMILY` in the creature
|
||||
// cache data block, reached through the unit here.
|
||||
OFF_CREATURE_CACHE_FAMILY = 0x1C,
|
||||
// Pointer to the CGUnit's 8-byte GUID at `*(CGUnit + 0x08)`. Verified
|
||||
// in `Script_GetInventoryItemLink` at `0x004C8CB0`-`0x004C8CB5`:
|
||||
// `mov eax, [esi+8]; mov edi, [eax]; mov ecx, [eax+4]` reads the
|
||||
@@ -781,6 +798,19 @@ enum Offsets {
|
||||
// mov edx, [eax + 0x110]
|
||||
// movzx eax, byte ptr [edx + 0x7B]
|
||||
OFF_UNIT_DESCRIPTOR_POWER_TYPE_BYTE = 0x7B,
|
||||
// UNIT_FIELD_STAT0..4 — the fully-computed (base+item+buff) primary
|
||||
// stats, broadcast in the descriptor. Order Str/Agi/Sta/Int/Spirit;
|
||||
// STAT4 (Spirit) = fieldIndex 0x94 → +0x250 (from the UpdateField name
|
||||
// table; cross-checked in-game: descriptor +0x250 read 173→182 when a
|
||||
// +9-Spirit item was equipped). What GetStat(STAT_SPIRIT) returns
|
||||
// server-side; used by GetSpellBonusHealing for the Spiritual-Guidance
|
||||
// (aura 175) spirit→healing conversion.
|
||||
OFF_UNIT_FIELD_STAT_SPIRIT = 0x250,
|
||||
// UNIT_FIELD_RESISTANCES[0] = armor (the fully-computed total), fieldIndex
|
||||
// 0x95 → +0x254 (7 resistances: armor, then the 6 magic schools). What
|
||||
// GetArmor() / GetResistance(SPELL_SCHOOL_NORMAL) returns; used by
|
||||
// GetSpellBonusHealing for Turtle's Ironclad (aura 199) armor→healing.
|
||||
OFF_UNIT_FIELD_RESISTANCE_ARMOR = 0x254,
|
||||
OFF_UNIT_FIELD_FLAGS = 0xA0,
|
||||
UNIT_FLAG_PLAYER_CONTROLLED = 0x08,
|
||||
// Bit 19 of UNIT_FIELD_FLAGS — `Script_UnitAffectingCombat` at
|
||||
@@ -851,6 +881,27 @@ enum Offsets {
|
||||
UNIT_AURA_TOTAL = 48,
|
||||
UNIT_AURA_VISIBLE_MASK = 0x0E, // nibble mask used by the engine's visibility gate
|
||||
|
||||
// PLAYER_FIELD_MOD_DAMAGE_DONE_POS/NEG — the player's per-school spell
|
||||
// damage bonus. Offsets from the engine's own UpdateField name/index
|
||||
// table in `.rdata` (20-byte entries `{char* name, u32 fieldIndex, u32
|
||||
// count, ...}` at VA 0x83B6C8): POS = fieldIndex 0x3F5 count 7 → byte
|
||||
// offset fieldIndex*4 = 0xFD4; NEG = 0x3FC → 0xFF0. Seven int32 each,
|
||||
// one per school (0 physical .. 6 arcane); net bonus = POS − NEG.
|
||||
//
|
||||
// **Read these off `[player + OFF_CGPLAYER_INFO]` (the +0xE68 CGPlayer
|
||||
// sub-struct), NOT the broadcast descriptor at +0x110.** The descriptor
|
||||
// copy is never sent to the client and reads zero (verified in-game: the
|
||||
// whole post-skills PLAYER descriptor region is zero on a geared L60 —
|
||||
// vanilla never displayed spell power, so the server doesn't broadcast
|
||||
// it). The +0xE68 struct holds the client's fully-computed value
|
||||
// (gear + enchants + buffs + talents + sets baked in). Same base +
|
||||
// offsets nampower's GetSpellPower uses; verified 613 generic / 647 Fire
|
||||
// on a geared mage. Read by GetSpellBonusDamage in [src/spell/BonusDamage.cpp].
|
||||
// (No MOD_HEALING_DONE equivalent — vanilla lacks the field entirely;
|
||||
// GetSpellBonusHealing is derived instead.)
|
||||
OFF_PLAYER_FIELD_MOD_DAMAGE_DONE_POS = 0xFD4,
|
||||
OFF_PLAYER_FIELD_MOD_DAMAGE_DONE_NEG = 0xFF0,
|
||||
|
||||
// Reusable "is this spell record a user-visible aura" predicate.
|
||||
// `__fastcall(spellRecord*) -> bool`. Checks Spell.dbc Attributes
|
||||
// (high bit of byte at +0x18 must be clear), AttributesEx bit
|
||||
@@ -1207,8 +1258,33 @@ enum Offsets {
|
||||
// locLeft/locRight/locTop/locBottom floats}
|
||||
VAR_WORLDMAP_AREA_RECORDS = 0x00C0D5BC,
|
||||
VAR_WORLDMAP_AREA_COUNT = 0x00C0D5C0,
|
||||
OFF_WMA_MAP_ID = 0x04,
|
||||
OFF_WMA_AREA_ID = 0x08,
|
||||
OFF_WMA_NAME = 0x0C,
|
||||
OFF_WMA_LOC_LEFT = 0x10, // world Y at the map's left edge (max Y)
|
||||
OFF_WMA_LOC_RIGHT = 0x14, // world Y at the map's right edge (min Y)
|
||||
OFF_WMA_LOC_TOP = 0x18, // world X at the map's top edge (max X)
|
||||
OFF_WMA_LOC_BOTTOM = 0x1C, // world X at the map's bottom edge (min X)
|
||||
|
||||
// AreaTrigger.dbc record (10 fields, 0x28 bytes) — instance cataloged
|
||||
// in docs/DBCs.md (records slot 0x00C0E034, count 0x00C0E038). The
|
||||
// client loads it but exposes NO geometry to Lua, which is why map
|
||||
// addons (pfQuest et al.) ship hand-scraped trigger tables. Schema:
|
||||
// {ID@+0, mapID@+4 (Map.dbc — continent or instance),
|
||||
// x@+8, y@+0xC, z@+0x10 (continent-space world coords),
|
||||
// radius@+0x14 (sphere trigger; 0 => box trigger),
|
||||
// boxLength@+0x18, boxWidth@+0x1C, boxHeight@+0x20, boxYaw@+0x24}
|
||||
VAR_AREATRIGGER_RECORDS = 0x00C0E034,
|
||||
VAR_AREATRIGGER_COUNT = 0x00C0E038,
|
||||
OFF_AT_MAP_ID = 0x04,
|
||||
OFF_AT_X = 0x08,
|
||||
OFF_AT_Y = 0x0C,
|
||||
OFF_AT_Z = 0x10,
|
||||
OFF_AT_RADIUS = 0x14,
|
||||
OFF_AT_BOX_LENGTH = 0x18,
|
||||
OFF_AT_BOX_WIDTH = 0x1C,
|
||||
OFF_AT_BOX_HEIGHT = 0x20,
|
||||
OFF_AT_BOX_YAW = 0x24,
|
||||
|
||||
// WorldMapOverlay.dbc record (17 fields, 0x44 bytes):
|
||||
// {ID@+0, worldMapAreaID@+4 (-> WorldMapArea row),
|
||||
@@ -1223,6 +1299,7 @@ enum Offsets {
|
||||
VAR_WORLDMAP_OVERLAY_RECORDS = 0x00C0D594,
|
||||
VAR_WORLDMAP_OVERLAY_COUNT = 0x00C0D598,
|
||||
OFF_WMO_WORLDMAP_AREA = 0x04,
|
||||
OFF_WMO_AREA_ID = 0x08, // areaID[4] @ +0x08..+0x14 (AreaTable ids revealed)
|
||||
OFF_WMO_MAP_POINT_X = 0x18,
|
||||
OFF_WMO_MAP_POINT_Y = 0x1C,
|
||||
OFF_WMO_TEXTURE_NAME = 0x20,
|
||||
@@ -1230,6 +1307,10 @@ enum Offsets {
|
||||
OFF_WMO_TEXTURE_HEIGHT = 0x28,
|
||||
OFF_WMO_OFFSET_X = 0x2C,
|
||||
OFF_WMO_OFFSET_Y = 0x30,
|
||||
OFF_WMO_HITRECT_TOP = 0x34, // hit rect in world-map canvas px (~1002x668):
|
||||
OFF_WMO_HITRECT_LEFT = 0x38, // the tight clickable bounds of the landmass,
|
||||
OFF_WMO_HITRECT_BOTTOM = 0x3C, // used for subzone-center placement (pfQuest's
|
||||
OFF_WMO_HITRECT_RIGHT = 0x40, // pfDB["zones"] was scraped from these)
|
||||
|
||||
// Current world-map view selection — the globals the engine's
|
||||
// current-map-name resolver (FUN_004a6cf0, behind GetMapInfo) reads:
|
||||
@@ -1357,6 +1438,11 @@ enum Offsets {
|
||||
// (see OFF_UNIT_FIELD_CHANNEL_SPELL below); PLAYER_FLAGS only
|
||||
// exists on the +0xE68 sub-struct here.
|
||||
OFF_CGPLAYER_INFO = 0xE68,
|
||||
// Explored-areas bitfield inside the CGPlayer info sub-struct
|
||||
// (`[player+0xE68] + 0xE6C`): bit `AreaBit` set == that AreaTable area
|
||||
// is explored. 0x100 bytes. Read by the discovered-overlay rebuild
|
||||
// FUN_004a67a0 as `bits[AreaBit>>3] & (1 << (AreaBit&7))`.
|
||||
OFF_PLAYER_EXPLORED_BITS = 0xE6C,
|
||||
|
||||
// PvP rank fields within the CGPlayer +0xE68 sub-struct (siblings of
|
||||
// the combo-point byte at +0x1029). Read by the item tooltip's
|
||||
@@ -1487,6 +1573,25 @@ enum Offsets {
|
||||
// Reads bagID at Lua stack[1] and slot at stack[2], validates them, and
|
||||
// returns the inventory manager + linear slot ready to feed into GetItemBySlot.
|
||||
FUN_PACK_BAG_SLOT = 0x004F9820,
|
||||
// Equipped-bag container-GUID getter — `uint64 __fastcall(uint bagIndex0)`
|
||||
// where `bagIndex0` is 0-based (Lua bagID 1..4 → 0..3; 4..9 are bank bags,
|
||||
// gated on the bank-open globals). Returns the CGContainer GUID of the bag
|
||||
// equipped in that slot, or 0 if none. This is the internal PackBagSlot
|
||||
// uses to resolve bags 1..4: get the bag GUID here, resolve the container
|
||||
// via FUN_OBJECT_RESOLVE_BY_GUID(OBJ_TYPE_CONTAINER, guid), then call the
|
||||
// container's vtable[+OFF_CONTAINER_GET_INVENTORY] to get the inventory
|
||||
// object GetItemBySlot indexes. Lets us enumerate bag contents in pure C++
|
||||
// without PackBagSlot's Lua-stack coupling. (Reads GUID arrays at
|
||||
// 0x00BDD060 lo / 0x00BDD064 hi, stride 8.)
|
||||
FUN_GET_EQUIPPED_BAG_GUID = 0x004F93E0,
|
||||
// CGContainer vtable slot (byte offset into the vtable) whose method
|
||||
// returns the container's inventory object — `void* __thiscall(container)`.
|
||||
// The inventory object's slot count lives at its +0x00 (OFF_INVMGR_SLOT_COUNT).
|
||||
OFF_CONTAINER_GET_INVENTORY = 0x10,
|
||||
// Backpack (bagID 0) linear-slot base in the player invMgr GUID/item
|
||||
// arrays: a 1-based backpack slot S maps to linear (S - 1) + 23. Matches
|
||||
// PackBagSlot's `*param_3 += 0x17` for the backpack branch.
|
||||
BACKPACK_LINEAR_BASE = 23,
|
||||
// Player inventory manager layout — used for direct GUID-array reads
|
||||
// that bypass `GetItemBySlot`'s bank gate at `0x006228C1`.
|
||||
// +0x00 uint32 max slot count
|
||||
@@ -1733,6 +1838,16 @@ enum Offsets {
|
||||
// last use"). Negative = single-use, destroyed when count hits 0.
|
||||
// `GetItemCount(includeUses=true)` uses abs() for the multiplier.
|
||||
OFF_DESCRIPTOR_SPELL_CHARGES_0 = 0x28,
|
||||
// ITEM_FIELD_ENCHANTMENT block (fields 16..36, per the +0x48
|
||||
// enchant-charges note above) — 7 enchant slots, 3 dwords each
|
||||
// `{enchantID, duration, charges}`. Slot 0 = permanent enchant,
|
||||
// slot 1 = temporary (weapon oils / sharpening stones). The
|
||||
// enchant ID indexes SpellItemEnchantment.dbc. Block base = field
|
||||
// 16 = +0x40 (field 15 FLAGS at +0x3C precedes it); slot stride
|
||||
// 0x0C, enchant ID at slot+0x00. Used by spell/BonusDamage.cpp to
|
||||
// fold enchant-granted spell power into GetSpellBonusDamage.
|
||||
OFF_DESCRIPTOR_ENCHANTMENT_ID = 0x40,
|
||||
DESCRIPTOR_ENCHANTMENT_SLOT_STRIDE = 0x0C,
|
||||
// ITEM_FIELD_DURABILITY (current) and ITEM_FIELD_MAXDURABILITY (max) live
|
||||
// adjacent to each other in the descriptor as plain dwords. Verified in
|
||||
// `Script_GetInventoryItemBroken` (`0x004C8590`): after resolving the
|
||||
@@ -1741,6 +1856,12 @@ enum Offsets {
|
||||
// no durability concept (consumables, materials) have both fields 0.
|
||||
OFF_DESCRIPTOR_DURABILITY = 0xA0,
|
||||
OFF_DESCRIPTOR_MAX_DURABILITY = 0xA4,
|
||||
// Per-instance "readable text" id (letters / mail-scroll items carry a
|
||||
// server-assigned text id here). Drives the `readable` return of
|
||||
// `Script_GetContainerItemInfo` (`0x004F9670`): at `0x004F97C1` it reads
|
||||
// `[descriptor+0x9C]` and, when non-zero, reports the slot as readable
|
||||
// even when the static ItemStats page-text is 0. Field index 0x27.
|
||||
OFF_DESCRIPTOR_READABLE_TEXT_ID = 0x9C,
|
||||
|
||||
// Per-item repair-cost helper used by Script_GameTooltip_SetInventoryItem
|
||||
// (3rd return value). __fastcall(ecx = CGItem *) -> int copperCost.
|
||||
@@ -2177,6 +2298,56 @@ enum Offsets {
|
||||
OFF_CHRRACES_FILENAME = 0x3C,
|
||||
OFF_CHRRACES_NAMES = 0x44,
|
||||
|
||||
// CreatureFamily.dbc — pet/beast family metadata. Standard 5-DWORD
|
||||
// class shape (instance 0x00C0DE74; records 0x00C0DE7C, count
|
||||
// 0x00C0DE80 — the same globals `Script_UnitCreatureFamily`
|
||||
// (`0x0051A310`) bounds-checks/indexes). 72-byte stride, familyID-keyed
|
||||
// and sparse (10/13/14/22/… are unused). Localized `Name[]` at +0x20
|
||||
// (the engine reads `record + 0x20 + locale*4`), `IconFile` texture-path
|
||||
// string at +0x44 (last field; empty for warlock-pet families). Backs
|
||||
// `C_CreatureInfo.GetCreatureFamilyInfo` / `GetCreatureFamilyIDs`.
|
||||
VAR_CREATUREFAMILY_RECORDS = 0x00C0DE7C,
|
||||
VAR_CREATUREFAMILY_COUNT = 0x00C0DE80,
|
||||
OFF_CREATUREFAMILY_NAMES = 0x20,
|
||||
OFF_CREATUREFAMILY_ICON = 0x44,
|
||||
|
||||
// CreatureType.dbc — pet/NPC classification (1=Beast … 11=Totem;
|
||||
// contiguous ids, 11 rows). Records 0x00C0DE2C, count 0x00C0DE30 (the
|
||||
// globals `Script_UnitCreatureType` (`0x0051A280`) bounds-checks/indexes),
|
||||
// localized `Name[]` at +0x04 (engine reads `record + 0x04 + locale*4`).
|
||||
// Backs `C_CreatureInfo.GetCreatureTypeInfo` / `GetCreatureTypeIDs`;
|
||||
// `UnitCreatureTypeID` returns the id `FUN_UNIT_CREATURE_TYPE` resolves.
|
||||
VAR_CREATURETYPE_RECORDS = 0x00C0DE2C,
|
||||
VAR_CREATURETYPE_COUNT = 0x00C0DE30,
|
||||
OFF_CREATURETYPE_NAMES = 0x04,
|
||||
|
||||
// Race → faction group, mirroring the player branch of
|
||||
// `Script_UnitFactionGroup` (`0x00516630`) — backs
|
||||
// `C_CreatureInfo.GetFactionInfo(raceID)`. Chain:
|
||||
// ChrRaces[race] + 0x08 → FactionTemplate id
|
||||
// FactionTemplate[id] + 0x0C → group mask (ourMask)
|
||||
// scan FactionGroup for the row whose bit is set (and whose
|
||||
// localized name is non-empty — that non-empty test is what
|
||||
// skips the always-set "Player" bit and lands on Alliance/Horde)
|
||||
// push { groupTag = row+0x08 (english), name = row+0x0C+locale*4 }
|
||||
// FactionTemplate is a pointer-array DBC (records[id]); FactionGroup
|
||||
// is stored CONTIGUOUSLY (records-base + i*stride, direct field reads,
|
||||
// NOT a pointer array), so iterate it by hand rather than via DBC::Record.
|
||||
// FactionTemplate globals match docs/DBCs.md; the FactionGroup
|
||||
// records-base/count globals are the ones the engine actually
|
||||
// dereferences per the disassembly (0x00C0DD5C / 0x00C0DD60), which
|
||||
// differ from DBCs.md's standard-shape tabulation for this table.
|
||||
OFF_CHRRACES_FACTION_TEMPLATE = 0x08,
|
||||
VAR_FACTIONTEMPLATE_RECORDS = 0x00C0DD3C,
|
||||
VAR_FACTIONTEMPLATE_COUNT = 0x00C0DD40,
|
||||
OFF_FACTIONTEMPLATE_GROUP_MASK = 0x0C,
|
||||
VAR_FACTIONGROUP_RECORDS = 0x00C0DD5C,
|
||||
VAR_FACTIONGROUP_COUNT = 0x00C0DD60,
|
||||
FACTIONGROUP_STRIDE = 0x30,
|
||||
OFF_FACTIONGROUP_BIT = 0x04,
|
||||
OFF_FACTIONGROUP_ENGLISH = 0x08,
|
||||
OFF_FACTIONGROUP_NAMES = 0x0C,
|
||||
|
||||
// AreaTable.dbc — used by `Script_GetCharacterInfo` to resolve
|
||||
// each character's last-known-area to a localized zone name.
|
||||
// Same shape as the other DBCs (records pointer + count globals,
|
||||
@@ -2184,6 +2355,55 @@ enum Offsets {
|
||||
VAR_AREATABLE_RECORDS = 0x00C0E048,
|
||||
VAR_AREATABLE_COUNT = 0x00C0E04C,
|
||||
OFF_AREATABLE_NAMES = 0x2C,
|
||||
// AreaBit @ +0x0C: index into the player's explored-areas bitfield
|
||||
// (OFF_PLAYER_EXPLORED_BITS). Gate @ +0x28: when < 0 the area needs no
|
||||
// exploration (always counts explored); when >= 0 the AreaBit check
|
||||
// applies. Source: the discovered-overlay rebuild FUN_004a67a0.
|
||||
OFF_AREATABLE_AREA_BIT = 0x0C,
|
||||
OFF_AREATABLE_EXPLORE_GATE = 0x28,
|
||||
|
||||
// TaxiNodes.dbc (16 fields, 0x40 bytes) — flight points. records/count
|
||||
// per docs/DBCs.md. mapID @ +0x04 is the Map.dbc continent; x/y/z floats;
|
||||
// Name[8] localized @ +0x14; the two MountCreatureID columns are
|
||||
// [Horde @ +0x38, Alliance @ +0x3C] (0 = that faction has no flight
|
||||
// master at this node — both 0 = a transport/system waypoint).
|
||||
VAR_TAXINODES_RECORDS = 0x00C0D6C0,
|
||||
VAR_TAXINODES_COUNT = 0x00C0D6C4,
|
||||
OFF_TAXINODE_MAP_ID = 0x04,
|
||||
OFF_TAXINODE_X = 0x08,
|
||||
OFF_TAXINODE_Y = 0x0C,
|
||||
OFF_TAXINODE_Z = 0x10,
|
||||
OFF_TAXINODE_NAME = 0x14,
|
||||
OFF_TAXINODE_HORDE_MOUNT = 0x38,
|
||||
OFF_TAXINODE_ALLIANCE_MOUNT = 0x3C,
|
||||
|
||||
// Live taxi session — the open flight master's reachable node list,
|
||||
// populated between TAXIMAP_OPENED/CLOSED (count 0 otherwise). Backs the
|
||||
// legacy NumTaxiNodes/TaxiNodeName/TaxiNodePosition/TaxiNodeGetType Lua
|
||||
// globals. Node slots are 0x20-stride records at VAR_TAXI_NODE_ARRAY:
|
||||
// +0x00 = pointer to the node's TaxiNodes.dbc record (id @ +0, name @
|
||||
// +0x14), +0x04 = flight-map x (0-1 float), +0x08 = y. Count at
|
||||
// VAR_TAXI_NODE_COUNT. Derived from Script_TaxiNode* (FUN_004dc380 name,
|
||||
// FUN_004dc3b0 position); the reachability type comes from
|
||||
// FUN_TAXI_NODE_TYPE.
|
||||
VAR_TAXI_NODE_COUNT = 0x00BB4AA8,
|
||||
VAR_TAXI_NODE_ARRAY = 0x00BB4F10,
|
||||
TAXI_NODE_STRIDE = 0x20,
|
||||
OFF_TAXI_SLOT_RECORD = 0x00,
|
||||
OFF_TAXI_SLOT_X = 0x04,
|
||||
OFF_TAXI_SLOT_Y = 0x08,
|
||||
// __fastcall(uint slot0Based) -> const char* ("CURRENT" / "REACHABLE" /
|
||||
// "DISTANT" / other) — the node's reachability for the open session.
|
||||
FUN_TAXI_NODE_TYPE = 0x004DC4E0,
|
||||
|
||||
// TaxiPath.dbc (4 fields, 0x10 bytes): {id, fromNode@+0x04, toNode@+0x08,
|
||||
// cost}. A real flight master is a node some flight path connects to;
|
||||
// Northshire Abbey / spurious duplicate rows / standalone markers are not
|
||||
// endpoints and so are filtered out of GetTaxiNodesForMap.
|
||||
VAR_TAXIPATH_RECORDS = 0x00C0D698,
|
||||
VAR_TAXIPATH_COUNT = 0x00C0D69C,
|
||||
OFF_TAXIPATH_FROM = 0x04,
|
||||
OFF_TAXIPATH_TO = 0x08,
|
||||
|
||||
// Engine player-info cache — populated by the
|
||||
// SMSG_NAME_QUERY_RESPONSE handler (opcode 0x51) at 0x005551A0,
|
||||
@@ -2960,6 +3180,18 @@ enum Offsets {
|
||||
// size matches Spell.dbc's row count. Pre-login the slot is NULL.
|
||||
VAR_PLAYER_SPELL_BITMAP = 0x00B710FC,
|
||||
|
||||
// Player spell-knowledge writers — the two functions that mutate the
|
||||
// bitmap above. Learn (`FUN_004b25b0`) sets the bit, rebuilds the
|
||||
// spellbook arrays, and fires SPELLS_CHANGED; unlearn (`FUN_004b2c50`)
|
||||
// clears the bit and removes the spell. Both `__fastcall`: learn is
|
||||
// (uint spellID, int notify, uint replacedSpellID), unlearn is
|
||||
// (uint spellID, int). Co-hooked (spell/Info.cpp) to bump
|
||||
// Player::StatSignal so a talent respec invalidates GetSpellBonusHealing's
|
||||
// talent-conversion cache. They fire at login (SMSG_INITIAL_SPELLS) and on
|
||||
// each learn/unlearn — never per-frame, so cool hook targets.
|
||||
FUN_LEARN_SPELL = 0x004B25B0,
|
||||
FUN_UNLEARN_SPELL = 0x004B2C50,
|
||||
|
||||
// Spell-rank-chain knowledge check — `char __thiscall(player /*ecx*/,
|
||||
// spellID)`. Walks `spellID`'s forward rank chain (the class/race-
|
||||
// resolved chain via `0x0060C7C0`) and returns nonzero if the player
|
||||
@@ -3426,6 +3658,15 @@ enum Offsets {
|
||||
// shape from SMSG_PARTY_MEMBER_STATS_FULL).
|
||||
FUN_GROUP_MEMBER_STATS_LOOKUP = 0x00496400,
|
||||
OFF_GROUP_MEMBER_AREA_ID = 0x14,
|
||||
// `+0x08` status-flags byte of the stats block (see field map above);
|
||||
// bit 0 is the online flag `Script_UnitIsConnected` tests. Sibling
|
||||
// `FUN_GROUP_MEMBER_SLOT_LOOKUP` (`0x00496420`, `__fastcall(u64 *guid)
|
||||
// → member slot*`) is the presence fallback UnitIsConnected consults
|
||||
// when no stats block exists — a non-null slot means the member is
|
||||
// present (and thus connected). Both take a pointer to the 64-bit GUID.
|
||||
OFF_GROUP_MEMBER_STATUS_FLAGS = 0x08,
|
||||
GROUP_MEMBER_STATUS_ONLINE = 0x1,
|
||||
FUN_GROUP_MEMBER_SLOT_LOOKUP = 0x00496420,
|
||||
|
||||
// Per-slot GUID tables — used to map party/raid token strings
|
||||
// ("party1", "raid17") to GUIDs without going through
|
||||
@@ -3555,6 +3796,27 @@ enum Offsets {
|
||||
// `rawset(nil)` clear leaves it stale and subsequent `table.insert`
|
||||
// appends past the wiped slots.
|
||||
LUAL_SETN = 0x6F4EA0,
|
||||
// `str_find` — the Lua 5.0 string-library `string.find` C function
|
||||
// (`int __fastcall(void *L)`), entry in the strlib luaL_reg table at
|
||||
// `0x00822dd0`. Returns `1` (nil pushed, no match), `2` (start, end),
|
||||
// or `2 + ncaptures`. `string.match` (a Lua 5.1 addition, missing here)
|
||||
// shares the exact same pattern engine — it's `find` that yields the
|
||||
// captures / whole match instead of the indices — so `BaseLib::StringLib`
|
||||
// calls this and transforms the result rather than reimplementing the
|
||||
// matcher. Verified by decoding the function at this address.
|
||||
FUN_LUA_STR_FIND = 0x007FC3B0,
|
||||
// `str_gfind` — the Lua 5.0 `string.gfind` C function (strlib entry at
|
||||
// `0x00822dd8`). 5.1 renamed it to `string.gmatch` with identical
|
||||
// behaviour (returns a stateful iterator closure over the matches), so
|
||||
// `BaseLib::StringLib` registers `string.gmatch` as a direct alias of
|
||||
// this function pointer — no wrapper.
|
||||
FUN_LUA_STR_GFIND = 0x007FCFA0,
|
||||
// `math_fmod` — the Lua 5.0 `math.mod` C function (mathlib entry at
|
||||
// `0x00822cb0`): `luaL_checknumber(1/2)` → `fmod(x, y)` → `lua_pushnumber`.
|
||||
// 5.1 renamed the Lua-facing name to `math.fmod` with the identical C
|
||||
// implementation, so `BaseLib::MathLib` registers `math.fmod` as a direct
|
||||
// alias of this function pointer — no wrapper. Verified by decode.
|
||||
FUN_LUA_MATH_FMOD = 0x007FAFD0,
|
||||
// `lua_iscfunction(L, idx)` — returns 1 if value is a function
|
||||
// closure with the `isC` byte set (`Closure.c.isC == 1`), 0
|
||||
// otherwise. Used by `coroutine.create` to reject C functions
|
||||
@@ -4717,8 +4979,27 @@ enum Offsets {
|
||||
// Auction sell slot — holds a single CGItem GUID for the item
|
||||
// currently sitting in the "sell" frame. Engine resolves it via
|
||||
// a typed object lookup; pass `OBJECT_TYPE_ITEM` and the GUID.
|
||||
// `Script_StartAuction` posts whatever GUID is here, so writing an
|
||||
// arbitrary bag item's GUID + calling it posts that item — no cursor,
|
||||
// no ClickAuctionSellItemButton. Used by `AuctionHouse::PostItem`.
|
||||
VAR_AUCTION_SELL_GUID_LO = 0x00B72608,
|
||||
VAR_AUCTION_SELL_GUID_HI = 0x00B7260C,
|
||||
// Auctioneer NPC GUID — set while an auction house window is open;
|
||||
// `Script_StartAuction` writes it as the packet target and bails if
|
||||
// it's zero (so this doubles as an "AH is open" probe).
|
||||
VAR_AUCTION_AUCTIONEER_GUID_LO = 0x00B725F8,
|
||||
VAR_AUCTION_AUCTIONEER_GUID_HI = 0x00B725FC,
|
||||
// `Script_StartAuction` (`0x004CE770`) — `int __fastcall(void *L)`;
|
||||
// reads (minBid, buyout, runTime) from L[1..3], validates the sell
|
||||
// slot + auctioneer + runTime, and sends CMSG_AUCTION_SELL_ITEM
|
||||
// (opcode 0x256 = { auctioneerGuid, sellItemGuid, bid, buyout,
|
||||
// runTime }). Reused verbatim by `AuctionHouse::PostItem` per stack.
|
||||
FUN_SCRIPT_START_AUCTION = 0x004CE770,
|
||||
// Valid `runTime` values `Script_StartAuction` accepts, in minutes:
|
||||
// {120, 480, 1440} = 2h / 8h / 24h (vanilla's three durations). The
|
||||
// modern `duration` arg (1/2/3) indexes this table.
|
||||
VAR_AUCTION_DURATION_TABLE = 0x00807028,
|
||||
AUCTION_DURATION_TABLE_COUNT = 3,
|
||||
// `__fastcall(ECX = typeCode, EDX = unused, [stack] = guidLo,
|
||||
// [stack] = guidHi, [stack] = anyInt) → CGObject *`.
|
||||
// Returns NULL when no object matches or the type doesn't match.
|
||||
@@ -4860,6 +5141,8 @@ enum Offsets {
|
||||
// sum). AttributesEx2 (+0x24) bit 0x20000000 disables spell mods.
|
||||
OFF_SPELL_RECORD_FAMILY_NAME = 0x280, // u32
|
||||
OFF_SPELL_RECORD_FAMILY_FLAGS = 0x284, // u64
|
||||
OFF_SPELL_RECORD_ATTRIBUTES = 0x18, // u32 (base Attributes)
|
||||
SPELL_ATTR_PASSIVE = 0x40, // bit 6 — always-on aura
|
||||
OFF_SPELL_RECORD_ATTRIBUTES_EX2 = 0x24, // u32
|
||||
SPELL_ATTR_EX2_NO_SPELL_MODS = 0x20000000,
|
||||
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_AuctionHouse.PostItem(itemLocation, duration, quantity, numStacks, bid,
|
||||
// buyout)` — post `numStacks` auctions of `quantity` each from a single
|
||||
// source stack, in one call. A ClassicAPI extension modelled on the retail
|
||||
// multi-sell flow; vanilla's `StartAuction` posts exactly one item object.
|
||||
//
|
||||
// Why this is a tick-driven state machine (not a synchronous loop):
|
||||
// vanilla's `CMSG_AUCTION_SELL_ITEM` (opcode 0x256) is
|
||||
// `{ auctioneerGuid, itemGuid, bid, buyout, runTime }` — NO count field
|
||||
// (WotLK added the server-side count; 1.12 has none, verified from
|
||||
// `Script_StartAuction`). So the server posts the *entire* item object a
|
||||
// GUID points at. To post a partial `quantity` out of a larger source, the
|
||||
// client must first materialize that quantity as its own server-committed
|
||||
// item object — i.e. split it into a bag slot (`CMSG_SPLIT_ITEM`) — and both
|
||||
// the split and the resulting post are server-authoritative and async (the
|
||||
// new stack's GUID doesn't exist client-side until `SMSG_UPDATE_OBJECT`
|
||||
// lands). We drive the split→post→split→post sequence off `WorldTick`,
|
||||
// keying each transition on observed bag state.
|
||||
//
|
||||
// Free-slot cost: a partial post needs at most ONE reused general-bag slot
|
||||
// (split `quantity` off → post that stack → the post empties the slot →
|
||||
// reuse it for the next split). The final stack (once the source is whittled
|
||||
// to exactly `quantity`) posts the source object directly, no split. Posting
|
||||
// a whole existing stack (`quantity` == the source count) needs zero free
|
||||
// slots.
|
||||
//
|
||||
// Posting reuses the engine's `Script_StartAuction` verbatim (write the
|
||||
// target stack's GUID into the sell-slot globals, push bid/buyout/runTime,
|
||||
// call it) so all of its validation runs per stack — damaged-item reject,
|
||||
// buyout normalization, runTime table check, auctioneer/"AH open" check.
|
||||
//
|
||||
// Events (retail multi-sell names, fired via Event::Custom):
|
||||
// AUCTION_MULTISELL_START — job accepted, posting begins
|
||||
// AUCTION_MULTISELL_UPDATE(done, total) — after each stack lands
|
||||
// AUCTION_MULTISELL_FAILURE — aborted (source changed, no slot,
|
||||
// AH closed, or a step timed out)
|
||||
//
|
||||
// Limitations (v1): source is the single stack at `itemLocation` (not
|
||||
// aggregated across bags); temp splits use general-purpose bags only, so
|
||||
// family-restricted items (arrows/shards in specialty bags) aren't
|
||||
// supported; one job at a time.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Swap.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace AuctionHouse::PostItem {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kEvtStart = "AUCTION_MULTISELL_START";
|
||||
constexpr const char *kEvtUpdate = "AUCTION_MULTISELL_UPDATE";
|
||||
constexpr const char *kEvtFailure = "AUCTION_MULTISELL_FAILURE";
|
||||
const Event::Custom::AutoReserve _r1{kEvtStart};
|
||||
const Event::Custom::AutoReserve _r2{kEvtUpdate};
|
||||
const Event::Custom::AutoReserve _r3{kEvtFailure};
|
||||
|
||||
// Generous per-step abort bound. A split or a post is one server round-trip
|
||||
// (~tens of frames); 600 frames (~10s at 60fps) means "something's wrong"
|
||||
// (disconnect, server refusal we can't see) rather than normal latency.
|
||||
constexpr int kMaxWaitTicks = 600;
|
||||
|
||||
enum class Phase { Idle, SplitWait, PostWait };
|
||||
|
||||
struct Job {
|
||||
bool active;
|
||||
int srcBag, srcSlot; // source stack (shrinks in place as we split off it)
|
||||
int itemID; // guards against the source slot changing under us
|
||||
int quantity;
|
||||
int totalStacks;
|
||||
int postedStacks;
|
||||
int bid, buyout, runTime;
|
||||
Phase phase;
|
||||
int workBag, workSlot; // slot currently being split-into / posted-from
|
||||
uint64_t postGuid; // GUID we posted — detect it leaving bags
|
||||
int waitTicks;
|
||||
};
|
||||
Job g_job{};
|
||||
|
||||
// --- small CGItem / cache readers ---------------------------------------
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
int CGItemCount(const uint8_t *item) {
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const int *>(desc + Offsets::OFF_DESCRIPTOR_STACK_COUNT);
|
||||
}
|
||||
|
||||
uint64_t CGItemGuid(const uint8_t *item) {
|
||||
auto *inst = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
if (inst == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(inst + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
}
|
||||
|
||||
struct SlotInfo {
|
||||
const uint8_t *item; // null = empty slot
|
||||
int itemID;
|
||||
int count;
|
||||
uint64_t guid;
|
||||
};
|
||||
|
||||
SlotInfo ReadSlot(void *L, int bag, int slot) {
|
||||
SlotInfo r{nullptr, 0, 0, 0};
|
||||
const uint8_t *item = Item::Location::ResolveBag(L, bag, slot);
|
||||
if (item == nullptr)
|
||||
return r;
|
||||
r.item = item;
|
||||
r.itemID = Item::ID::FromCGItem(item);
|
||||
r.count = CGItemCount(item);
|
||||
r.guid = CGItemGuid(item);
|
||||
return r;
|
||||
}
|
||||
|
||||
// --- auction-house globals ----------------------------------------------
|
||||
|
||||
uint64_t AuctioneerGuid() {
|
||||
const uint32_t lo = *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_AUCTION_AUCTIONEER_GUID_LO);
|
||||
const uint32_t hi = *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_AUCTION_AUCTIONEER_GUID_HI);
|
||||
return (static_cast<uint64_t>(hi) << 32) | lo;
|
||||
}
|
||||
bool AHOpen() { return AuctioneerGuid() != 0; }
|
||||
|
||||
void SetSellGuid(uint64_t guid) {
|
||||
*reinterpret_cast<uint32_t *>(Offsets::VAR_AUCTION_SELL_GUID_LO) =
|
||||
static_cast<uint32_t>(guid);
|
||||
*reinterpret_cast<uint32_t *>(Offsets::VAR_AUCTION_SELL_GUID_HI) =
|
||||
static_cast<uint32_t>(guid >> 32);
|
||||
}
|
||||
|
||||
// Point the sell slot at `guid` and fire the engine's own StartAuction for
|
||||
// this bid/buyout/runTime. StartAuction reads its args from L[1..3]; we own
|
||||
// the stack at frame-tail, so set top to 0, push the three, call, reset.
|
||||
void PostStack(void *L, uint64_t guid) {
|
||||
SetSellGuid(guid);
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_job.bid));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_job.buyout));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_job.runTime));
|
||||
auto fn = reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_START_AUCTION);
|
||||
fn(L);
|
||||
Game::Lua::SetTop(L, 0);
|
||||
}
|
||||
|
||||
// --- free-slot discovery (general-purpose bags only) --------------------
|
||||
|
||||
bool BagIsGeneral(int bag) {
|
||||
if (bag == 0)
|
||||
return true; // backpack is always general
|
||||
if (bag < 1 || bag > 4)
|
||||
return false;
|
||||
const uint8_t *bagItem =
|
||||
Item::Location::ResolveEquipmentSlot(Offsets::INVSLOT_BAG1 + bag - 1);
|
||||
if (bagItem == nullptr)
|
||||
return false; // no bag equipped there
|
||||
const int id = Item::ID::FromCGItem(bagItem);
|
||||
if (id == 0)
|
||||
return false;
|
||||
const uint8_t *rec = PeekItemRecord(static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
return *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_ITEMSTATS_BAG_FAMILY) == 0;
|
||||
}
|
||||
|
||||
bool FindFreeGeneralSlot(void *L, int *outBag, int *outSlot) {
|
||||
for (int bag = 0; bag <= 4; ++bag) {
|
||||
if (!BagIsGeneral(bag))
|
||||
continue;
|
||||
const int n = Item::Location::GetBagSlotCount(bag);
|
||||
for (int slot = 1; slot <= n; ++slot) {
|
||||
if (Item::Location::ResolveBag(L, bag, slot) == nullptr) {
|
||||
*outBag = bag;
|
||||
*outSlot = slot;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- state machine ------------------------------------------------------
|
||||
|
||||
void Finish(bool success) {
|
||||
g_job.active = false;
|
||||
if (!success)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtFailure), "");
|
||||
// success needs no event: the last PostWait already fired
|
||||
// AUCTION_MULTISELL_UPDATE(total, total).
|
||||
}
|
||||
|
||||
void Tick() {
|
||||
if (!g_job.active)
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int savedTop = Game::Lua::GetTop(L);
|
||||
|
||||
if (!AHOpen()) { // window closed / walked away mid-job
|
||||
Finish(false);
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (g_job.phase) {
|
||||
case Phase::Idle: {
|
||||
if (g_job.postedStacks >= g_job.totalStacks) {
|
||||
Finish(true);
|
||||
break;
|
||||
}
|
||||
const SlotInfo s = ReadSlot(L, g_job.srcBag, g_job.srcSlot);
|
||||
if (s.item == nullptr || s.itemID != g_job.itemID || s.count < g_job.quantity) {
|
||||
Finish(false); // source moved / consumed / insufficient
|
||||
break;
|
||||
}
|
||||
if (s.count == g_job.quantity) {
|
||||
// Final exact stack — post the source object directly, no split.
|
||||
g_job.workBag = g_job.srcBag;
|
||||
g_job.workSlot = g_job.srcSlot;
|
||||
g_job.postGuid = s.guid;
|
||||
PostStack(L, s.guid);
|
||||
g_job.phase = Phase::PostWait;
|
||||
g_job.waitTicks = 0;
|
||||
} else {
|
||||
int fb = 0, fs = 0;
|
||||
if (!FindFreeGeneralSlot(L, &fb, &fs)) {
|
||||
Finish(false); // no room to materialize the stack
|
||||
break;
|
||||
}
|
||||
if (!Item::Swap::MoveCount(L, g_job.srcBag, g_job.srcSlot, fb, fs,
|
||||
g_job.quantity)) {
|
||||
Finish(false);
|
||||
break;
|
||||
}
|
||||
g_job.workBag = fb;
|
||||
g_job.workSlot = fs;
|
||||
g_job.phase = Phase::SplitWait;
|
||||
g_job.waitTicks = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Phase::SplitWait: {
|
||||
const SlotInfo w = ReadSlot(L, g_job.workBag, g_job.workSlot);
|
||||
if (w.item != nullptr && w.itemID == g_job.itemID && w.count == g_job.quantity) {
|
||||
g_job.postGuid = w.guid;
|
||||
PostStack(L, w.guid);
|
||||
g_job.phase = Phase::PostWait;
|
||||
g_job.waitTicks = 0;
|
||||
} else if (++g_job.waitTicks > kMaxWaitTicks) {
|
||||
Finish(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Phase::PostWait: {
|
||||
const SlotInfo w = ReadSlot(L, g_job.workBag, g_job.workSlot);
|
||||
// Post confirmed once the server pulls the item out of the slot.
|
||||
if (w.item == nullptr || w.guid != g_job.postGuid) {
|
||||
++g_job.postedStacks;
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtUpdate), "%d%d",
|
||||
g_job.postedStacks, g_job.totalStacks);
|
||||
g_job.phase = Phase::Idle;
|
||||
g_job.waitTicks = 0;
|
||||
} else if (++g_job.waitTicks > kMaxWaitTicks) {
|
||||
Finish(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&Tick};
|
||||
|
||||
// --- Lua entry ----------------------------------------------------------
|
||||
|
||||
// Reads `loc.field` as an int. False if missing/non-numeric. Leaves the
|
||||
// stack as found. (Same shape as Item::Location's file-static helper.)
|
||||
bool ReadIntField(void *L, int locIdx, const char *field, int *out) {
|
||||
Game::Lua::PushString(L, field);
|
||||
Game::Lua::GetTable(L, locIdx);
|
||||
const bool ok = Game::Lua::IsNumber(L, -1);
|
||||
if (ok)
|
||||
*out = static_cast<int>(Game::Lua::ToNumber(L, -1));
|
||||
Game::Lua::SetTop(L, -2);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Map the modern `duration` arg to a valid engine runTime (minutes).
|
||||
// Accepts the 1/2/3 index form and the raw {120,480,1440} value form.
|
||||
// Returns 0 for anything else.
|
||||
int MapDuration(int d) {
|
||||
auto *table = reinterpret_cast<const uint32_t *>(Offsets::VAR_AUCTION_DURATION_TABLE);
|
||||
if (d >= 1 && d <= Offsets::AUCTION_DURATION_TABLE_COUNT)
|
||||
return static_cast<int>(table[d - 1]);
|
||||
for (int i = 0; i < Offsets::AUCTION_DURATION_TABLE_COUNT; ++i)
|
||||
if (static_cast<int>(table[i]) == d)
|
||||
return d;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Push (false, reason) and return 2 — the immediate-rejection shape.
|
||||
int Reject(void *L, const char *reason) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushString(L, reason);
|
||||
return 2;
|
||||
}
|
||||
|
||||
constexpr int kLuaTable = 5; // LUA_TTABLE (no named constant in Game::Lua)
|
||||
|
||||
int __fastcall Script_C_AuctionHouse_PostItem(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != kLuaTable) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_AuctionHouse.PostItem(itemLocation, duration, quantity, numStacks, bid, buyout)");
|
||||
return 0;
|
||||
}
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3) ||
|
||||
!Game::Lua::IsNumber(L, 4) || !Game::Lua::IsNumber(L, 5)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_AuctionHouse.PostItem(itemLocation, duration, quantity, numStacks, bid, buyout)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int bagID = 0, slotIndex = 0;
|
||||
if (!ReadIntField(L, 1, "bagID", &bagID) ||
|
||||
!ReadIntField(L, 1, "slotIndex", &slotIndex))
|
||||
return Reject(L, "itemLocation must be a { bagID, slotIndex } table");
|
||||
|
||||
const int duration = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
const int quantity = static_cast<int>(Game::Lua::ToNumber(L, 3));
|
||||
const int numStacks = static_cast<int>(Game::Lua::ToNumber(L, 4));
|
||||
const int bid = static_cast<int>(Game::Lua::ToNumber(L, 5));
|
||||
const int buyout = Game::Lua::IsNumber(L, 6) ? static_cast<int>(Game::Lua::ToNumber(L, 6)) : 0;
|
||||
|
||||
const int runTime = MapDuration(duration);
|
||||
if (runTime == 0)
|
||||
return Reject(L, "invalid duration (use 1/2/3 or 120/480/1440)");
|
||||
if (quantity < 1 || quantity > 255)
|
||||
return Reject(L, "quantity must be 1..255");
|
||||
if (numStacks < 1)
|
||||
return Reject(L, "numStacks must be >= 1");
|
||||
if (bid < 1)
|
||||
return Reject(L, "bid must be >= 1");
|
||||
|
||||
if (!AHOpen())
|
||||
return Reject(L, "auction house is not open");
|
||||
if (g_job.active)
|
||||
return Reject(L, "a multi-post is already in progress");
|
||||
|
||||
const SlotInfo s = ReadSlot(L, bagID, slotIndex);
|
||||
if (s.item == nullptr)
|
||||
return Reject(L, "source slot is empty");
|
||||
|
||||
const long need = static_cast<long>(quantity) * numStacks;
|
||||
if (static_cast<long>(s.count) < need)
|
||||
return Reject(L, "not enough items for numStacks x quantity");
|
||||
|
||||
// A partial post (source larger than one stack) needs one reusable
|
||||
// general-bag slot to materialize each split. A whole-stack post
|
||||
// (count == quantity, numStacks == 1) needs none.
|
||||
if (s.count > quantity) {
|
||||
int fb = 0, fs = 0;
|
||||
if (!FindFreeGeneralSlot(L, &fb, &fs))
|
||||
return Reject(L, "need at least one free general bag slot to split");
|
||||
}
|
||||
|
||||
g_job = Job{};
|
||||
g_job.active = true;
|
||||
g_job.srcBag = bagID;
|
||||
g_job.srcSlot = slotIndex;
|
||||
g_job.itemID = s.itemID;
|
||||
g_job.quantity = quantity;
|
||||
g_job.totalStacks = numStacks;
|
||||
g_job.postedStacks = 0;
|
||||
g_job.bid = bid;
|
||||
g_job.buyout = buyout;
|
||||
g_job.runTime = runTime;
|
||||
g_job.phase = Phase::Idle;
|
||||
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtStart), "");
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_AuctionHouse", "PostItem",
|
||||
&Script_C_AuctionHouse_PostItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace AuctionHouse::PostItem
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketReader.h"
|
||||
#include "player/StatSignal.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
@@ -288,6 +289,15 @@ int8_t KindForSlot(int slot) {
|
||||
return slot >= Offsets::UNIT_AURA_BUFF_COUNT ? KIND_HARMFUL : KIND_HELPFUL;
|
||||
}
|
||||
|
||||
// Bump the player-stat-inputs signal when an aura change hits the LOCAL
|
||||
// player — buffs/debuffs move GetSpellBonusHealing's flat and Spirit/Armor
|
||||
// terms, so its lazy cache must invalidate. Guarded on the player object so
|
||||
// other units' aura churn (combat) doesn't needlessly invalidate it.
|
||||
void NotifyIfPlayer(void *unit) {
|
||||
if (static_cast<const uint8_t *>(unit) == Unit::Identity::PlayerObject())
|
||||
Player::StatSignal::Notify();
|
||||
}
|
||||
|
||||
// OnAuraAdded — a new aura occupies a slot (gives the spellId directly).
|
||||
using OnAuraAdded_t = void(__fastcall *)(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId);
|
||||
@@ -297,6 +307,7 @@ void __fastcall OnAuraAdded_h(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId) {
|
||||
g_origOnAuraAdded(unit, edx, slot, spellId);
|
||||
StampApplication(unit, spellId, KindForSlot(static_cast<int>(slot)));
|
||||
NotifyIfPlayer(unit);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAuraAdded{
|
||||
@@ -317,6 +328,7 @@ void __fastcall OnAuraStacksChanged_h(void *unit, void *edx, int slot,
|
||||
unit,
|
||||
Aura::Data::ReadSpellID(static_cast<const uint8_t *>(unit), slot),
|
||||
KindForSlot(slot));
|
||||
NotifyIfPlayer(unit);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAuraStacks{
|
||||
@@ -346,6 +358,7 @@ void __fastcall OnAuraRemoved_h(void *unit, void *edx, uint32_t slot,
|
||||
g_origOnAuraRemoved(unit, edx, slot, spellId);
|
||||
(void)slot;
|
||||
Evict(Unit::Identity::GuidForObject(unit), spellId);
|
||||
NotifyIfPlayer(unit);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAuraRemoved{
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "bag/UpdateDelayed.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
@@ -71,8 +72,15 @@ const Event::Custom::AutoReserve _reserve{kEventName};
|
||||
// Single bool, no atomics needed — the engine is single-threaded.
|
||||
bool g_pending = false;
|
||||
|
||||
// C++ subscribers notified once per bag-change frame (see UpdateDelayed.h).
|
||||
AutoSubscribe *g_subs = nullptr;
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoSubscribe::AutoSubscribe(Callback cb) : cb(cb), next(g_subs) {
|
||||
g_subs = this;
|
||||
}
|
||||
|
||||
// `FUN_004F91A0` — `__cdecl void()`. Bag-slot diff loop. Walks the
|
||||
// 10 bag slots, fires BAG_UPDATE for each that changed.
|
||||
using BagSlotDiff_t = void(__cdecl *)();
|
||||
@@ -123,6 +131,9 @@ void OnWorldTick() {
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "");
|
||||
// Notify C++ subscribers of the bag-change frame (see UpdateDelayed.h).
|
||||
for (auto *node = g_subs; node != nullptr; node = node->next)
|
||||
node->cb();
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookSlotDiff{
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// Authoritative "a bag changed this frame" subscription. `Bag::UpdateDelayed`
|
||||
// co-hooks the three (and only three) engine `BAG_UPDATE` fire sites and
|
||||
// coalesces them into a single per-frame signal — the same signal that backs
|
||||
// the `BAG_UPDATE_DELAYED` event. Subscribe here to run C++ work exactly once
|
||||
// per frame in which any bag actually changed, instead of polling every frame.
|
||||
//
|
||||
// Callbacks fire at the tail of the world tick, after every BAG_UPDATE for the
|
||||
// frame has already fired (so bag contents are settled). No firing on frames
|
||||
// where nothing changed. Order across subscribers is unspecified.
|
||||
//
|
||||
// Module pattern, matching `Tick::WorldTick::AutoSubscribe`: declare a
|
||||
// file-scope `static const Bag::UpdateDelayed::AutoSubscribe _s{&Callback};`.
|
||||
|
||||
namespace Bag::UpdateDelayed {
|
||||
|
||||
using Callback = void (*)();
|
||||
|
||||
struct AutoSubscribe {
|
||||
explicit AutoSubscribe(Callback cb);
|
||||
Callback cb;
|
||||
AutoSubscribe *next;
|
||||
};
|
||||
|
||||
} // namespace Bag::UpdateDelayed
|
||||
@@ -0,0 +1,47 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Lua 5.1 math-library additions that 1.12's Lua 5.0 is missing:
|
||||
//
|
||||
// - `math.fmod(x, y)` — floating-point remainder of `x / y`.
|
||||
//
|
||||
// This is a pure rename: Lua 5.0 ships the exact same C function under the
|
||||
// name `math.mod`; 5.1 renamed the Lua-facing entry to `math.fmod`. We
|
||||
// register `math.fmod` as a direct alias of the engine's `math.mod` C
|
||||
// function (`FUN_LUA_MATH_FMOD`) — no wrapper. `math.mod` stays available too
|
||||
// (we don't touch it), so code written for either name works.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace BaseLib::MathLib {
|
||||
|
||||
namespace {
|
||||
|
||||
// `math.fmod` is 5.0's `math.mod` verbatim — register the engine function
|
||||
// pointer directly rather than wrapping it.
|
||||
const auto Script_math_fmod =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_MATH_FMOD);
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("math", "fmod", Script_math_fmod);
|
||||
}
|
||||
|
||||
// Both states are Lua 5.0 and equally missing this; RegisterTableFunction
|
||||
// targets whichever state is active in each registration hook.
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterFns};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterFns};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::MathLib
|
||||
@@ -0,0 +1,87 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `select(index, ...)` — the Lua 5.1 base-library vararg helper, missing
|
||||
// from 1.12's Lua 5.0 (it predates the function). Two forms:
|
||||
// - `select('#', ...)` → the number of extra arguments.
|
||||
// - `select(n, ...)` → all arguments from position `n` onward.
|
||||
// `n` may be negative to index from the end (`-1` = the last arg).
|
||||
//
|
||||
// Ported from Lua 5.1's `luaB_select` (lbaselib.c). The numeric form needs
|
||||
// no pushes at all: the varargs are already sitting on the stack at
|
||||
// positions `2..top`, so returning `top - n` hands back exactly the slice
|
||||
// from `n+1` onward — the same trick the reference implementation uses.
|
||||
// The `'#'` form pushes the count and returns 1.
|
||||
//
|
||||
// Registered on BOTH Lua states (in-game + glue): it's a language-level
|
||||
// helper, state-agnostic, and both states are 5.0 and equally missing it.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::Select {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_select(void *L) {
|
||||
const int n = Game::Lua::GetTop(L);
|
||||
if (n < 1) {
|
||||
Game::Lua::Error(L, "bad argument #1 to 'select' (number expected, got no value)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
|
||||
// `select('#', ...)` — count form. Only inspect the string when the
|
||||
// arg *is* a string: calling lua_tostring on a number would convert
|
||||
// the stack value in place (Lua 5.0/5.1 semantics), corrupting arg 1.
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_STRING) {
|
||||
const char *s = Game::Lua::ToString(L, 1);
|
||||
if (s != nullptr && s[0] == '#') {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(n - 1));
|
||||
return 1;
|
||||
}
|
||||
// A non-'#' string falls through: lua_isnumber accepts numeric
|
||||
// strings (e.g. "2"), matching the reference's luaL_checkint
|
||||
// coercion; a non-numeric string errors below.
|
||||
}
|
||||
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "bad argument #1 to 'select' (number expected)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
int i = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (i < 0)
|
||||
i = n + i; // negative indexes count back from the last argument
|
||||
else if (i > n)
|
||||
i = n;
|
||||
if (i < 1) {
|
||||
Game::Lua::Error(L, "bad argument #1 to 'select' (index out of range)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
// The varargs from position i+1..n are already on the stack; returning
|
||||
// their count hands exactly that slice back to the caller.
|
||||
return n - i;
|
||||
}
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("select", &Script_select);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("select", &Script_select);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::Select
|
||||
@@ -0,0 +1,98 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Lua 5.1 string-library additions that 1.12's Lua 5.0 is missing:
|
||||
//
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction.
|
||||
// - `string.gmatch(s, pattern)` — match iterator.
|
||||
//
|
||||
// Both are the same underlying pattern engine as functions 5.0 already ships:
|
||||
// - `match` is `find` returning captures / the whole match instead of the
|
||||
// start/end indices — so we call the engine's `str_find` and transform.
|
||||
// - `gmatch` is exactly 5.0's `string.gfind` (renamed in 5.1); we register
|
||||
// it as a direct alias of the engine's `gfind` C function.
|
||||
//
|
||||
// NOTE: only the `string.foo(s, ...)` call form works — NOT the
|
||||
// `("x"):foo(...)` method sugar. WoW's Lua VM strips type-metatables for
|
||||
// every value except tables/userdata (both `lua_setmetatable` and the VM's
|
||||
// `luaT_gettmbyobj` hard-return "no metamethod" for strings), so strings have
|
||||
// no `__index`, and there's no client-side way to add one short of hooking a
|
||||
// hot VM-core function. This is the long-standing vanilla constraint.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace BaseLib::StringLib {
|
||||
|
||||
namespace {
|
||||
|
||||
// `string.match(s, pattern [, init])`. Calls the engine `str_find` and
|
||||
// reshapes its result: no match → nil; match with captures → the captures;
|
||||
// match with no captures → the whole matched substring.
|
||||
int __fastcall Script_string_match(void *L) {
|
||||
// Normalize to exactly (s, pattern, init): pad missing args with nil so
|
||||
// str_find defaults `init` to 1, and drop any stray 4th value so it can't
|
||||
// be misread as find's `plain` flag (match has no plain form).
|
||||
Game::Lua::SetTop(L, 3);
|
||||
constexpr int kBase = 3;
|
||||
|
||||
auto strFind = reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_FIND);
|
||||
const int nres = strFind(L); // pushes start,end,caps.. above kBase, or a nil
|
||||
|
||||
if (nres <= 1) {
|
||||
// No match. str_find pushed the nil (return 1); guard the degenerate
|
||||
// nres==0 case by pushing our own nil so the shape holds.
|
||||
if (nres < 1)
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nres == 2) {
|
||||
// No captures → the whole matched substring s[start..end] (1-based,
|
||||
// inclusive). An empty match has end == start-1 → an empty string.
|
||||
const int start = static_cast<int>(Game::Lua::ToNumber(L, kBase + 1));
|
||||
const int end = static_cast<int>(Game::Lua::ToNumber(L, kBase + 2));
|
||||
// str_find already coerced arg 1 to a string, so this is safe.
|
||||
const char *s = Game::Lua::ToString(L, 1);
|
||||
Game::Lua::SetTop(L, kBase); // drop start, end
|
||||
const size_t len = (end >= start) ? static_cast<size_t>(end - start + 1) : 0;
|
||||
Game::Lua::PushLString(L, s + start - 1, static_cast<unsigned int>(len));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Captures present → drop start & end, hand back only the captures.
|
||||
Game::Lua::Remove(L, kBase + 1); // remove start; end → kBase+1, caps → kBase+2..
|
||||
Game::Lua::Remove(L, kBase + 1); // remove end; caps → kBase+1..
|
||||
return nres - 2;
|
||||
}
|
||||
|
||||
// `string.gmatch` is 5.0's `string.gfind` verbatim — register the engine
|
||||
// function pointer directly rather than wrapping it.
|
||||
const auto Script_string_gmatch =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_GFIND);
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "match", &Script_string_match);
|
||||
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch);
|
||||
}
|
||||
|
||||
// Both states are Lua 5.0 and equally missing these; RegisterTableFunction
|
||||
// targets whichever state is active in each registration hook.
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterFns};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterFns};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::StringLib
|
||||
@@ -130,11 +130,39 @@ int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `C_Container.CalculateTotalNumberOfFreeBagSlots()` — the total number of
|
||||
// free slots across the player's *general-purpose* bags (backpack + equipped
|
||||
// bags 0..4 whose BagFamily is 0). Specialty bags (quivers, soul bags, herb
|
||||
// bags, …) are excluded, mirroring FrameXML's implementation exactly:
|
||||
//
|
||||
// for bag = BACKPACK .. lastBag:
|
||||
// free, family = C_Container.GetContainerNumFreeSlots(bag)
|
||||
// if family == 0 then total = total + free end
|
||||
//
|
||||
// Takes no arguments. Caveat inherited from `GetContainerNumFreeSlots`:
|
||||
// vanilla-era server data leaves most bags' BagFamily at 0, so specialty
|
||||
// bags that retail *would* exclude (Soul Pouch, Enchanting Bag, …) get
|
||||
// counted here — the client just doesn't carry the family bit for them. The
|
||||
// result stays consistent with what `GetContainerNumFreeSlots` reports for
|
||||
// those same bags.
|
||||
int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
|
||||
int total = 0;
|
||||
for (int bagID = 0; bagID <= 4; bagID++) {
|
||||
const BagInfo info = ResolveBagInfo(bagID);
|
||||
if (info.slotCount > 0 && info.bagType == 0)
|
||||
total += CountFreeSlotsInBag(L, bagID, info.slotCount);
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(total));
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerNumFreeSlots",
|
||||
&Script_C_Container_GetContainerNumFreeSlots);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "CalculateTotalNumberOfFreeBagSlots",
|
||||
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Container.HasContainerItem(bagIndex, slotIndex)` — returns whether the
|
||||
// given bag slot currently holds an item (a Classic Era addition). It's the
|
||||
// occupancy sibling of `GetContainerItemID`: resolves the slot to its CGItem
|
||||
// the same way (`Item::Location::ResolveBag` → engine `PackBagSlot` →
|
||||
// `GetItemBySlot`), and a populated slot yields a non-null pointer while an
|
||||
// empty slot yields null. Presence only — it does not depend on the item's
|
||||
// static data being cached. Returns `false` for empty slots and invalid
|
||||
// bag/slot indices.
|
||||
|
||||
#include "Game.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
namespace Container::HasItem {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_C_Container_HasContainerItem(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: C_Container.HasContainerItem(bagIndex, slotIndex)");
|
||||
return 0;
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
|
||||
const bool hasItem = Item::Location::ResolveBag(L, bagID, slotIndex) != nullptr;
|
||||
Game::Lua::PushBool(L, hasItem);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "HasContainerItem",
|
||||
&Script_C_Container_HasContainerItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::HasItem
|
||||
@@ -0,0 +1,194 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Container.GetContainerItemInfo(containerIndex, slotIndex)` — the modern
|
||||
// structured-table accessor (namespaced + table-returning since Patch
|
||||
// 10.0.2). Vanilla's global `GetContainerItemInfo` returns a flat 5-tuple
|
||||
// (`texture, itemCount, locked, quality, readable` — verified by decoding
|
||||
// `Script_GetContainerItemInfo` at `0x004F9670`); backported addons expect
|
||||
// the `ContainerItemInfo` table with the wider modern field set. We assemble
|
||||
// that table from the same sources the engine reads plus the extra fields it
|
||||
// never surfaced in 1.12.
|
||||
//
|
||||
// Field sources:
|
||||
// iconFileID — ItemStats displayInfoID → ItemDisplayInfo.dbc icon path.
|
||||
// (1.12 has no fileID system; we return the icon *path*
|
||||
// string, same convention as `GetItemIcon`.)
|
||||
// stackCount — CGItem descriptor `+OFF_DESCRIPTOR_STACK_COUNT` (the
|
||||
// engine's `itemCount` return).
|
||||
// isLocked — CGItem `+OFF_ITEM_CLIENT_LOCK & 1` (the `locked` return).
|
||||
// quality — ItemStats `+OFF_ITEMSTATS_QUALITY`; nil when uncached
|
||||
// (modern contract: `Enum.ItemQuality or nil`).
|
||||
// isReadable — static page text OR the per-instance readable-text id,
|
||||
// mirroring the engine's `readable` branch at `0x004F97BD`.
|
||||
// hasLoot — ItemStats LOOTABLE flag (best-effort static approximation:
|
||||
// vanilla can't track post-loot dynamic emptiness client-side).
|
||||
// hyperlink — engine link builder via `Item::Link::FromCGItem` (enchant /
|
||||
// random-suffix decorated, same as `GetContainerItemLink`).
|
||||
// isFiltered — always false (vanilla has no bag search-filter system).
|
||||
// hasNoValue — ItemStats sellPrice == 0.
|
||||
// itemID — instance block itemID (`Item::ID::FromCGItem`).
|
||||
// isBound — CGItem descriptor SOULBOUND flag (`C_Item.IsBound`'s bit).
|
||||
// itemName — instance-decorated display name, base ItemStats name on miss.
|
||||
//
|
||||
// Returns nil for an empty slot or an invalid bag/slot index (modern
|
||||
// contract). Cache-derived fields (icon / quality / hasLoot / hasNoValue /
|
||||
// name) are omitted/nil when the item's static data isn't cached yet — the
|
||||
// live per-instance fields (count / locked / bound / itemID / link) are always
|
||||
// present. Passive: does not warm the item cache.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/StatAccum.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace Container::ItemInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
bool BuildIconPath(uint32_t displayInfoID, char *out, size_t outSize) {
|
||||
out[0] = '\0';
|
||||
const char *iconName = DBC::StringField(
|
||||
Offsets::VAR_ITEMDISPLAYINFO_RECORDS, Offsets::VAR_ITEMDISPLAYINFO_COUNT,
|
||||
displayInfoID, Offsets::OFF_ITEMDISPLAYINFO_ICON);
|
||||
if (iconName == nullptr || iconName[0] == '\0')
|
||||
return false;
|
||||
std::snprintf(out, outSize, "Interface\\Icons\\%s", iconName);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mirrors the engine's `readable` decision (0x004F97BD..0x004F97CF): a slot
|
||||
// is readable when its static page text is set OR the per-instance
|
||||
// readable-text id is non-zero (letters / server-authored notes).
|
||||
bool ItemReadable(const uint8_t *item, const uint8_t *record) {
|
||||
if (record != nullptr &&
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_ITEMSTATS_PAGE_TEXT) != 0)
|
||||
return true;
|
||||
const uint8_t *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
return descriptor != nullptr &&
|
||||
*reinterpret_cast<const uint32_t *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_READABLE_TEXT_ID) != 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_C_Container_GetContainerItemInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Container.GetContainerItemInfo(containerIndex, slotIndex)");
|
||||
return 0;
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
|
||||
const uint8_t *item = Item::Location::ResolveBag(L, bagID, slotIndex);
|
||||
if (item == nullptr) {
|
||||
Game::Lua::PushNil(L); // empty slot / bad index → nil (modern contract)
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- Gather everything into locals BEFORE touching the Lua stack. The
|
||||
// engine link/name builders and cache/DBC reads below are pure C; keeping
|
||||
// them off the half-built table avoids any stack interleaving. ---
|
||||
const int itemID = Item::ID::FromCGItem(item);
|
||||
const uint8_t *record = (itemID > 0)
|
||||
? Item::StatAccum::FetchRecord(static_cast<uint32_t>(itemID))
|
||||
: nullptr;
|
||||
|
||||
// Live per-instance descriptor fields.
|
||||
const uint8_t *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
int stackCount = 0;
|
||||
bool isBound = false;
|
||||
if (descriptor != nullptr) {
|
||||
stackCount = *reinterpret_cast<const int *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT);
|
||||
isBound = (*reinterpret_cast<const uint32_t *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_FLAGS) &
|
||||
Offsets::ITEM_FLAG_SOULBOUND) != 0;
|
||||
}
|
||||
const bool isLocked = (*reinterpret_cast<const uint32_t *>(
|
||||
item + Offsets::OFF_ITEM_CLIENT_LOCK) &
|
||||
Offsets::ITEM_CLIENT_LOCK_BIT) != 0;
|
||||
const bool isReadable = ItemReadable(item, record);
|
||||
|
||||
// Static template fields (cache-dependent).
|
||||
bool haveIcon = false;
|
||||
char iconPath[260];
|
||||
bool haveQuality = false;
|
||||
uint32_t quality = 0;
|
||||
bool hasLoot = false;
|
||||
bool hasNoValue = false;
|
||||
if (record != nullptr) {
|
||||
const uint32_t displayInfoID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
haveIcon = BuildIconPath(displayInfoID, iconPath, sizeof(iconPath));
|
||||
quality = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
haveQuality = true;
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_FLAGS);
|
||||
hasLoot = (flags & Offsets::ITEM_FLAG_LOOTABLE) != 0;
|
||||
hasNoValue = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SELL_PRICE) == 0;
|
||||
}
|
||||
|
||||
const char *hyperlink = Item::Link::FromCGItem(item);
|
||||
|
||||
char nameBuf[256];
|
||||
const char *itemName = nullptr;
|
||||
if (Item::Link::NameFromCGItem(item, nameBuf, sizeof(nameBuf))) {
|
||||
itemName = nameBuf;
|
||||
} else if (record != nullptr) {
|
||||
const char *baseName = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
if (baseName != nullptr && baseName[0] != '\0')
|
||||
itemName = baseName;
|
||||
}
|
||||
|
||||
// --- Build the ContainerItemInfo table (only stack-safe SetField* now). ---
|
||||
Game::Lua::NewTable(L);
|
||||
if (haveIcon)
|
||||
Game::Lua::SetFieldString(L, "iconFileID", iconPath);
|
||||
Game::Lua::SetFieldNumber(L, "stackCount", static_cast<double>(stackCount));
|
||||
Game::Lua::SetFieldBool(L, "isLocked", isLocked);
|
||||
if (haveQuality)
|
||||
Game::Lua::SetFieldNumber(L, "quality", static_cast<double>(quality));
|
||||
Game::Lua::SetFieldBool(L, "isReadable", isReadable);
|
||||
Game::Lua::SetFieldBool(L, "hasLoot", hasLoot);
|
||||
if (hyperlink != nullptr && hyperlink[0] != '\0')
|
||||
Game::Lua::SetFieldString(L, "hyperlink", hyperlink);
|
||||
Game::Lua::SetFieldBool(L, "isFiltered", false);
|
||||
Game::Lua::SetFieldBool(L, "hasNoValue", hasNoValue);
|
||||
Game::Lua::SetFieldNumber(L, "itemID", static_cast<double>(itemID));
|
||||
Game::Lua::SetFieldBool(L, "isBound", isBound);
|
||||
if (itemName != nullptr)
|
||||
Game::Lua::SetFieldString(L, "itemName", itemName);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemInfo",
|
||||
&Script_C_Container_GetContainerItemInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::ItemInfo
|
||||
@@ -36,10 +36,24 @@
|
||||
// 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_*`.
|
||||
//
|
||||
// This file also hosts the two static-DBC lookups on the same table:
|
||||
//
|
||||
// C_CreatureInfo.GetRaceInfo(raceID) -> { raceName, clientFileString, raceID }
|
||||
// C_CreatureInfo.GetClassInfo(classID) -> { className, classFile, classID }
|
||||
//
|
||||
// retail backports over `ChrRaces.dbc` / `ChrClasses.dbc`. Unlike the
|
||||
// creature cache these DBCs are always loaded, localized, and synchronous —
|
||||
// so a by-id lookup works for any race/class id at any time, with no WDB
|
||||
// cold-cache problem. The file-string columns (`clientFileString` /
|
||||
// `classFile`) are the same non-localized tokens `UnitRace`/`UnitClass`
|
||||
// return as their 2nd value ("NightElf", "WARRIOR"), so they round-trip
|
||||
// with the unit APIs. See Offsets.h `OFF_CHRRACES_*` / `OFF_CHRCLASSES_*`.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cache/QueryLoad.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -122,11 +136,242 @@ int __fastcall Script_RequestLoadCreatureByID(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetRaceInfo(raceID)` — RaceInfo table, or nil for a
|
||||
// non-numeric / non-positive id or one with no ChrRaces row.
|
||||
int __fastcall Script_GetRaceInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int raceID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *file =
|
||||
(raceID > 0) ? DBC::StringField(Offsets::VAR_CHRRACES_RECORDS,
|
||||
Offsets::VAR_CHRRACES_COUNT,
|
||||
static_cast<uint32_t>(raceID),
|
||||
Offsets::OFF_CHRRACES_FILENAME)
|
||||
: nullptr;
|
||||
if (file == nullptr)
|
||||
return 0; // unknown id -> nil
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_CHRRACES_RECORDS,
|
||||
Offsets::VAR_CHRRACES_COUNT,
|
||||
static_cast<uint32_t>(raceID),
|
||||
Offsets::OFF_CHRRACES_NAMES);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "raceName", name); // NULL coerced to ""
|
||||
Game::Lua::SetFieldString(L, "clientFileString", file);
|
||||
Game::Lua::SetFieldNumber(L, "raceID", raceID);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetClassInfo(classID)` — ClassInfo table, or nil for a
|
||||
// non-numeric / non-positive id or one with no ChrClasses row.
|
||||
int __fastcall Script_GetClassInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int classID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *file =
|
||||
(classID > 0) ? DBC::StringField(Offsets::VAR_CHRCLASSES_RECORDS,
|
||||
Offsets::VAR_CHRCLASSES_COUNT,
|
||||
static_cast<uint32_t>(classID),
|
||||
Offsets::OFF_CHRCLASSES_FILENAME)
|
||||
: nullptr;
|
||||
if (file == nullptr)
|
||||
return 0; // unknown id -> nil
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_CHRCLASSES_RECORDS,
|
||||
Offsets::VAR_CHRCLASSES_COUNT,
|
||||
static_cast<uint32_t>(classID),
|
||||
Offsets::OFF_CHRCLASSES_NAMES);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "className", name); // NULL coerced to ""
|
||||
Game::Lua::SetFieldString(L, "classFile", file);
|
||||
Game::Lua::SetFieldNumber(L, "classID", classID);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetCreatureFamilyInfo(creatureFamilyID)` — CreatureFamilyInfo
|
||||
// table for a `CreatureFamily.dbc` row (1 = Wolf, 27 = Wind Serpent, …), or
|
||||
// nil for a non-numeric / non-positive / unused id. `iconFile` is a texture
|
||||
// path string here (retail exposes a numeric fileID; vanilla's DBC stores the
|
||||
// path) — empty "" for families with no icon (e.g. warlock pets).
|
||||
int __fastcall Script_GetCreatureFamilyInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int familyID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *name =
|
||||
(familyID > 0) ? DBC::LocalizedField(Offsets::VAR_CREATUREFAMILY_RECORDS,
|
||||
Offsets::VAR_CREATUREFAMILY_COUNT,
|
||||
static_cast<uint32_t>(familyID),
|
||||
Offsets::OFF_CREATUREFAMILY_NAMES)
|
||||
: nullptr;
|
||||
if (name == nullptr)
|
||||
return 0; // unused / OOR id -> nil
|
||||
const char *icon = DBC::StringField(Offsets::VAR_CREATUREFAMILY_RECORDS,
|
||||
Offsets::VAR_CREATUREFAMILY_COUNT,
|
||||
static_cast<uint32_t>(familyID),
|
||||
Offsets::OFF_CREATUREFAMILY_ICON);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "id", familyID);
|
||||
Game::Lua::SetFieldString(L, "name", name);
|
||||
Game::Lua::SetFieldString(L, "iconFile", icon); // NULL/empty coerced to ""
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetCreatureFamilyIDs()` — array of every populated
|
||||
// `CreatureFamily.dbc` id (sparse: skips the unused rows). Round-trips with
|
||||
// GetCreatureFamilyInfo for each element.
|
||||
int __fastcall Script_GetCreatureFamilyIDs(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_CREATUREFAMILY_COUNT);
|
||||
int n = 0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_CREATUREFAMILY_RECORDS,
|
||||
Offsets::VAR_CREATUREFAMILY_COUNT,
|
||||
static_cast<uint32_t>(id),
|
||||
Offsets::OFF_CREATUREFAMILY_NAMES);
|
||||
if (name == nullptr)
|
||||
continue; // unused row
|
||||
++n;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(n));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(id));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetFactionInfo(raceID)` — faction-group info for a race:
|
||||
// { name = localized ("Alliance"/"Horde"/…), groupTag = english }. Mirrors
|
||||
// the player branch of `Script_UnitFactionGroup` (`0x00516630`): resolve the
|
||||
// race's FactionTemplate group mask, then scan `FactionGroup.dbc` for the row
|
||||
// whose bit the mask sets and whose localized name is non-empty (which is what
|
||||
// skips the always-set "Player" / the nameless "Monster" rows and lands on
|
||||
// Alliance / Horde). nil for a non-numeric / non-positive / unknown race or a
|
||||
// race with no named group. See Offsets.h for the full chain.
|
||||
int __fastcall Script_GetFactionInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int raceID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (raceID <= 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *crRec = DBC::Record(Offsets::VAR_CHRRACES_RECORDS,
|
||||
Offsets::VAR_CHRRACES_COUNT,
|
||||
static_cast<uint32_t>(raceID));
|
||||
if (crRec == nullptr)
|
||||
return 0; // unknown race
|
||||
const int ftId = *reinterpret_cast<const int *>(
|
||||
crRec + Offsets::OFF_CHRRACES_FACTION_TEMPLATE);
|
||||
if (ftId <= 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *ftRec = DBC::Record(Offsets::VAR_FACTIONTEMPLATE_RECORDS,
|
||||
Offsets::VAR_FACTIONTEMPLATE_COUNT,
|
||||
static_cast<uint32_t>(ftId));
|
||||
if (ftRec == nullptr)
|
||||
return 0;
|
||||
const uint32_t mask = *reinterpret_cast<const uint32_t *>(
|
||||
ftRec + Offsets::OFF_FACTIONTEMPLATE_GROUP_MASK);
|
||||
|
||||
// FactionGroup is stored contiguously (records-base + i*stride), not as a
|
||||
// pointer array — iterate by hand rather than via DBC::Record.
|
||||
auto *fgBase = *reinterpret_cast<const uint8_t *const *>(
|
||||
Offsets::VAR_FACTIONGROUP_RECORDS);
|
||||
const int fgCount =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_FACTIONGROUP_COUNT);
|
||||
const int locale = *reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
if (fgBase == nullptr)
|
||||
return 0;
|
||||
|
||||
for (int i = 0; i < fgCount; ++i) {
|
||||
const uint8_t *row = fgBase + i * Offsets::FACTIONGROUP_STRIDE;
|
||||
const int bit = *reinterpret_cast<const int *>(
|
||||
row + Offsets::OFF_FACTIONGROUP_BIT);
|
||||
if ((mask & (1u << (bit & 0x1F))) == 0)
|
||||
continue;
|
||||
const char *loc = *reinterpret_cast<const char *const *>(
|
||||
row + Offsets::OFF_FACTIONGROUP_NAMES + locale * 4);
|
||||
if (loc == nullptr || *loc == '\0')
|
||||
continue; // "Player" / "Monster" have empty localized names
|
||||
const char *eng = *reinterpret_cast<const char *const *>(
|
||||
row + Offsets::OFF_FACTIONGROUP_ENGLISH);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "name", loc);
|
||||
Game::Lua::SetFieldString(L, "groupTag", eng);
|
||||
return 1;
|
||||
}
|
||||
return 0; // no named group -> nil
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetCreatureTypeInfo(creatureTypeID)` — CreatureTypeInfo
|
||||
// table for a `CreatureType.dbc` row (1 = Beast … 11 = Totem), or nil for a
|
||||
// non-numeric / non-positive / unused id.
|
||||
int __fastcall Script_GetCreatureTypeInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int typeID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *name =
|
||||
(typeID > 0) ? DBC::LocalizedField(Offsets::VAR_CREATURETYPE_RECORDS,
|
||||
Offsets::VAR_CREATURETYPE_COUNT,
|
||||
static_cast<uint32_t>(typeID),
|
||||
Offsets::OFF_CREATURETYPE_NAMES)
|
||||
: nullptr;
|
||||
if (name == nullptr)
|
||||
return 0; // unused / OOR id -> nil
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "id", typeID);
|
||||
Game::Lua::SetFieldString(L, "name", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetCreatureTypeIDs()` — array of every populated
|
||||
// `CreatureType.dbc` id (contiguous 1..N in vanilla). Round-trips with
|
||||
// GetCreatureTypeInfo for each element.
|
||||
int __fastcall Script_GetCreatureTypeIDs(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_CREATURETYPE_COUNT);
|
||||
int n = 0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_CREATURETYPE_RECORDS,
|
||||
Offsets::VAR_CREATURETYPE_COUNT,
|
||||
static_cast<uint32_t>(id),
|
||||
Offsets::OFF_CREATURETYPE_NAMES);
|
||||
if (name == nullptr)
|
||||
continue;
|
||||
++n;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(n));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(id));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureInfoByID",
|
||||
&Script_GetCreatureInfoByID);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "RequestLoadCreatureByID",
|
||||
&Script_RequestLoadCreatureByID);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetRaceInfo",
|
||||
&Script_GetRaceInfo);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetClassInfo",
|
||||
&Script_GetClassInfo);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureFamilyInfo",
|
||||
&Script_GetCreatureFamilyInfo);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureFamilyIDs",
|
||||
&Script_GetCreatureFamilyIDs);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetFactionInfo",
|
||||
&Script_GetFactionInfo);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureTypeInfo",
|
||||
&Script_GetCreatureTypeInfo);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureTypeIDs",
|
||||
&Script_GetCreatureTypeIDs);
|
||||
Cache::QueryLoad::Register(reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE),
|
||||
kCreatureDataLoadResult, Offsets::FUN_CREATURE_GET_RECORD);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "event/Custom.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -31,24 +32,15 @@ std::string g_path;
|
||||
bool g_loaded = false;
|
||||
bool g_ignoredForSave[SLOT_COUNT] = {};
|
||||
|
||||
void *ResolvePlayerInvMgr() {
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *);
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<uint8_t *>(fn("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
|
||||
}
|
||||
|
||||
// Reads the GUID currently in paperdoll slot `slot1Based` (1..19) by
|
||||
// indexing the player invMgr's flat GUID array. Slot 1 (HEAD) lives
|
||||
// at linear index 0; the equipment paperdoll occupies indices 0..18.
|
||||
// Returns 0 for empty slots or unresolved player.
|
||||
uint64_t ReadEquippedGUID(int slot1Based) {
|
||||
auto *invMgr = static_cast<uint8_t *>(ResolvePlayerInvMgr());
|
||||
const uint8_t *invMgr = Unit::Identity::PlayerInventoryManager();
|
||||
if (invMgr == nullptr)
|
||||
return 0;
|
||||
auto *guids = *reinterpret_cast<uint64_t **>(
|
||||
auto *guids = *reinterpret_cast<uint64_t *const *>(
|
||||
invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (guids == nullptr)
|
||||
return 0;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "item/Location.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -41,19 +42,6 @@ constexpr int LINEAR_BAG_SLOT_FIRST = 19; // bag1 → linear 19
|
||||
constexpr int LINEAR_BACKPACK_FIRST = 23;
|
||||
constexpr int LINEAR_BACKPACK_LAST = 38;
|
||||
|
||||
void *ResolvePlayer() {
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *);
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return fn("player");
|
||||
}
|
||||
|
||||
uint8_t *ResolvePlayerInvMgr() {
|
||||
auto *player = static_cast<uint8_t *>(ResolvePlayer());
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
|
||||
}
|
||||
|
||||
uint64_t *InvMgrGuidArray(const uint8_t *invMgr) {
|
||||
if (invMgr == nullptr)
|
||||
return nullptr;
|
||||
@@ -79,20 +67,6 @@ uint8_t *ResolveByGUID(int type, uint64_t guid) {
|
||||
0x172));
|
||||
}
|
||||
|
||||
// CGContainer exposes its own `CContainerInventory *` via vtable
|
||||
// `+0x10` — same dispatch `GetItemCount`'s bank walk uses. The
|
||||
// returned invMgr has the standard shape: slot count at +0x00,
|
||||
// flat GUID array at +0x04.
|
||||
uint8_t *BagInvMgr(uint8_t *bag) {
|
||||
if (bag == nullptr)
|
||||
return nullptr;
|
||||
auto *vtable = *reinterpret_cast<uint8_t **>(bag);
|
||||
using GetInvMgr_t = void *(__thiscall *)(void *);
|
||||
auto getInvMgr = reinterpret_cast<GetInvMgr_t>(
|
||||
*reinterpret_cast<uintptr_t *>(vtable + 0x10));
|
||||
return static_cast<uint8_t *>(getInvMgr(bag));
|
||||
}
|
||||
|
||||
// Walks one bag's contents for a matching GUID. Returns the 1-based
|
||||
// slot index of the match, or 0 if not found / bag unresolvable.
|
||||
// Works for both player bags (linear 19..22 of player invMgr) and
|
||||
@@ -102,7 +76,8 @@ int WalkBagContents(uint64_t bagGuid, uint64_t targetGuid) {
|
||||
auto *bag = ResolveByGUID(Offsets::OBJ_TYPE_CONTAINER, bagGuid);
|
||||
if (bag == nullptr)
|
||||
return 0;
|
||||
auto *bagInvMgr = BagInvMgr(bag);
|
||||
auto *bagInvMgr =
|
||||
static_cast<const uint8_t *>(Item::Location::ContainerInventory(bag));
|
||||
if (bagInvMgr == nullptr)
|
||||
return 0;
|
||||
const int count = InvMgrSlotCount(bagInvMgr);
|
||||
@@ -122,7 +97,7 @@ int FindGUID(uint64_t targetGuid) {
|
||||
if (targetGuid == GUID_EMPTY || targetGuid == GUID_IGNORED)
|
||||
return 0;
|
||||
|
||||
auto *invMgr = ResolvePlayerInvMgr();
|
||||
auto *invMgr = Unit::Identity::PlayerInventoryManager();
|
||||
auto *guids = InvMgrGuidArray(invMgr);
|
||||
if (guids == nullptr)
|
||||
return 0;
|
||||
@@ -180,7 +155,7 @@ const uint8_t *ResolveItemByGUID(uint64_t guid) {
|
||||
void SnapshotPaperdoll(uint64_t *out) {
|
||||
for (int i = 0; i < SLOT_COUNT; ++i)
|
||||
out[i] = 0;
|
||||
auto *invMgr = ResolvePlayerInvMgr();
|
||||
auto *invMgr = Unit::Identity::PlayerInventoryManager();
|
||||
auto *guids = InvMgrGuidArray(invMgr);
|
||||
if (guids == nullptr)
|
||||
return;
|
||||
|
||||
+11
-1
@@ -28,7 +28,17 @@ namespace {
|
||||
// any engine code runs. `RetryClaims` (triggered by the
|
||||
// `Frame::RegisterEvent` hook) walks this list and claims a NULL slot
|
||||
// for each unclaimed entry.
|
||||
constexpr int MAX_RESERVED = 32;
|
||||
//
|
||||
// MUST stay comfortably above the total number of `AutoReserve` instances
|
||||
// across the codebase (file-scope + the lazily-registered TTS ones) — the
|
||||
// constructor SILENTLY drops any reservation past the cap, and which ones
|
||||
// overflow depends on unspecified cross-TU static-init order, so exceeding
|
||||
// it makes an arbitrary subset of custom events quietly stop firing (their
|
||||
// `Lookup` returns -1 and `Fire` no-ops). We hit exactly that at 32: ~40
|
||||
// reservations meant a shifting handful (e.g. ITEM_DATA_LOAD_RESULT /
|
||||
// GET_ITEM_INFO_RECEIVED) never claimed a slot. Grep `AutoReserve` before
|
||||
// bumping the reservation count near this ceiling.
|
||||
constexpr int MAX_RESERVED = 64;
|
||||
struct ReservedName {
|
||||
const char *name;
|
||||
int slot; // -1 until claimed
|
||||
|
||||
+3
-2
@@ -37,8 +37,9 @@ namespace Event::Custom {
|
||||
// DLL operate on the table independently of the others.
|
||||
//
|
||||
// The name pointer must outlive the engine (a string literal does).
|
||||
// Same name reserved twice is deduped; reserving more than 32 names
|
||||
// total silently drops the overflow.
|
||||
// Same name reserved twice is deduped; reserving more than `MAX_RESERVED`
|
||||
// names total (see Custom.cpp) silently drops the overflow — keep that cap
|
||||
// above the codebase-wide `AutoReserve` count.
|
||||
struct AutoReserve {
|
||||
explicit AutoReserve(const char *name);
|
||||
};
|
||||
|
||||
+68
-19
@@ -63,6 +63,14 @@ const char *const kKeyName[6] = {
|
||||
"LSHIFT", "RSHIFT", "LCTRL", "RCTRL", "LALT", "RALT",
|
||||
};
|
||||
|
||||
// L/R-specific virtual keys, one per bit, for the focus-regain resync.
|
||||
// `GetAsyncKeyState` accepts the L/R-distinguishing VKs directly (unlike
|
||||
// the keyboard-message path, where `VK_SHIFT` arrives merged and has to be
|
||||
// disambiguated via the scancode).
|
||||
const int kBitVk[6] = {
|
||||
VK_LSHIFT, VK_RSHIFT, VK_LCONTROL, VK_RCONTROL, VK_LMENU, VK_RMENU,
|
||||
};
|
||||
|
||||
constexpr const char *kModifierStateChanged = "MODIFIER_STATE_CHANGED";
|
||||
|
||||
// Static-init reservation — runs before DllMain. The
|
||||
@@ -72,6 +80,7 @@ const Event::Custom::AutoReserve _reserveModifierStateChanged{kModifierStateChan
|
||||
|
||||
uint32_t g_modifierMask = 0;
|
||||
HHOOK g_msgHook = nullptr;
|
||||
HHOOK g_wndProcHook = nullptr;
|
||||
|
||||
void PushDown(void *L, bool down) {
|
||||
if (down)
|
||||
@@ -80,6 +89,28 @@ void PushDown(void *L, bool down) {
|
||||
Game::Lua::PushNil(L);
|
||||
}
|
||||
|
||||
// Apply a single modifier bit transition: update the cached mask and, on a
|
||||
// genuine change, fire `MODIFIER_STATE_CHANGED`. Both the keyboard-message
|
||||
// path and the focus-regain resync funnel through here so the "only
|
||||
// transitions fire" invariant holds regardless of source.
|
||||
void SetBit(int bitIdx, bool down) {
|
||||
const uint32_t bit = 1u << bitIdx;
|
||||
const bool wasDown = (g_modifierMask & bit) != 0;
|
||||
if (down == wasDown)
|
||||
return;
|
||||
|
||||
if (down) g_modifierMask |= bit;
|
||||
else g_modifierMask &= ~bit;
|
||||
|
||||
// Slot was assigned when the engine populated its event table; our
|
||||
// hook on `RebuildEventTable` recorded the index. Lookup at fire
|
||||
// time rather than caching because `/reload` re-runs the rebuild
|
||||
// and the index may shift.
|
||||
const int slot = Event::Custom::Lookup(kModifierStateChanged);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%s%d", kKeyName[bitIdx], static_cast<int>(down));
|
||||
}
|
||||
|
||||
template <int BitIdx>
|
||||
int __fastcall Script_IsKeyDown(void *L) {
|
||||
const bool down = (g_modifierMask & (1u << BitIdx)) != 0;
|
||||
@@ -131,23 +162,22 @@ void ProcessKeyMessage(UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
const int bitIdx = VkToBitIdx(wParam, lParam);
|
||||
if (bitIdx < 0)
|
||||
return;
|
||||
|
||||
const uint32_t bit = 1u << bitIdx;
|
||||
const bool wasDown = (g_modifierMask & bit) != 0;
|
||||
const bool nowDown = (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN);
|
||||
if (nowDown == wasDown)
|
||||
return;
|
||||
SetBit(bitIdx, nowDown);
|
||||
}
|
||||
|
||||
if (nowDown) g_modifierMask |= bit;
|
||||
else g_modifierMask &= ~bit;
|
||||
|
||||
// Slot was assigned when the engine populated its event table; our
|
||||
// hook on `RebuildEventTable` recorded the index. Lookup at fire
|
||||
// time rather than caching because `/reload` re-runs the rebuild
|
||||
// and the index may shift.
|
||||
const int slot = Event::Custom::Lookup(kModifierStateChanged);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%s%d", kKeyName[bitIdx], static_cast<int>(nowDown));
|
||||
// Reconcile the cached mask with the true physical key state. Needed on
|
||||
// focus regain: keyboard messages only arrive while WoW has focus, so a
|
||||
// modifier released while WoW was in the background is never seen by the
|
||||
// `WH_GETMESSAGE` hook and the bit stays stuck down (and the release event
|
||||
// never fires). `GetAsyncKeyState` reads global async keyboard state
|
||||
// regardless of focus, so polling it on activation recovers the missed
|
||||
// transitions — `SetBit` fires `MODIFIER_STATE_CHANGED` for each drift.
|
||||
void ResyncFromPhysicalState() {
|
||||
for (int b = 0; b < 6; ++b) {
|
||||
const bool down = (GetAsyncKeyState(kBitVk[b]) & 0x8000) != 0;
|
||||
SetBit(b, down);
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK GetMsgHook(int code, WPARAM wParam, LPARAM lParam) {
|
||||
@@ -164,11 +194,30 @@ LRESULT CALLBACK GetMsgHook(int code, WPARAM wParam, LPARAM lParam) {
|
||||
return CallNextHookEx(nullptr, code, wParam, lParam);
|
||||
}
|
||||
|
||||
// `WM_ACTIVATEAPP` is *sent* (via the window manager), not *posted* to the
|
||||
// queue, so `WH_GETMESSAGE` never sees it — a `WH_CALLWNDPROC` hook catches
|
||||
// sent messages before the target WNDPROC runs. On app activation
|
||||
// (`wParam != FALSE`) we resync from physical key state to recover any
|
||||
// modifier released while WoW was in the background. Like the message hook,
|
||||
// this is thread-level (not per-`HWND`), so it survives window recreation.
|
||||
LRESULT CALLBACK CallWndProcHook(int code, WPARAM wParam, LPARAM lParam) {
|
||||
if (code == HC_ACTION) {
|
||||
const CWPSTRUCT *cwp = reinterpret_cast<const CWPSTRUCT *>(lParam);
|
||||
if (cwp->message == WM_ACTIVATEAPP && cwp->wParam != FALSE)
|
||||
ResyncFromPhysicalState();
|
||||
}
|
||||
return CallNextHookEx(nullptr, code, wParam, lParam);
|
||||
}
|
||||
|
||||
void InstallHook() {
|
||||
if (g_msgHook != nullptr)
|
||||
return;
|
||||
g_msgHook = SetWindowsHookExA(WH_GETMESSAGE, &GetMsgHook, nullptr,
|
||||
GetCurrentThreadId());
|
||||
if (g_msgHook == nullptr) {
|
||||
g_msgHook = SetWindowsHookExA(WH_GETMESSAGE, &GetMsgHook, nullptr,
|
||||
GetCurrentThreadId());
|
||||
}
|
||||
if (g_wndProcHook == nullptr) {
|
||||
g_wndProcHook = SetWindowsHookExA(WH_CALLWNDPROC, &CallWndProcHook,
|
||||
nullptr, GetCurrentThreadId());
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -153,21 +154,11 @@ ItemInfo ReadItemInfo(const uint8_t *cgItem) {
|
||||
// Bank-walk helpers — bypass the bank-window gate on `GetItemBySlot`
|
||||
// by reading the player's invMgr GUID array directly. Same shape as
|
||||
// `C_Item.GetItemCount`'s bank path.
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using ResolveObjectByGuid_t = void *(__fastcall *)(int type,
|
||||
const char *debugName,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int priority);
|
||||
using GetBagInvMgr_t = void *(__thiscall *)(void *bag);
|
||||
|
||||
const uint8_t *PlayerInvMgr() {
|
||||
auto resolveUnit = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<uint8_t *>(resolveUnit("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
|
||||
}
|
||||
|
||||
const uint8_t *ResolveByGuid(int type, uint64_t guid) {
|
||||
if (guid == 0)
|
||||
@@ -229,7 +220,7 @@ void WalkGuidArrayRange(const uint8_t *invMgr, int firstSlot, int lastSlot,
|
||||
// player invMgr hold the bank bags themselves; each bag has its own
|
||||
// invMgr accessed via the bag CGContainer's vtable+0x10 method).
|
||||
void WalkBankBags(std::vector<Candidate> &candidates) {
|
||||
auto *playerInvMgr = PlayerInvMgr();
|
||||
auto *playerInvMgr = Unit::Identity::PlayerInventoryManager();
|
||||
if (playerInvMgr == nullptr)
|
||||
return;
|
||||
auto *playerGuidArray = *reinterpret_cast<const uint64_t *const *>(
|
||||
@@ -242,11 +233,8 @@ void WalkBankBags(std::vector<Candidate> &candidates) {
|
||||
playerGuidArray[slot]);
|
||||
if (bag == nullptr)
|
||||
continue;
|
||||
auto *vtable = *reinterpret_cast<const uint8_t *const *>(bag);
|
||||
auto getInvMgr = reinterpret_cast<GetBagInvMgr_t>(
|
||||
*reinterpret_cast<const uintptr_t *>(vtable + 0x10));
|
||||
auto *bagInvMgr = static_cast<const uint8_t *>(
|
||||
getInvMgr(const_cast<uint8_t *>(bag)));
|
||||
auto *bagInvMgr =
|
||||
static_cast<const uint8_t *>(Item::Location::ContainerInventory(bag));
|
||||
if (bagInvMgr == nullptr)
|
||||
continue;
|
||||
const int bagSlots =
|
||||
@@ -295,7 +283,7 @@ static int __fastcall Script_GetAverageItemLevel(void *L) {
|
||||
// session — GUIDs are populated from server data at login. Main
|
||||
// bank first (linear slots 39..62 in the player invMgr), then
|
||||
// each equipped bank bag's contents via the per-bag CGContainer.
|
||||
const uint8_t *playerInvMgr = PlayerInvMgr();
|
||||
const uint8_t *playerInvMgr = Unit::Identity::PlayerInventoryManager();
|
||||
WalkGuidArrayRange(playerInvMgr,
|
||||
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT,
|
||||
|
||||
+5
-19
@@ -16,6 +16,7 @@
|
||||
#include "item/Arg.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -94,22 +95,12 @@ int CountInBag(void *L, int bagID, int targetItemID, bool includeUses) {
|
||||
// object resolver `FUN_OBJECT_RESOLVE_BY_GUID` (the same function
|
||||
// `GetItemBySlot` would call internally if the gate let us through).
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using ResolveObjectByGuid_t = void *(__fastcall *)(int type,
|
||||
const char *debugName,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int priority);
|
||||
|
||||
const uint8_t *PlayerInvMgr() {
|
||||
auto ResolveUnitToken =
|
||||
reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<uint8_t *>(ResolveUnitToken("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
|
||||
}
|
||||
|
||||
const uint8_t *ResolveByGuid(int type, uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
@@ -155,7 +146,7 @@ int CountInGuidArray(const uint8_t *invMgr, int firstSlot, int lastSlot,
|
||||
// Layout: bag invMgr+0x00 = max slot count (the bag's size); bag
|
||||
// invMgr+0x04 = its GUID array. Same shape as the player invMgr.
|
||||
int CountInBankBags(int targetItemID, bool includeUses) {
|
||||
auto *playerInvMgr = PlayerInvMgr();
|
||||
auto *playerInvMgr = Unit::Identity::PlayerInventoryManager();
|
||||
if (playerInvMgr == nullptr)
|
||||
return 0;
|
||||
auto *playerGuidArray = *reinterpret_cast<const uint64_t *const *>(
|
||||
@@ -164,19 +155,14 @@ int CountInBankBags(int targetItemID, bool includeUses) {
|
||||
return 0;
|
||||
|
||||
int total = 0;
|
||||
using GetBagInvMgr_t = void *(__thiscall *)(void *bag);
|
||||
for (int slot = Offsets::INVMGR_BANK_BAG_FIRST_SLOT;
|
||||
slot <= Offsets::INVMGR_BANK_BAG_LAST_SLOT; slot++) {
|
||||
const uint8_t *bag = ResolveByGuid(Offsets::OBJ_TYPE_CONTAINER,
|
||||
playerGuidArray[slot]);
|
||||
if (bag == nullptr)
|
||||
continue;
|
||||
// vtable +0x10 returns the bag's own invMgr pointer
|
||||
auto *vtable = *reinterpret_cast<const uint8_t *const *>(bag);
|
||||
auto getInvMgr = reinterpret_cast<GetBagInvMgr_t>(
|
||||
*reinterpret_cast<const uintptr_t *>(vtable + 0x10));
|
||||
auto *bagInvMgr = static_cast<const uint8_t *>(
|
||||
getInvMgr(const_cast<uint8_t *>(bag)));
|
||||
auto *bagInvMgr =
|
||||
static_cast<const uint8_t *>(Item::Location::ContainerInventory(bag));
|
||||
if (bagInvMgr == nullptr)
|
||||
continue;
|
||||
const int bagSlots = static_cast<int>(*reinterpret_cast<const uint32_t *>(bagInvMgr));
|
||||
@@ -251,7 +237,7 @@ int __fastcall Script_C_Item_GetItemCount(void *L) {
|
||||
// Direct GUID-array reads — bypass the bank gate so this works
|
||||
// without the bank window having ever been opened in the
|
||||
// session.
|
||||
total += CountInGuidArray(PlayerInvMgr(),
|
||||
total += CountInGuidArray(Unit::Identity::PlayerInventoryManager(),
|
||||
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT,
|
||||
itemID, includeUses);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "item/Arg.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -245,7 +246,89 @@ static int __fastcall Script_C_Item_GetItemFamily(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Item.GetItemInfo(itemInfo)` — the full modern info tuple, sourced
|
||||
// entirely from the cached `ItemStats_C` record (+ the class/subclass/invtype
|
||||
// DBC name lookups this file already does for `GetItemInfoInstant`). Accepts
|
||||
// a numeric itemID, an `"item:NNN"` string, or a full chat link — same parser
|
||||
// as `GetItemInfoInstant`.
|
||||
//
|
||||
// Returns (18 values):
|
||||
// itemName, itemLink, itemQuality, itemLevel, itemMinLevel, itemType,
|
||||
// itemSubType, itemStackCount, itemEquipLoc, itemTexture, sellPrice,
|
||||
// classID, subclassID, bindType, expansionID, setID, isCraftingReagent,
|
||||
// itemDescription
|
||||
//
|
||||
// `expansionID` is always 0 (vanilla) and `isCraftingReagent` always false
|
||||
// (no such flag in 1.12's item data); `setID` is nil when the item has no
|
||||
// set. On a cache miss it warms the cache and returns nothing (nil) —
|
||||
// the value lands on a retry after `GET_ITEM_INFO_RECEIVED`,
|
||||
// matching the modern async contract.
|
||||
static int __fastcall Script_C_Item_GetItemInfo(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *record = FetchItemRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0; // nil this call; GET_ITEM_INFO_RECEIVED fires when ready
|
||||
}
|
||||
|
||||
auto u32 = [record](int off) {
|
||||
return *reinterpret_cast<const uint32_t *>(record + off);
|
||||
};
|
||||
const uint32_t quality = u32(Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
const uint32_t itemLevel = u32(Offsets::OFF_ITEMSTATS_ITEM_LEVEL);
|
||||
const uint32_t reqLevel = u32(Offsets::OFF_ITEMSTATS_REQUIRED_LEVEL);
|
||||
const uint32_t classID = u32(Offsets::OFF_ITEMSTATS_CLASS);
|
||||
const uint32_t subClassID = u32(Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
const uint32_t stackCount = u32(Offsets::OFF_ITEMSTATS_STACKABLE);
|
||||
const uint32_t invType = u32(Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
const uint32_t displayInfoID = u32(Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
const uint32_t sellPrice = u32(Offsets::OFF_ITEMSTATS_SELL_PRICE);
|
||||
const uint32_t bindType = u32(Offsets::OFF_ITEMSTATS_BONDING);
|
||||
const uint32_t setID = u32(Offsets::OFF_ITEMSTATS_ITEM_SET);
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
const char *description = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DESCRIPTION);
|
||||
|
||||
char iconPath[260];
|
||||
if (!BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
iconPath[0] = 0;
|
||||
char link[256];
|
||||
const bool haveLink = Item::Link::BasicFromItemID(static_cast<uint32_t>(itemID),
|
||||
link, sizeof(link));
|
||||
|
||||
Game::Lua::PushString(L, PushedOrEmpty(name)); // 1 itemName
|
||||
if (haveLink)
|
||||
Game::Lua::PushString(L, link); // 2 itemLink
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(quality)); // 3 itemQuality
|
||||
Game::Lua::PushNumber(L, static_cast<double>(itemLevel)); // 4 itemLevel
|
||||
Game::Lua::PushNumber(L, static_cast<double>(reqLevel)); // 5 itemMinLevel
|
||||
Game::Lua::PushString(L, LookupItemClassName(classID)); // 6 itemType
|
||||
Game::Lua::PushString(L, LookupItemSubClassName(classID, subClassID)); // 7 itemSubType
|
||||
Game::Lua::PushNumber(L, static_cast<double>(stackCount)); // 8 itemStackCount
|
||||
Game::Lua::PushString(L, LookupInvType(invType)); // 9 itemEquipLoc
|
||||
Game::Lua::PushString(L, iconPath); // 10 itemTexture
|
||||
Game::Lua::PushNumber(L, static_cast<double>(sellPrice)); // 11 sellPrice
|
||||
Game::Lua::PushNumber(L, static_cast<double>(classID)); // 12 classID
|
||||
Game::Lua::PushNumber(L, static_cast<double>(subClassID)); // 13 subclassID
|
||||
Game::Lua::PushNumber(L, static_cast<double>(bindType)); // 14 bindType
|
||||
Game::Lua::PushNumber(L, 0.0); // 15 expansionID (classic)
|
||||
if (setID != 0)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(setID)); // 16 setID
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushBool(L, false); // 17 isCraftingReagent
|
||||
Game::Lua::PushString(L, PushedOrEmpty(description)); // 18 itemDescription
|
||||
return 18;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemInfo", &Script_C_Item_GetItemInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemInfoInstant", &Script_GetItemInfoInstant);
|
||||
Game::Lua::RegisterGlobalFunction("GetItemIcon", &Script_GetItemIcon);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemIcon", &Script_C_Item_GetItemIcon);
|
||||
|
||||
+30
-11
@@ -16,6 +16,7 @@
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "../unit/Identity.h"
|
||||
#include "Arg.h"
|
||||
#include "ID.h"
|
||||
#include "Link.h"
|
||||
@@ -29,20 +30,10 @@ namespace {
|
||||
using GetItemBySlot_t = void *(__thiscall *)(void *thisInvMgr, int slot);
|
||||
using PackBagSlot_t = int(__fastcall *)(void *L, void **outInvMgr, int *outLinearSlot,
|
||||
int *outUnused);
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
void *ResolveActivePlayerInvMgr() {
|
||||
auto ResolveUnitToken =
|
||||
reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<uint8_t *>(ResolveUnitToken("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
|
||||
}
|
||||
|
||||
// Reads `loc.fieldName` and returns it as an int. Returns false via the
|
||||
// boolean result if the field is missing or non-numeric. Always leaves the
|
||||
// Lua stack as it found it.
|
||||
@@ -84,7 +75,7 @@ uint64_t ReadCGItemGUID(const uint8_t *item) {
|
||||
} // namespace
|
||||
|
||||
const uint8_t *ResolveEquipmentSlot(int slot1Based) {
|
||||
void *invMgr = ResolveActivePlayerInvMgr();
|
||||
void *invMgr = const_cast<uint8_t *>(Unit::Identity::PlayerInventoryManager());
|
||||
if (invMgr == nullptr)
|
||||
return nullptr;
|
||||
// GetItemBySlot expects 0-based linearized slot indices. The built-in
|
||||
@@ -95,6 +86,34 @@ const uint8_t *ResolveEquipmentSlot(int slot1Based) {
|
||||
return static_cast<const uint8_t *>(GetItemBySlot(invMgr, slot1Based - 1));
|
||||
}
|
||||
|
||||
void *ContainerInventory(const uint8_t *container) {
|
||||
if (container == nullptr)
|
||||
return nullptr;
|
||||
auto *vtable = *reinterpret_cast<const uint8_t *const *>(container);
|
||||
using GetInventory_t = void *(__thiscall *)(void *container);
|
||||
auto getInventory = *reinterpret_cast<const GetInventory_t *>(
|
||||
vtable + Offsets::OFF_CONTAINER_GET_INVENTORY);
|
||||
return getInventory(const_cast<uint8_t *>(container));
|
||||
}
|
||||
|
||||
void *EquippedBagInventory(int bagID) {
|
||||
if (bagID < 1 || bagID > 4)
|
||||
return nullptr;
|
||||
using GetBagGuid_t = uint64_t(__fastcall *)(uint32_t bagIndex0);
|
||||
auto getBagGuid =
|
||||
reinterpret_cast<GetBagGuid_t>(Offsets::FUN_GET_EQUIPPED_BAG_GUID);
|
||||
const uint64_t bagGuid = getBagGuid(static_cast<uint32_t>(bagID - 1));
|
||||
if (bagGuid == 0)
|
||||
return nullptr; // no bag equipped in that slot
|
||||
using ResolveByGUID_t = void *(__fastcall *)(int, const char *, uint32_t,
|
||||
uint32_t, int);
|
||||
auto resolve = reinterpret_cast<ResolveByGUID_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
|
||||
auto *container = static_cast<const uint8_t *>(
|
||||
resolve(Offsets::OBJ_TYPE_CONTAINER, "ItemMgr", static_cast<uint32_t>(bagGuid),
|
||||
static_cast<uint32_t>(bagGuid >> 32), 0x172));
|
||||
return ContainerInventory(container);
|
||||
}
|
||||
|
||||
// Mirrors `Item::Bag::ResolveBagInfo`'s slot-count derivation: backpack
|
||||
// is fixed at 16, equipped bags read `m_containerSlots` off the cache
|
||||
// record. Returns 0 for empty bag slots or out-of-range bagIDs.
|
||||
|
||||
@@ -69,6 +69,28 @@ const uint8_t *ResolveEquipmentSlot(int slot1Based);
|
||||
// guaranteed cached by the engine's bag-sync flow at login.
|
||||
int GetBagSlotCount(int bagID);
|
||||
|
||||
// Given a `CGContainer*` (an equipped bag or bank bag, resolved by GUID via
|
||||
// `FUN_OBJECT_RESOLVE_BY_GUID` with `OBJ_TYPE_CONTAINER`), returns its own
|
||||
// inventory-manager object via the container's vtable method at
|
||||
// `+OFF_CONTAINER_GET_INVENTORY`. The returned object has the same layout as
|
||||
// the player inventory manager: slot count at `+0x00`, flat GUID array at
|
||||
// `+OFF_INVMGR_GUID_ARRAY`. Null-safe (null container → null). Pure C — no
|
||||
// Lua stack, safe from any context. Consolidates the vtable dispatch that was
|
||||
// hand-rolled across the bag/bank walks in `item/Count`, `item/AverageLevel`,
|
||||
// `equipmentset/Locations`, and `item/NewItems`.
|
||||
void *ContainerInventory(const uint8_t *container);
|
||||
|
||||
// Returns the inventory object of the equipped bag in player bag slot `bagID`
|
||||
// (1..4): its container GUID via `FUN_GET_EQUIPPED_BAG_GUID`, resolved to the
|
||||
// CGContainer, then its inventory via `ContainerInventory`. The result has the
|
||||
// player-invMgr shape (slot count at `+0x00`, GUID array at
|
||||
// `+OFF_INVMGR_GUID_ARRAY`) and is what `GetItemBySlot` indexes with a 0-based
|
||||
// slot. Returns null for `bagID` outside 1..4 or when no bag is equipped
|
||||
// there. Pure C — no Lua stack. (The backpack, `bagID` 0, has no container
|
||||
// object; index it directly off the player inventory manager at
|
||||
// `BACKPACK_LINEAR_BASE + slot - 1`.)
|
||||
void *EquippedBagInventory(int bagID);
|
||||
|
||||
// GUID-walk result. `equipmentSlotIndex != 0` means the item was
|
||||
// found in a character-pane equipment slot; otherwise it's in
|
||||
// `bagID`/`slotIndex`. `item` always points to the found CGItem
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_NewItems` — the modern "new item glow" bookkeeping system. Retail
|
||||
// flags items freshly added to your bags as "new" (the sparkle/flash on
|
||||
// the bag button) and clears the flag once you've acknowledged them.
|
||||
// Vanilla 1.12 never had the feature — no DBC, no server data, no engine
|
||||
// bit; it's pure client-side bookkeeping over bag contents. So we add it,
|
||||
// entirely in C++ the same way retail's client owns it — no addon Lua.
|
||||
//
|
||||
// Public surface (matches retail exactly):
|
||||
// C_NewItems.IsNewItem(bagID, slotIndex) -> bool
|
||||
// C_NewItems.RemoveNewItem(bagID, slotIndex)
|
||||
// C_NewItems.ClearAll()
|
||||
//
|
||||
// Newness is keyed on each item's **instance GUID** (read off the CGItem),
|
||||
// not on (bagID, slotIndex). That's what makes a flag survive the player
|
||||
// rearranging a bag — exactly like retail, and impossible for a pure-Lua
|
||||
// addon (vanilla exposes no per-instance GUID to Lua).
|
||||
//
|
||||
// The feed is event-driven, not polled: it hangs off `Bag::UpdateDelayed`,
|
||||
// which co-hooks the engine's three `BAG_UPDATE` fire sites and signals once
|
||||
// per frame in which a bag actually changed. On that signal we scan bags
|
||||
// 0..4, diff the resident item GUIDs against the previous scan, flag anything
|
||||
// newly acquired, and prune flags for items that left. `BAG_NEW_ITEMS_UPDATED`
|
||||
// (no payload — matching retail) fires whenever the new-item set changes. So
|
||||
// nothing runs on quiet frames.
|
||||
//
|
||||
// The one exception is establishing the login baseline: a lightweight
|
||||
// WorldTick handler waits ~1.5s after the player is first resolvable (long
|
||||
// enough for the initial bag population to finish), takes a single scan as the
|
||||
// baseline, and then goes idle — items already owned at login are therefore
|
||||
// never flagged. It re-arms on player-GUID change (relog / character switch).
|
||||
//
|
||||
// The "already owned" baseline spans **everything the player owns** — bags,
|
||||
// worn equipment (paperdoll + equipped bags), AND the bank (main + bank bags)
|
||||
// — even though only *bag* items are ever flagged new. That's what stops a
|
||||
// move INTO a bag from any of those (unequip, or a pull from the bank) from
|
||||
// looking like a fresh loot: the item's GUID was already in the owned set, so
|
||||
// the bag diff treats it as a move. The bank is reachable without the bank
|
||||
// window open because its GUIDs are read straight from the invMgr array (the
|
||||
// same gate-bypass `C_Item.GetItemCount` uses; the array is server-populated
|
||||
// at login).
|
||||
//
|
||||
// Bag enumeration is pure C++: it replicates what `PackBagSlot` does
|
||||
// internally (see Offsets.h). The backpack is indexed straight off the
|
||||
// player inventory manager; each equipped bag 1..4 is resolved to its
|
||||
// container object and inventory via the same GUID getter + vtable call the
|
||||
// engine uses, then walked with `GetItemBySlot`. None of the Lua-stack-bound
|
||||
// helpers are touched, so the scan is safe from the frame-loop tick.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "bag/UpdateDelayed.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/Location.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Item::NewItems {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kEvtNewItems = "BAG_NEW_ITEMS_UPDATED";
|
||||
const Event::Custom::AutoReserve _r{kEvtNewItems};
|
||||
|
||||
// Sized for the whole owned universe the baseline tracks — backpack + four
|
||||
// bags + 19 worn slots + 4 equipped bags + main bank (24) + six bank bags.
|
||||
// A full inventory-and-bank can push ~300, so 512 leaves generous headroom.
|
||||
// Overflow silently drops (AddUnique bails at the cap) — a dropped GUID just
|
||||
// can't be flagged/recognized, never a crash.
|
||||
constexpr int kMaxItems = 512;
|
||||
|
||||
// Delay after the player is first resolvable before the baseline is taken,
|
||||
// giving the initial bag population time to finish. Wall-clock (via the
|
||||
// engine ms counter) so it's independent of frame rate.
|
||||
constexpr uint32_t kSettleMs = 1500;
|
||||
|
||||
uint64_t g_seen[kMaxItems]; // all owned GUIDs (bags + equipment + bank) at the last scan
|
||||
int g_seenCount = 0;
|
||||
uint64_t g_new[kMaxItems]; // subset currently flagged "new"
|
||||
int g_newCount = 0;
|
||||
bool g_seeded = false; // true once the login baseline has been taken
|
||||
uint64_t g_playerGuid = 0; // re-arm the baseline when this changes (relog)
|
||||
uint32_t g_loginMs = 0; // engine ms at which the current player resolved
|
||||
|
||||
uint32_t NowMs() {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
return reinterpret_cast<TickCount_t>(Offsets::FUN_OS_TICKCOUNT_MS)();
|
||||
}
|
||||
|
||||
bool Contains(const uint64_t *arr, int count, uint64_t guid) {
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (arr[i] == guid)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AddUnique(uint64_t *arr, int *count, uint64_t guid) {
|
||||
if (Contains(arr, *count, guid) || *count >= kMaxItems)
|
||||
return false;
|
||||
arr[(*count)++] = guid;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RemoveFrom(uint64_t *arr, int *count, uint64_t guid) {
|
||||
for (int i = 0; i < *count; ++i) {
|
||||
if (arr[i] == guid) {
|
||||
arr[i] = arr[--(*count)];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t ItemGUID(const uint8_t *item) {
|
||||
if (item == nullptr)
|
||||
return 0;
|
||||
auto *instance = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
if (instance == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
instance + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
}
|
||||
|
||||
// --- pure-C++ bag enumeration (no Lua stack) -----------------------------
|
||||
|
||||
using GetItemBySlot_t = void *(__thiscall *)(void *inventory, int slot0Based);
|
||||
|
||||
// Collects the instance GUIDs of every item in bags 0..4 into `out`.
|
||||
// Returns the count. Pure C++ — safe to call off the frame-loop tick.
|
||||
int ScanBags(const uint8_t *player, uint64_t *out) {
|
||||
auto getItemBySlot =
|
||||
reinterpret_cast<GetItemBySlot_t>(Offsets::FUN_ITEMMGR_GET_ITEM_BY_SLOT);
|
||||
int count = 0;
|
||||
|
||||
// Backpack (bagID 0): indexed directly off the player inventory manager
|
||||
// at linear slot (S-1) + 23. GetItemBySlot only reads the manager, so the
|
||||
// const_cast to satisfy its `this` parameter is safe.
|
||||
auto *invMgr = const_cast<uint8_t *>(player) + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
|
||||
const int backpackSlots = Item::Location::GetBagSlotCount(0);
|
||||
for (int slot = 1; slot <= backpackSlots; ++slot) {
|
||||
auto *item = static_cast<const uint8_t *>(
|
||||
getItemBySlot(invMgr, Offsets::BACKPACK_LINEAR_BASE + slot - 1));
|
||||
const uint64_t guid = ItemGUID(item);
|
||||
if (guid != 0)
|
||||
AddUnique(out, &count, guid);
|
||||
}
|
||||
|
||||
// Equipped bags 1..4: resolve each bag's inventory object, then walk it.
|
||||
for (int bagID = 1; bagID <= 4; ++bagID) {
|
||||
const int slots = Item::Location::GetBagSlotCount(bagID);
|
||||
if (slots <= 0)
|
||||
continue;
|
||||
void *bagInv = Item::Location::EquippedBagInventory(bagID);
|
||||
if (bagInv == nullptr)
|
||||
continue;
|
||||
for (int slot = 1; slot <= slots; ++slot) {
|
||||
auto *item = static_cast<const uint8_t *>(getItemBySlot(bagInv, slot - 1));
|
||||
const uint64_t guid = ItemGUID(item);
|
||||
if (guid != 0)
|
||||
AddUnique(out, &count, guid);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Appends GUIDs from an invMgr's GUID array (the flat `+OFF_INVMGR_GUID_ARRAY`
|
||||
// list `GetItemBySlot` indexes) over linear slots [first, last]. Reading the
|
||||
// array directly is how we reach BANK slots without the bank window open —
|
||||
// `GetItemBySlot` gates slots 39..68 on a live banker GUID, but the array
|
||||
// itself is server-populated at login (same bypass `C_Item.GetItemCount`
|
||||
// uses). We only need the GUID here, not the resolved object.
|
||||
void AppendGuidArray(const uint8_t *invMgr, int first, int last, uint64_t *out,
|
||||
int *count) {
|
||||
if (invMgr == nullptr)
|
||||
return;
|
||||
auto *arr = *reinterpret_cast<const uint64_t *const *>(
|
||||
invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (arr == nullptr)
|
||||
return;
|
||||
for (int slot = first; slot <= last; ++slot)
|
||||
if (arr[slot] != 0)
|
||||
AddUnique(out, count, arr[slot]);
|
||||
}
|
||||
|
||||
using ResolveByGuid_t = void *(__fastcall *)(int type, const char *debugName,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
int priority);
|
||||
const uint8_t *ResolveByGuid(int type, uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveByGuid_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
|
||||
return static_cast<const uint8_t *>(
|
||||
fn(type, "ItemMgr", static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0x172));
|
||||
}
|
||||
|
||||
// Appends every item the player owns that ISN'T in bags 0..4 — worn
|
||||
// equipment (paperdoll 1..19 + the four equipped-bag containers 20..23),
|
||||
// the main bank (24 slots), and each bank bag's contents. Used only to widen
|
||||
// the "already owned" baseline so a move INTO a bag from any of these
|
||||
// (unequip, or a pull from the bank) reads as a move, not a fresh loot. We
|
||||
// never *flag* these as new — this only feeds the seen-set. Pure C++ (no Lua
|
||||
// stack) — safe off the frame tick.
|
||||
void AppendNonBagOwned(uint64_t *out, int *count) {
|
||||
// Worn equipment + equipped bags: GetItemBySlot works for these (no
|
||||
// banker gate), so go through the normal per-slot resolver.
|
||||
for (int slot = Offsets::EQUIPMENT_SLOT_FIRST; slot <= Offsets::INVSLOT_BAG1 + 3;
|
||||
++slot) {
|
||||
const uint64_t guid = ItemGUID(Item::Location::ResolveEquipmentSlot(slot));
|
||||
if (guid != 0)
|
||||
AddUnique(out, count, guid);
|
||||
}
|
||||
|
||||
const uint8_t *playerInvMgr = Unit::Identity::PlayerInventoryManager();
|
||||
if (playerInvMgr == nullptr)
|
||||
return;
|
||||
// Main bank slots 39..62 live in the player invMgr's own GUID array.
|
||||
AppendGuidArray(playerInvMgr, Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT, out, count);
|
||||
// Bank bags (containers at player slots 63..68) each carry their own
|
||||
// invMgr; resolve the container, then walk its GUID array.
|
||||
auto *playerArr = *reinterpret_cast<const uint64_t *const *>(
|
||||
playerInvMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (playerArr == nullptr)
|
||||
return;
|
||||
for (int slot = Offsets::INVMGR_BANK_BAG_FIRST_SLOT;
|
||||
slot <= Offsets::INVMGR_BANK_BAG_LAST_SLOT; ++slot) {
|
||||
const uint8_t *bag = ResolveByGuid(Offsets::OBJ_TYPE_CONTAINER, playerArr[slot]);
|
||||
if (bag == nullptr)
|
||||
continue;
|
||||
auto *bagInvMgr =
|
||||
static_cast<const uint8_t *>(Item::Location::ContainerInventory(bag));
|
||||
if (bagInvMgr == nullptr)
|
||||
continue;
|
||||
const int bagSlots = static_cast<int>(*reinterpret_cast<const uint32_t *>(bagInvMgr));
|
||||
if (bagSlots > 0)
|
||||
AppendGuidArray(bagInvMgr, 0, bagSlots - 1, out, count);
|
||||
}
|
||||
}
|
||||
|
||||
void FireChanged() {
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtNewItems), "");
|
||||
}
|
||||
|
||||
void CopyToSeen(const uint64_t *current, int count) {
|
||||
for (int i = 0; i < count; ++i)
|
||||
g_seen[i] = current[i];
|
||||
g_seenCount = count;
|
||||
}
|
||||
|
||||
// Login-baseline handler. Runs every frame but does no work once the
|
||||
// baseline is taken — it only watches for (re)login and, after a short
|
||||
// settle, takes the single authoritative baseline scan. Steady state is just
|
||||
// a GUID read + `if (g_seeded) return`.
|
||||
void OnTick() {
|
||||
const uint64_t guid = Unit::Identity::PlayerGuid();
|
||||
if (guid == 0)
|
||||
return; // not in world yet
|
||||
|
||||
// Fresh login / character switch: drop all state and re-arm the settle.
|
||||
if (guid != g_playerGuid) {
|
||||
g_playerGuid = guid;
|
||||
g_newCount = 0;
|
||||
g_seenCount = 0;
|
||||
g_seeded = false;
|
||||
g_loginMs = NowMs();
|
||||
}
|
||||
|
||||
if (g_seeded)
|
||||
return; // baseline established; the bag-change signal drives the rest
|
||||
if (NowMs() - g_loginMs < kSettleMs)
|
||||
return; // let the initial bag population finish
|
||||
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return; // not resolvable yet — retry next tick
|
||||
|
||||
// Everything currently owned — bags AND worn equipment — becomes the
|
||||
// baseline; nothing owned at login is "new". Including equipment means a
|
||||
// later unequip (paperdoll → bag) is recognized as a move, not a loot.
|
||||
g_seenCount = ScanBags(player, g_seen);
|
||||
AppendNonBagOwned(g_seen, &g_seenCount);
|
||||
g_newCount = 0;
|
||||
g_seeded = true;
|
||||
}
|
||||
|
||||
// Authoritative bag-change handler — fires only on frames where the engine
|
||||
// actually changed a bag (via Bag::UpdateDelayed). Diffs the current bag set
|
||||
// against the baseline, flags newly-acquired items, prunes departed ones.
|
||||
void OnBagChanged() {
|
||||
if (!g_seeded)
|
||||
return; // pre-baseline changes are folded into the login baseline
|
||||
const uint64_t guid = Unit::Identity::PlayerGuid();
|
||||
if (guid == 0 || guid != g_playerGuid)
|
||||
return; // mid-relog; OnTick re-arms the baseline
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return;
|
||||
|
||||
uint64_t currentBags[kMaxItems];
|
||||
const int bagCount = ScanBags(player, currentBags);
|
||||
|
||||
bool changed = false;
|
||||
// Drop flags for items no longer in the bags (used, sold, equipped, banked).
|
||||
for (int i = 0; i < g_newCount;) {
|
||||
if (!Contains(currentBags, bagCount, g_new[i])) {
|
||||
g_new[i] = g_new[--g_newCount];
|
||||
changed = true;
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < bagCount; ++i) {
|
||||
if (!Contains(g_seen, g_seenCount, currentBags[i]))
|
||||
changed |= AddUnique(g_new, &g_newCount, currentBags[i]);
|
||||
}
|
||||
CopyToSeen(currentBags, bagCount);
|
||||
AppendNonBagOwned(g_seen, &g_seenCount);
|
||||
|
||||
if (changed)
|
||||
FireChanged();
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
const Bag::UpdateDelayed::AutoSubscribe _bagChange{&OnBagChanged};
|
||||
|
||||
// --- C_NewItems Lua surface ----------------------------------------------
|
||||
|
||||
// Resolves a `(bagID, slotIndex)` Lua-arg pair to the item's instance GUID,
|
||||
// or 0 if the args are missing/non-numeric or the slot is empty. Uses the
|
||||
// Lua-context bag resolver — fine here, these run as Lua C functions.
|
||||
uint64_t SlotGUID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2))
|
||||
return 0;
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
return ItemGUID(Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
}
|
||||
|
||||
int __fastcall Script_IsNewItem(void *L) {
|
||||
const uint64_t guid = SlotGUID(L);
|
||||
Game::Lua::PushBool(L, guid != 0 && Contains(g_new, g_newCount, guid));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_RemoveNewItem(void *L) {
|
||||
const uint64_t guid = SlotGUID(L);
|
||||
if (guid != 0 && RemoveFrom(g_new, &g_newCount, guid))
|
||||
FireChanged();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_ClearAll(void *L) {
|
||||
(void)L;
|
||||
if (g_newCount > 0) {
|
||||
g_newCount = 0;
|
||||
FireChanged();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_NewItems", "IsNewItem", &Script_IsNewItem);
|
||||
Game::Lua::RegisterTableFunction("C_NewItems", "RemoveNewItem", &Script_RemoveNewItem);
|
||||
Game::Lua::RegisterTableFunction("C_NewItems", "ClearAll", &Script_ClearAll);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Item::NewItems
|
||||
+39
-28
@@ -279,6 +279,15 @@ void AddByKey(Accum *acc, const char *key, long delta) {
|
||||
}
|
||||
}
|
||||
|
||||
long Value(const Accum *acc, const char *key) {
|
||||
if (acc == nullptr || key == nullptr)
|
||||
return 0;
|
||||
for (int i = 0; i < kCount; ++i)
|
||||
if (std::strcmp(acc[i].key, key) == 0)
|
||||
return acc[i].value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint8_t *FetchRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
@@ -306,6 +315,34 @@ double ComputeDPS(const uint8_t *record) {
|
||||
return avgTotal / (static_cast<double>(delayMs) / 1000.0);
|
||||
}
|
||||
|
||||
void ApplyEnchant(Accum *acc, uint32_t enchantID, long sign) {
|
||||
if (enchantID == 0)
|
||||
return;
|
||||
const uint8_t *en = DBC::Record(Offsets::VAR_SPELLITEMENCHANT_RECORDS,
|
||||
Offsets::VAR_SPELLITEMENCHANT_COUNT, enchantID);
|
||||
if (en == nullptr)
|
||||
return;
|
||||
auto type = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_TYPE);
|
||||
auto amount = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_AMOUNT);
|
||||
auto arg = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_ARG);
|
||||
for (int i = 0; i < 3; ++i) { // SpellItemEnchantment has 3 effect slots
|
||||
switch (type[i]) {
|
||||
case 3: // equip spell — stat/resist value lives in the spell
|
||||
AddSpellStatAuras(acc, arg[i], sign);
|
||||
break;
|
||||
case 4: // resistance — arg = school index (0 = armor)
|
||||
AddByKey(acc, ResistKey(arg[i]), sign * static_cast<long>(amount[i]));
|
||||
break;
|
||||
case 5: // direct stat — arg = ItemModType, amount = value
|
||||
AddByKey(acc, StatKeyForItemModType(static_cast<uint32_t>(arg[i])),
|
||||
sign * static_cast<long>(amount[i]));
|
||||
break;
|
||||
default: // 1 proc / 2 weapon-damage / 6 totem / 7 use spell
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyRandomSuffix(Accum *acc, int suffixID, long sign) {
|
||||
if (suffixID <= 0)
|
||||
return;
|
||||
@@ -316,34 +353,8 @@ void ApplyRandomSuffix(Accum *acc, int suffixID, long sign) {
|
||||
return;
|
||||
auto enchants = reinterpret_cast<const uint32_t *>(
|
||||
rp + Offsets::OFF_ITEMRANDOMPROP_ENCHANT);
|
||||
for (int s = 0; s < Offsets::ITEMRANDOMPROP_ENCHANT_SLOT_COUNT; ++s) {
|
||||
const uint32_t enchantID = enchants[s];
|
||||
if (enchantID == 0)
|
||||
continue;
|
||||
const uint8_t *en = DBC::Record(Offsets::VAR_SPELLITEMENCHANT_RECORDS,
|
||||
Offsets::VAR_SPELLITEMENCHANT_COUNT, enchantID);
|
||||
if (en == nullptr)
|
||||
continue;
|
||||
auto type = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_TYPE);
|
||||
auto amount = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_AMOUNT);
|
||||
auto arg = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_ARG);
|
||||
for (int i = 0; i < 3; ++i) { // SpellItemEnchantment has 3 effect slots
|
||||
switch (type[i]) {
|
||||
case 3: // equip spell — stat/resist value lives in the spell
|
||||
AddSpellStatAuras(acc, arg[i], sign);
|
||||
break;
|
||||
case 4: // resistance — arg = school index (0 = armor)
|
||||
AddByKey(acc, ResistKey(arg[i]), sign * static_cast<long>(amount[i]));
|
||||
break;
|
||||
case 5: // direct stat — arg = ItemModType, amount = value
|
||||
AddByKey(acc, StatKeyForItemModType(static_cast<uint32_t>(arg[i])),
|
||||
sign * static_cast<long>(amount[i]));
|
||||
break;
|
||||
default: // 1 proc / 2 weapon-damage / 6 totem / 7 use spell
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int s = 0; s < Offsets::ITEMRANDOMPROP_ENCHANT_SLOT_COUNT; ++s)
|
||||
ApplyEnchant(acc, enchants[s], sign);
|
||||
}
|
||||
|
||||
void AccumulateRecord(Accum *acc, const uint8_t *record, long sign) {
|
||||
|
||||
@@ -51,6 +51,9 @@ void Init(Accum *acc);
|
||||
// a zero delta). Linear scan — the table is small and fixed.
|
||||
void AddByKey(Accum *acc, const char *key, long delta);
|
||||
|
||||
// Read the accumulated value for `key` (0 for an unknown key). Linear scan.
|
||||
long Value(const Accum *acc, const char *key);
|
||||
|
||||
// Passive cache peek for an item's `ItemStats_C` record. Does NOT warm
|
||||
// the cache (returns nullptr for an uncached item) — see the item-cache
|
||||
// race note in CLAUDE.md.
|
||||
@@ -66,6 +69,13 @@ void AccumulateRecord(Accum *acc, const uint8_t *record, long sign);
|
||||
// suffixID <= 0.
|
||||
void ApplyRandomSuffix(Accum *acc, int suffixID, long sign);
|
||||
|
||||
// Fold a single SpellItemEnchantment record (by enchant ID) into `acc`,
|
||||
// scaled by `sign`. Handles equip-spell (type 3), resistance (4), and
|
||||
// direct-stat (5) enchant effects. Shared by `ApplyRandomSuffix` (a suffix's
|
||||
// enchant slots) and callers reading an item's *applied* permanent/temporary
|
||||
// enchants off its CGItem descriptor. No-op for enchantID 0.
|
||||
void ApplyEnchant(Accum *acc, uint32_t enchantID, long sign);
|
||||
|
||||
// Weapon DPS = average damage / swing time, or 0 for non-weapons.
|
||||
double ComputeDPS(const uint8_t *record);
|
||||
|
||||
|
||||
+8
-13
@@ -16,6 +16,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Location.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -23,7 +24,6 @@ namespace Item::Swap {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using SwapFn_t = void(__thiscall *)(
|
||||
void *player,
|
||||
uint32_t srcItemLo, uint32_t srcItemHi,
|
||||
@@ -33,11 +33,6 @@ using SwapFn_t = void(__thiscall *)(
|
||||
uint32_t dstLinearSlot,
|
||||
int flag);
|
||||
|
||||
void *ResolvePlayer() {
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return fn("player");
|
||||
}
|
||||
|
||||
// Reads the 64-bit GUID stored at instance-block offset 0 of any
|
||||
// CGObject-derived pointer (CGItem, CGPlayer, CGContainer all use
|
||||
// the same +0x8 → instance block → +0x00 GUID layout).
|
||||
@@ -98,7 +93,7 @@ bool EncodeBagSlot(int bagID, int slotInBag,
|
||||
if (slotInBag < 1)
|
||||
return false;
|
||||
if (bagID == 0) {
|
||||
void *player = ResolvePlayer();
|
||||
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
if (!ReadGuid(player, containerLo, containerHi))
|
||||
@@ -119,7 +114,7 @@ bool EncodeBagSlot(int bagID, int slotInBag,
|
||||
if (bagID == -1) {
|
||||
if (slotInBag > BANK_MAIN_NUM_SLOTS)
|
||||
return false;
|
||||
void *player = ResolvePlayer();
|
||||
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
if (!ReadGuid(player, containerLo, containerHi))
|
||||
@@ -152,7 +147,7 @@ bool SendSwap(const void *srcItem,
|
||||
if (srcItem == nullptr)
|
||||
return false;
|
||||
|
||||
void *player = ResolvePlayer();
|
||||
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
|
||||
@@ -187,7 +182,7 @@ bool FromPaperdoll(const void *cgItem, int srcPaperdollSlot, int dstPaperdollSlo
|
||||
if (srcPaperdollSlot < 1 || srcPaperdollSlot > 19)
|
||||
return false;
|
||||
|
||||
void *player = ResolvePlayer();
|
||||
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
uint32_t playerLo = 0, playerHi = 0;
|
||||
@@ -205,7 +200,7 @@ bool ToBag(const void *cgItem, int srcPaperdollSlot, int dstBagID, int dstSlotIn
|
||||
if (cgItem == nullptr)
|
||||
return false;
|
||||
|
||||
void *player = ResolvePlayer();
|
||||
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
uint32_t playerLo = 0, playerHi = 0;
|
||||
@@ -272,7 +267,7 @@ bool MoveCount(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot, int co
|
||||
if (count == srcStack)
|
||||
return Containers(L, srcBag, srcSlot, dstBag, dstSlot);
|
||||
|
||||
void *player = ResolvePlayer();
|
||||
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
|
||||
@@ -308,7 +303,7 @@ bool Containers(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot) {
|
||||
if (srcItem == nullptr)
|
||||
return false; // empty source slot
|
||||
|
||||
void *player = ResolvePlayer();
|
||||
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "map/Area.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
|
||||
namespace Map::Area {
|
||||
|
||||
int RowForAreaID(uint32_t areaID) {
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_AREA_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec != nullptr &&
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_WMA_AREA_ID) ==
|
||||
areaID)
|
||||
return id;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int CurrentViewRow() {
|
||||
const int continent =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_CONTINENT_INDEX);
|
||||
if (continent < 0)
|
||||
return *reinterpret_cast<const int *>(
|
||||
Offsets::VAR_WORLDMAP_DEFAULT_AREA_ROW);
|
||||
const uint8_t *blob = *reinterpret_cast<const uint8_t *const *>(
|
||||
Offsets::VAR_WORLDMAP_CONTINENT_DATA);
|
||||
if (blob == nullptr)
|
||||
return -1;
|
||||
const uint8_t *entry = blob + continent * Offsets::WORLDMAP_CONTINENT_STRIDE;
|
||||
const int zone =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_ZONE_INDEX);
|
||||
if (zone < 0)
|
||||
return *reinterpret_cast<const int *>(entry + 0x04);
|
||||
const int *zoneRows = *reinterpret_cast<const int *const *>(entry + 0x10);
|
||||
return (zoneRows != nullptr) ? zoneRows[zone] : -1;
|
||||
}
|
||||
|
||||
namespace {
|
||||
double FloatField(const uint8_t *rec, int off) {
|
||||
return *reinterpret_cast<const float *>(rec + off);
|
||||
}
|
||||
|
||||
// Vanilla zone world-map canvas size (WorldMapDetailFrame); WorldMapOverlay
|
||||
// hit rects live in this pixel space.
|
||||
constexpr double kZoneCanvasW = 1002.0;
|
||||
constexpr double kZoneCanvasH = 668.0;
|
||||
|
||||
// How deep inside its containing WorldMapOverlay hit rect the map-percent point
|
||||
// (0..100) sits on WMA row `wmaRow` — the max, over that zone's overlays whose
|
||||
// hit rect encloses the point, of the normalized margin to the nearest overlay
|
||||
// edge (0 at the edge .. 0.5 at center). Returns -1 when the point is on none
|
||||
// of the zone's overlays.
|
||||
//
|
||||
// This is the runtime analog of an ADT area-grid lookup. WorldMapArea rects are
|
||||
// loose overlapping boxes; the overlay hit rects trace the real landmass, so
|
||||
// "which zone is the point drawn on" disambiguates them (Un'Goro's crater vs
|
||||
// Thousand Needles' empty overlap). But hit rects are themselves rectangles
|
||||
// that overlap at borders, so the DEPTH matters: a border node deep inside its
|
||||
// own zone's overlay beats one merely clipping a neighbor's overlay edge
|
||||
// (Nijel's Point is deep in Desolace's "Nijel's Point" overlay but only nicks
|
||||
// the edge of Stonetalon's Charred Vale box).
|
||||
double OverlayMargin(int wmaRow, double mapXpct, double mapYpct) {
|
||||
const double cx = mapXpct / 100.0 * kZoneCanvasW;
|
||||
const double cy = mapYpct / 100.0 * kZoneCanvasH;
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_OVERLAY_COUNT);
|
||||
double best = -1.0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_OVERLAY_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_OVERLAY_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr ||
|
||||
*reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_WORLDMAP_AREA) !=
|
||||
wmaRow)
|
||||
continue;
|
||||
const int hl = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_HITRECT_LEFT);
|
||||
const int hr = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_HITRECT_RIGHT);
|
||||
const int ht = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_HITRECT_TOP);
|
||||
const int hb = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_HITRECT_BOTTOM);
|
||||
const double sw = hr - hl, sh = hb - ht;
|
||||
if (sw <= 0.0 || sh <= 0.0)
|
||||
continue; // no / degenerate hit rect
|
||||
if (cx < hl || cx > hr || cy < ht || cy > hb)
|
||||
continue; // point outside this overlay
|
||||
const double mX = (cx - hl < hr - cx ? cx - hl : hr - cx) / sw;
|
||||
const double mY = (cy - ht < hb - cy ? cy - ht : hb - cy) / sh;
|
||||
const double m = mX < mY ? mX : mY;
|
||||
if (m > best)
|
||||
best = m;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ZonePercent(int mapID, float x, float y, int *outAreaID, double *outMapX,
|
||||
double *outMapY) {
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
// Two candidate tracks over the zones whose rect contains the point:
|
||||
// land* — the point is on the zone's drawn landmass (an overlay hit rect
|
||||
// encloses it): authoritative, wins whenever any zone qualifies.
|
||||
// Scored by how DEEP inside the overlay the point sits, so a
|
||||
// border node deep in its own zone's overlay beats one nicking a
|
||||
// neighbor's overlay edge.
|
||||
// box* — bounding-box only: the fallback when the point is on no zone's
|
||||
// overlay (map gaps / overlay-less custom zones), scored by the
|
||||
// most-interior zone-rect margin.
|
||||
bool haveLand = false, haveBox = false;
|
||||
double landScore = -1.0, boxMargin = 0.0;
|
||||
int landArea = 0, boxArea = 0;
|
||||
double landX = 0.0, landY = 0.0, boxX = 0.0, boxY = 0.0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_AREA_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
if (*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_MAP_ID) != mapID)
|
||||
continue;
|
||||
const int areaID =
|
||||
*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_AREA_ID);
|
||||
if (areaID == 0)
|
||||
continue; // continent-spanning row, not a zone
|
||||
|
||||
const double left = FloatField(rec, Offsets::OFF_WMA_LOC_LEFT);
|
||||
const double right = FloatField(rec, Offsets::OFF_WMA_LOC_RIGHT);
|
||||
const double top = FloatField(rec, Offsets::OFF_WMA_LOC_TOP);
|
||||
const double bottom = FloatField(rec, Offsets::OFF_WMA_LOC_BOTTOM);
|
||||
const double spanX = top - bottom; // > 0
|
||||
const double spanY = left - right; // > 0
|
||||
if (spanX <= 0.0 || spanY <= 0.0)
|
||||
continue; // degenerate / unmapped zone
|
||||
|
||||
if (x < bottom || x > top || y < right || y > left)
|
||||
continue; // point outside this zone's rect
|
||||
|
||||
// mapX = horizontal (world Y), mapY = vertical (world X).
|
||||
const double mapX = (left - y) / spanY * 100.0;
|
||||
const double mapY = (top - x) / spanX * 100.0;
|
||||
const double marginX = (x - bottom < top - x ? x - bottom : top - x) / spanX;
|
||||
const double marginY = (y - right < left - y ? y - right : left - y) / spanY;
|
||||
const double margin = marginX < marginY ? marginX : marginY;
|
||||
|
||||
if (!haveBox || margin > boxMargin) {
|
||||
haveBox = true;
|
||||
boxMargin = margin;
|
||||
boxArea = areaID;
|
||||
boxX = mapX;
|
||||
boxY = mapY;
|
||||
}
|
||||
const double overlayMargin = OverlayMargin(id, mapX, mapY);
|
||||
if (overlayMargin >= 0.0 && (!haveLand || overlayMargin > landScore)) {
|
||||
haveLand = true;
|
||||
landScore = overlayMargin;
|
||||
landArea = areaID;
|
||||
landX = mapX;
|
||||
landY = mapY;
|
||||
}
|
||||
}
|
||||
|
||||
if (haveLand) {
|
||||
*outAreaID = landArea;
|
||||
*outMapX = landX;
|
||||
*outMapY = landY;
|
||||
return true;
|
||||
}
|
||||
if (haveBox) {
|
||||
*outAreaID = boxArea;
|
||||
*outMapX = boxX;
|
||||
*outMapY = boxY;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PercentInZone(int areaID, float x, float y, double *outMapX, double *outMapY) {
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_AREA_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
if (*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_AREA_ID) != areaID)
|
||||
continue;
|
||||
const double left = FloatField(rec, Offsets::OFF_WMA_LOC_LEFT);
|
||||
const double right = FloatField(rec, Offsets::OFF_WMA_LOC_RIGHT);
|
||||
const double top = FloatField(rec, Offsets::OFF_WMA_LOC_TOP);
|
||||
const double bottom = FloatField(rec, Offsets::OFF_WMA_LOC_BOTTOM);
|
||||
const double spanX = top - bottom;
|
||||
const double spanY = left - right;
|
||||
if (spanX <= 0.0 || spanY <= 0.0)
|
||||
continue; // degenerate row — keep scanning for a usable one
|
||||
if (x < bottom || x > top || y < right || y > left)
|
||||
return false; // point outside this zone's rect — not in this zone
|
||||
*outMapX = (left - y) / spanY * 100.0; // horizontal (world Y)
|
||||
*outMapY = (top - x) / spanX * 100.0; // vertical (world X)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ContinentPercent(int mapID, float x, float y, double *outPx, double *outPy) {
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_AREA_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
if (*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_MAP_ID) != mapID)
|
||||
continue;
|
||||
if (*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_AREA_ID) != 0)
|
||||
continue; // want the continent-spanning row
|
||||
|
||||
const double left = FloatField(rec, Offsets::OFF_WMA_LOC_LEFT);
|
||||
const double right = FloatField(rec, Offsets::OFF_WMA_LOC_RIGHT);
|
||||
const double top = FloatField(rec, Offsets::OFF_WMA_LOC_TOP);
|
||||
const double bottom = FloatField(rec, Offsets::OFF_WMA_LOC_BOTTOM);
|
||||
const double spanX = top - bottom;
|
||||
const double spanY = left - right;
|
||||
if (spanX <= 0.0 || spanY <= 0.0)
|
||||
continue; // stray zero-rect continent row (e.g. "World") — skip
|
||||
|
||||
*outPx = (left - y) / spanY; // horizontal (world Y)
|
||||
*outPy = (top - x) / spanX; // vertical (world X)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Map::Area
|
||||
@@ -0,0 +1,79 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// `Map::Area` — shared `WorldMapArea.dbc` row resolution. A WorldMapArea
|
||||
// row is the client's per-zone map descriptor (directory name + the
|
||||
// world-coordinate rect used for world↔map transforms); every C_Map.*
|
||||
// reader that keys off "which zone" resolves a row first. Both
|
||||
// `Map::Overlays` (GetMapOverlays) and `Map::WorldSize` (GetMapWorldSize)
|
||||
// go through here so the two ways of naming a zone — an AreaTable areaID
|
||||
// argument, or the world map's current continent/zone view — live in one
|
||||
// place.
|
||||
namespace Map::Area {
|
||||
|
||||
// WorldMapArea row for an AreaTable `areaID` (the stable zone identity
|
||||
// `C_Map.GetBestMapForUnit` returns). Linear walk of the ~175-row table.
|
||||
// Returns -1 when no row carries that areaID.
|
||||
int RowForAreaID(uint32_t areaID);
|
||||
|
||||
// WorldMapArea row for the world map's currently *viewed* zone — the same
|
||||
// continent/zone selection the engine's map-name resolver (FUN_004a6cf0,
|
||||
// behind GetMapInfo) performs: continent index → per-continent blob, zone
|
||||
// index (-1 = whole continent) → that continent's zone-row array; no
|
||||
// continent selected falls back to the default-row global. Returns -1 when
|
||||
// the continent blob isn't resident.
|
||||
int CurrentViewRow();
|
||||
|
||||
// Resolves world point (x, y) on continent `mapID` to a zone: its AreaTable
|
||||
// areaID and a 0..100 zone-relative percent (`mapX` horizontal off world Y,
|
||||
// `mapY` vertical off world X — the WoW/pfQuest convention).
|
||||
//
|
||||
// WorldMapArea rects are loose, overlapping bounding boxes, so a point near a
|
||||
// zone edge falls inside several. Resolution therefore prefers the zone whose
|
||||
// **drawn landmass** (WorldMapOverlay hit rect) the point actually sits on —
|
||||
// the runtime analog of an ADT area-grid lookup (Un'Goro's crater vs Thousand
|
||||
// Needles' empty overlap). Overlay hit rects are themselves rectangles that
|
||||
// overlap at borders, so among landmass matches the winner is the zone the
|
||||
// point is DEEPEST inside an overlay of (Nijel's Point is deep in Desolace's
|
||||
// own overlay but only clips the edge of Stonetalon's Charred Vale box). If the
|
||||
// point is on no zone's overlay, falls back to the most-interior containing
|
||||
// rect. Returns false when no zone rect encloses the point (open sea,
|
||||
// degenerate rect, an instance with no zone row). Shared by GetAreaTriggerInfo
|
||||
// and the taxi-node zone resolve.
|
||||
bool ZonePercent(int mapID, float x, float y, int *outAreaID, double *outMapX,
|
||||
double *outMapY);
|
||||
|
||||
// Projects world (x, y) into the WorldMapArea rect of a SPECIFIC AreaTable
|
||||
// zone (`areaID`), filling the 0..100 zone-relative percent (`mapX` horizontal
|
||||
// off world Y, `mapY` vertical off world X). Returns false when that zone has
|
||||
// no usable WorldMapArea row OR the point lies outside its rect. Used when the
|
||||
// zone is already known by other means (a taxi node's name) and only the
|
||||
// in-zone position is needed — bypassing the containment / landmass search of
|
||||
// ZonePercent. The containment check lets a caller test candidate zones (a
|
||||
// node's city vs its outdoor region) and take the first that actually holds
|
||||
// the point.
|
||||
bool PercentInZone(int areaID, float x, float y, double *outMapX, double *outMapY);
|
||||
|
||||
// Projects world (x, y) on `mapID` to 0..1 CONTINENT-map coordinates using
|
||||
// the continent's WorldMapArea row (the areaID == 0 row with a non-degenerate
|
||||
// rect — some maps carry a stray zero-rect continent row that is skipped).
|
||||
// `outPx` is horizontal (off world Y), `outPy` vertical (off world X).
|
||||
// Returns false when the map has no usable continent row. This is the
|
||||
// projection retail's TaxiNodeInfo.position uses.
|
||||
bool ContinentPercent(int mapID, float x, float y, double *outPx, double *outPy);
|
||||
|
||||
} // namespace Map::Area
|
||||
@@ -0,0 +1,169 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Map.GetAreaTriggerInfo(triggerID)` and `C_Map.GetAreaTriggers([mapID])`
|
||||
// — expose `AreaTrigger.dbc`, the client's static trigger-volume geometry
|
||||
// (subzone-entry / exploration / teleport triggers). Vanilla loads the DBC
|
||||
// but exposes NONE of its geometry to Lua, so map addons (pfQuest, …) ship
|
||||
// hand-scraped trigger tables. Same value proposition as `GetMapOverlays`:
|
||||
// live, correct client data, no per-server table drift.
|
||||
//
|
||||
// ## Return shape (one info table)
|
||||
//
|
||||
// id — AreaTrigger.dbc row id
|
||||
// mapID — Map.dbc id (continent 0/1, or an instance map)
|
||||
// x, y, z — continent-space world coordinates of the trigger center
|
||||
// radius — sphere-trigger radius (0 for a box trigger)
|
||||
// isBox — true when the trigger is an oriented box (see below)
|
||||
// boxLength / boxWidth / boxHeight / boxYaw — box dimensions + yaw
|
||||
// (all 0 for a pure sphere trigger)
|
||||
// areaID — AreaTable zone id the point falls in (via WorldMapArea);
|
||||
// absent when no zone rect on this map contains the point
|
||||
// mapX / mapY — the trigger's position as a 0..100 map-relative percent
|
||||
// within that zone (the coordinate form map addons draw
|
||||
// with); absent alongside areaID when unresolved
|
||||
//
|
||||
// The raw world geometry is authoritative (straight from the DBC); the
|
||||
// derived `areaID`/`mapX`/`mapY` is the consumable form. Both are provided
|
||||
// so a consumer can draw immediately or run its own world→map transform.
|
||||
//
|
||||
// ## World → map-percent transform
|
||||
//
|
||||
// `WorldMapArea.dbc` gives each zone a world-coordinate rect
|
||||
// (`locTop`/`locBottom` bound world X, `locLeft`/`locRight` bound world Y —
|
||||
// WoW's world axes: +X north, +Y west). In map space the HORIZONTAL axis
|
||||
// (mapX) runs off world Y and the VERTICAL axis (mapY) off world X — the
|
||||
// standard WoW / pfQuest / Astrolabe convention:
|
||||
// mapX% = (locLeft - y) / (locLeft - locRight) * 100 (horizontal)
|
||||
// mapY% = (locTop - x) / (locTop - locBottom) * 100 (vertical)
|
||||
// We pick the zone by containment: among WorldMapArea rows on the trigger's
|
||||
// map with a real (non-continent) areaID whose rect encloses the point, the
|
||||
// smallest-area rect wins (the tightest zone — a subzone beats its parent).
|
||||
// The engine's own map-name resolver reads the same rect fields; see
|
||||
// `Map::Overlays` for the shared WorldMapArea wiring.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "map/Area.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Map::AreaTriggers {
|
||||
|
||||
namespace {
|
||||
|
||||
float FloatField(const uint8_t *rec, int off) {
|
||||
return *reinterpret_cast<const float *>(rec + off);
|
||||
}
|
||||
|
||||
// Zone containment + map-percent projection lives in the shared
|
||||
// `Map::Area::ZonePercent` (taxi nodes use the same transform).
|
||||
|
||||
// Builds one trigger's info table and leaves it on the Lua stack top.
|
||||
// Returns false (pushing nothing) when `id` is out of range or the slot
|
||||
// is empty.
|
||||
bool PushTriggerInfo(void *L, int id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_AREATRIGGER_RECORDS,
|
||||
Offsets::VAR_AREATRIGGER_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
|
||||
const int mapID = *reinterpret_cast<const int *>(rec + Offsets::OFF_AT_MAP_ID);
|
||||
const float x = FloatField(rec, Offsets::OFF_AT_X);
|
||||
const float y = FloatField(rec, Offsets::OFF_AT_Y);
|
||||
const float z = FloatField(rec, Offsets::OFF_AT_Z);
|
||||
const float radius = FloatField(rec, Offsets::OFF_AT_RADIUS);
|
||||
const float boxLength = FloatField(rec, Offsets::OFF_AT_BOX_LENGTH);
|
||||
const float boxWidth = FloatField(rec, Offsets::OFF_AT_BOX_WIDTH);
|
||||
const float boxHeight = FloatField(rec, Offsets::OFF_AT_BOX_HEIGHT);
|
||||
const float boxYaw = FloatField(rec, Offsets::OFF_AT_BOX_YAW);
|
||||
const bool isBox = (boxLength != 0.0f || boxWidth != 0.0f || boxHeight != 0.0f);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "id", id);
|
||||
Game::Lua::SetFieldNumber(L, "mapID", mapID);
|
||||
Game::Lua::SetFieldNumber(L, "x", x);
|
||||
Game::Lua::SetFieldNumber(L, "y", y);
|
||||
Game::Lua::SetFieldNumber(L, "z", z);
|
||||
Game::Lua::SetFieldNumber(L, "radius", radius);
|
||||
Game::Lua::SetFieldBool(L, "isBox", isBox);
|
||||
Game::Lua::SetFieldNumber(L, "boxLength", boxLength);
|
||||
Game::Lua::SetFieldNumber(L, "boxWidth", boxWidth);
|
||||
Game::Lua::SetFieldNumber(L, "boxHeight", boxHeight);
|
||||
Game::Lua::SetFieldNumber(L, "boxYaw", boxYaw);
|
||||
|
||||
int areaID = 0;
|
||||
double mapX = 0.0, mapY = 0.0;
|
||||
if (Map::Area::ZonePercent(mapID, x, y, &areaID, &mapX, &mapY)) {
|
||||
Game::Lua::SetFieldNumber(L, "areaID", areaID);
|
||||
Game::Lua::SetFieldNumber(L, "mapX", mapX);
|
||||
Game::Lua::SetFieldNumber(L, "mapY", mapY);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// `C_Map.GetAreaTriggerInfo(triggerID)` — one trigger's info table, or nil
|
||||
// for a missing / out-of-range id.
|
||||
int __fastcall Script_GetAreaTriggerInfo(void *L) {
|
||||
const bool hasID = Game::Lua::IsNumber(L, 1);
|
||||
const int id = hasID ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : 0;
|
||||
Game::Lua::SetTop(L, 0);
|
||||
if (!hasID || !PushTriggerInfo(L, id))
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Map.GetAreaTriggers([mapID])` — array of every trigger's info table,
|
||||
// optionally filtered to a single Map.dbc id.
|
||||
int __fastcall Script_GetAreaTriggers(void *L) {
|
||||
const bool filter = Game::Lua::IsNumber(L, 1);
|
||||
const int wantMap = filter ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : 0;
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_AREATRIGGER_COUNT);
|
||||
int outIdx = 0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_AREATRIGGER_RECORDS,
|
||||
Offsets::VAR_AREATRIGGER_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
if (filter &&
|
||||
*reinterpret_cast<const int *>(rec + Offsets::OFF_AT_MAP_ID) != wantMap)
|
||||
continue;
|
||||
|
||||
outIdx += 1;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(outIdx));
|
||||
PushTriggerInfo(L, id); // rec is valid, so this always pushes a table
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Map", "GetAreaTriggerInfo",
|
||||
&Script_GetAreaTriggerInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Map", "GetAreaTriggers",
|
||||
&Script_GetAreaTriggers);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Map::AreaTriggers
|
||||
@@ -0,0 +1,133 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Map.GetAreaInfo(areaID)` and `C_Map.GetAreas()` — surface
|
||||
// `AreaTable.dbc`'s localized name column to Lua. `AreaTable.dbc` is a
|
||||
// client DBC (always loaded, synchronous, localized, ~1500 rows), so
|
||||
// unlike creature/item names there's no WDB cold-cache problem — the name
|
||||
// is always available for any id.
|
||||
//
|
||||
// - `GetAreaInfo(areaID)` is a retail backport: the localized name for one
|
||||
// area id (zone or subzone), matching retail's signature.
|
||||
// - `GetAreas()` is a ClassicAPI extension (retail uses the uiMapID tree,
|
||||
// not raw AreaTable enumeration): the full id→name map, the load-bearing
|
||||
// piece for name databases that need name→id / the full id set (reverse
|
||||
// search, name resolution). Same "surface the hidden DBC" pattern as
|
||||
// `GetAreaTriggers` / `GetMapOverlays`.
|
||||
//
|
||||
// Names are the client's active locale (same source as `GetRealZoneText` /
|
||||
// `Script_GetCharacterInfo`'s zone-name resolve), so they match what the
|
||||
// game shows everywhere — no locale/rename drift vs a shipped table.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Map::Areas {
|
||||
|
||||
namespace {
|
||||
|
||||
// `C_Map.GetAreaInfo(areaID)` — localized AreaTable name, or nil for
|
||||
// non-numeric / non-positive input or an id with no row / empty name.
|
||||
int __fastcall Script_GetAreaInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const int areaID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *name =
|
||||
(areaID > 0) ? DBC::LocalizedField(Offsets::VAR_AREATABLE_RECORDS,
|
||||
Offsets::VAR_AREATABLE_COUNT,
|
||||
static_cast<uint32_t>(areaID),
|
||||
Offsets::OFF_AREATABLE_NAMES)
|
||||
: nullptr;
|
||||
if (name == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushString(L, name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Map.GetAreas()` — { [areaID] = name } for every row with a non-empty
|
||||
// localized name. Empty-name rows (DBC::LocalizedField → nullptr) are
|
||||
// omitted, so the map round-trips with GetAreaInfo for every key.
|
||||
int __fastcall Script_GetAreas(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_AREATABLE_COUNT);
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_AREATABLE_RECORDS,
|
||||
Offsets::VAR_AREATABLE_COUNT,
|
||||
static_cast<uint32_t>(id),
|
||||
Offsets::OFF_AREATABLE_NAMES);
|
||||
if (name == nullptr)
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(id));
|
||||
Game::Lua::PushString(L, name);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Map.GetMapAreaIDs()` — { [mapName] = areaID } for every
|
||||
// `WorldMapArea.dbc` row: the map's name (the dir the engine uses for its map
|
||||
// textures) → its `AreaTable` areaID. Vanilla has no `GetCurrentMapAreaID`;
|
||||
// this lets an addon resolve a *browsed* map to a zone id — including the
|
||||
// city / instance maps `GetMapZones()` doesn't enumerate (Orgrimmar,
|
||||
// Undercity, Alterac Valley), which otherwise can't be matched to zone content
|
||||
// (e.g. a flight master pinned by areaID). Continent-spanning rows (areaID 0)
|
||||
// and unnamed rows are omitted.
|
||||
//
|
||||
// Note: this is the WorldMapArea NAME, which matches `GetMapInfo()`'s returned
|
||||
// dir for almost all maps but not a few misspelled Blizzard folders (the
|
||||
// Orgrimmar map's texture folder is "Ogrimmar"), so `[GetMapInfo()]` can miss
|
||||
// for those — resolve them by zone name instead.
|
||||
int __fastcall Script_GetMapAreaIDs(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_AREA_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
const int areaID = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_AREA_ID);
|
||||
if (areaID == 0)
|
||||
continue; // continent-spanning row, no zone id
|
||||
const char *dir = *reinterpret_cast<const char *const *>(rec + Offsets::OFF_WMA_NAME);
|
||||
if (dir == nullptr || dir[0] == '\0')
|
||||
continue;
|
||||
Game::Lua::SetFieldNumber(L, dir, static_cast<double>(areaID));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Map", "GetAreaInfo", &Script_GetAreaInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Map", "GetAreas", &Script_GetAreas);
|
||||
Game::Lua::RegisterTableFunction("C_Map", "GetMapAreaIDs", &Script_GetMapAreaIDs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Map::Areas
|
||||
@@ -0,0 +1,80 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_MapExplorationInfo.GetExploredMapTextures(areaID)` and
|
||||
// `GetUnexploredMapTextures(areaID)` — the exploration-filtered views of a
|
||||
// zone's `WorldMapOverlay.dbc` entries.
|
||||
//
|
||||
// - `GetExploredMapTextures` is the retail-named backport: only the overlays
|
||||
// the local player has **discovered**. Retail keys on `uiMapID` and returns
|
||||
// numeric `fileDataIDs`; here it's an `AreaTable` areaID and the tiles are
|
||||
// texture paths (`fileDataIDs` holds those paths, see Overlays.cpp).
|
||||
// - `GetUnexploredMapTextures` is the ClassicAPI-extension complement: the
|
||||
// overlays NOT yet discovered — what a map-reveal addon draws over the
|
||||
// fogged base map. No retail equivalent (retail only ships the explored
|
||||
// getter); its inverse, `C_Map.GetMapOverlays`, returns *all* overlays.
|
||||
//
|
||||
// All three share `Map::Overlays::PushZoneOverlays` — same per-overlay table
|
||||
// shape (texture, resolved tiles, hit rect, areaIDs, fileDataIDs); these two
|
||||
// just pass an exploration Filter. "Explored" is computed per-areaID from the
|
||||
// player's explored-areas bitfield (see Overlays.cpp `IsOverlayExplored`), so
|
||||
// the areaID argument works for any zone, not just the one on screen.
|
||||
//
|
||||
// With no argument, both default to the world map's currently viewed zone
|
||||
// (like `GetMapOverlays()`).
|
||||
|
||||
#include "Game.h"
|
||||
#include "map/Area.h"
|
||||
#include "map/Overlays.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Map::Exploration {
|
||||
|
||||
namespace {
|
||||
|
||||
int ResolveRow(void *L) {
|
||||
return Game::Lua::IsNumber(L, 1)
|
||||
? Map::Area::RowForAreaID(
|
||||
static_cast<uint32_t>(Game::Lua::ToNumber(L, 1)))
|
||||
: Map::Area::CurrentViewRow();
|
||||
}
|
||||
|
||||
int __fastcall Script_GetExploredMapTextures(void *L) {
|
||||
const int row = ResolveRow(L);
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Map::Overlays::PushZoneOverlays(L, row, Map::Overlays::Filter::Explored);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetUnexploredMapTextures(void *L) {
|
||||
const int row = ResolveRow(L);
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Map::Overlays::PushZoneOverlays(L, row, Map::Overlays::Filter::Unexplored);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_MapExplorationInfo",
|
||||
"GetExploredMapTextures",
|
||||
&Script_GetExploredMapTextures);
|
||||
Game::Lua::RegisterTableFunction("C_MapExplorationInfo",
|
||||
"GetUnexploredMapTextures",
|
||||
&Script_GetUnexploredMapTextures);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Map::Exploration
|
||||
+181
-71
@@ -80,23 +80,37 @@
|
||||
// Array of overlay tables:
|
||||
// textureName — bare DBC name ("GOLDSHIRE")
|
||||
// texturePath — "Interface\WorldMap\<dir>\GOLDSHIRE" (engine format)
|
||||
// areaID — primary AreaTable id revealed (first nonzero)
|
||||
// areaIDs — all AreaTable ids revealed (WorldMapOverlay.dbc
|
||||
// areaID[4]; usually one, some overlays span 2-3 subzones)
|
||||
// textureWidth / textureHeight — the DBC placement rect (reliable for
|
||||
// placement; its IMPLIED tile count is what lies)
|
||||
// offsetX / offsetY — placement on the zone canvas
|
||||
// mapPointX / mapPointY
|
||||
// hitRectTop/Left/Bottom/Right — hit rectangle in world-map canvas px
|
||||
// (~1002x668); the tight clickable bounds of the landmass,
|
||||
// the subzone-center source map addons scrape
|
||||
// tileCols / tileRows / upscaled — the resolved grid
|
||||
// tiles — array of ready-to-draw tiles:
|
||||
// { file — texture path incl. number (SetTexture-ready)
|
||||
// width, height — draw size in map pixels
|
||||
// texCoordX, texCoordY — right/bottom texcoords
|
||||
// offsetX, offsetY } — absolute canvas position
|
||||
// fileDataIDs — ordered tile texture paths (retail's field name; vanilla
|
||||
// has no numeric fileDataIDs, so these are paths). Redundant
|
||||
// with tiles[].file — for retail-ported exploration addons.
|
||||
//
|
||||
// A consumer draws an overlay with a dumb loop: per tile SetTexture,
|
||||
// SetWidth/Height, SetTexCoord(0, texCoordX, 0, texCoordY), SetPoint.
|
||||
// Texture-less rows (a subzone hit region with no distinct art) still appear,
|
||||
// with textureName == "", zero size, and an empty `tiles` table — so
|
||||
// geometry consumers see every subzone while `ipairs(ov.tiles)` draw loops
|
||||
// stay safe. A consumer draws a textured overlay with a dumb loop: per tile
|
||||
// SetTexture, SetWidth/Height, SetTexCoord(0, texCoordX, 0, texCoordY), SetPoint.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "map/Area.h"
|
||||
#include "map/Overlays.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -279,46 +293,8 @@ const ResolvedOverlay *CachedResolve(int overlayId, const char *basePath,
|
||||
return g_cache[overlayId];
|
||||
}
|
||||
|
||||
// WorldMapArea row id for the world map's current view — the same
|
||||
// resolution the engine's current-map-name getter (FUN_004a6cf0, behind
|
||||
// GetMapInfo) performs: continent/zone selection indexes into the
|
||||
// per-continent data blob; no continent selected falls back to the
|
||||
// default row global.
|
||||
int CurrentWorldMapAreaRow() {
|
||||
const int continent =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_CONTINENT_INDEX);
|
||||
if (continent < 0)
|
||||
return *reinterpret_cast<const int *>(
|
||||
Offsets::VAR_WORLDMAP_DEFAULT_AREA_ROW);
|
||||
const uint8_t *blob = *reinterpret_cast<const uint8_t *const *>(
|
||||
Offsets::VAR_WORLDMAP_CONTINENT_DATA);
|
||||
if (blob == nullptr)
|
||||
return -1;
|
||||
const uint8_t *entry = blob + continent * Offsets::WORLDMAP_CONTINENT_STRIDE;
|
||||
const int zone =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_ZONE_INDEX);
|
||||
if (zone < 0)
|
||||
return *reinterpret_cast<const int *>(entry + 0x04);
|
||||
const int *zoneRows = *reinterpret_cast<const int *const *>(entry + 0x10);
|
||||
return (zoneRows != nullptr) ? zoneRows[zone] : -1;
|
||||
}
|
||||
|
||||
// WorldMapArea row id for an AreaTable areaID. Linear walk — the table
|
||||
// has ~175 rows; called once per Lua call.
|
||||
int WorldMapAreaRowForAreaID(uint32_t areaID) {
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_AREA_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec != nullptr &&
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_WMA_AREA_ID) ==
|
||||
areaID)
|
||||
return id;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
// WorldMapArea row resolution (areaID → row, current view → row) lives in
|
||||
// the shared `Map::Area` helper — `GetMapWorldSize` keys off the same rows.
|
||||
|
||||
// Pushes the `tiles` array for one overlay (leaves it on the stack top).
|
||||
void PushTiles(void *L, const ResolvedOverlay *r, const char *basePath,
|
||||
@@ -401,20 +377,78 @@ void PushTiles(void *L, const ResolvedOverlay *r, const char *basePath,
|
||||
}
|
||||
}
|
||||
|
||||
// `C_Map.GetMapOverlays([areaID])` — see the module comment.
|
||||
int __fastcall Script_GetMapOverlays(void *L) {
|
||||
int wmaRow = -1;
|
||||
if (Game::Lua::IsNumber(L, 1)) {
|
||||
wmaRow = WorldMapAreaRowForAreaID(
|
||||
static_cast<uint32_t>(Game::Lua::ToNumber(L, 1)));
|
||||
} else {
|
||||
wmaRow = CurrentWorldMapAreaRow();
|
||||
}
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// The local player's explored-areas bitfield (`[player+0xE68] + 0xE6C`), or
|
||||
// null before the player is resident. Bit `AreaBit` set == that AreaTable
|
||||
// area is explored. Same storage the engine's discovered-overlay rebuild
|
||||
// (FUN_004a67a0) reads.
|
||||
const uint8_t *ExplorationBits() {
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(fn("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
auto *sub = *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_CGPLAYER_INFO);
|
||||
if (sub == nullptr)
|
||||
return nullptr;
|
||||
return sub + Offsets::OFF_PLAYER_EXPLORED_BITS;
|
||||
}
|
||||
|
||||
// True if the local player has discovered `overlayRec` — mirrors the
|
||||
// per-areaID test in FUN_004a67a0's discovered-overlay rebuild: an overlay
|
||||
// is explored if ANY of its (up to 4) AreaTable ids is explored. An area
|
||||
// with gate (+0x28) < 0 needs no exploration; otherwise its AreaBit (+0x0C)
|
||||
// is looked up in `bits`. `bits == null` (no player) → not explored.
|
||||
bool IsOverlayExplored(const uint8_t *overlayRec, const uint8_t *bits) {
|
||||
if (bits == nullptr)
|
||||
return false;
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
const int areaID = *reinterpret_cast<const int *>(
|
||||
overlayRec + Offsets::OFF_WMO_AREA_ID + k * 4);
|
||||
if (areaID <= 0)
|
||||
continue;
|
||||
const uint8_t *areaRec =
|
||||
DBC::Record(Offsets::VAR_AREATABLE_RECORDS,
|
||||
Offsets::VAR_AREATABLE_COUNT, static_cast<uint32_t>(areaID));
|
||||
if (areaRec == nullptr)
|
||||
continue;
|
||||
if (*reinterpret_cast<const int *>(areaRec + Offsets::OFF_AREATABLE_EXPLORE_GATE) < 0)
|
||||
return true; // area needs no exploration → always counts
|
||||
const int areaBit = *reinterpret_cast<const int *>(
|
||||
areaRec + Offsets::OFF_AREATABLE_AREA_BIT);
|
||||
if (areaBit < 0)
|
||||
continue;
|
||||
const int byteIdx = areaBit >> 3;
|
||||
if (byteIdx >= 0x100)
|
||||
return true; // out of bitfield range → engine treats as explored
|
||||
if ((bits[byteIdx] & (1u << (areaBit & 7))) != 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// `C_Map.GetMapOverlays([areaID])` — see the module comment. Thin wrapper
|
||||
// over the shared `PushZoneOverlays` with no exploration filter.
|
||||
int __fastcall Script_GetMapOverlays(void *L) {
|
||||
const int wmaRow = Game::Lua::IsNumber(L, 1)
|
||||
? Map::Area::RowForAreaID(static_cast<uint32_t>(
|
||||
Game::Lua::ToNumber(L, 1)))
|
||||
: Map::Area::CurrentViewRow();
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Map::Overlays::PushZoneOverlays(L, wmaRow, Map::Overlays::Filter::All);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Shared overlay walk — see Overlays.h. Filters by exploration and builds
|
||||
// the rich per-overlay table (texture + resolved tiles + hit rect + areaIDs
|
||||
// + fileDataIDs). Uses the anonymous-namespace helpers above (same TU).
|
||||
void PushZoneOverlays(void *L, int wmaRow, Filter filter) {
|
||||
Game::Lua::NewTable(L);
|
||||
if (wmaRow <= 0)
|
||||
return 1;
|
||||
return;
|
||||
|
||||
const uint8_t *wmaRec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_AREA_COUNT,
|
||||
@@ -425,6 +459,10 @@ int __fastcall Script_GetMapOverlays(void *L) {
|
||||
Offsets::OFF_WMA_NAME)
|
||||
: nullptr;
|
||||
|
||||
// Exploration bitfield only needed for the filtered variants.
|
||||
const uint8_t *bits =
|
||||
(filter == Filter::All) ? nullptr : ExplorationBits();
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_OVERLAY_COUNT);
|
||||
int outIdx = 0;
|
||||
@@ -436,35 +474,56 @@ int __fastcall Script_GetMapOverlays(void *L) {
|
||||
*reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_WORLDMAP_AREA) !=
|
||||
wmaRow)
|
||||
continue;
|
||||
|
||||
if (filter != Filter::All) {
|
||||
const bool explored = IsOverlayExplored(rec, bits);
|
||||
if (filter == Filter::Explored && !explored)
|
||||
continue;
|
||||
if (filter == Filter::Unexplored && explored)
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *texName = *reinterpret_cast<const char *const *>(
|
||||
rec + Offsets::OFF_WMO_TEXTURE_NAME);
|
||||
if (texName == nullptr || texName[0] == '\0')
|
||||
continue; // hit-rect-only rows have no drawable texture
|
||||
|
||||
// Full path, mirroring the engine's own GetMapOverlayInfo format
|
||||
// (unresolvable map dir falls back to the "World" folder).
|
||||
char basePath[0x104];
|
||||
if (mapDir != nullptr && mapDir[0] != '\0')
|
||||
std::snprintf(basePath, sizeof(basePath),
|
||||
"Interface\\WorldMap\\%s\\%s", mapDir, texName);
|
||||
else
|
||||
std::snprintf(basePath, sizeof(basePath),
|
||||
"Interface\\WorldMap\\World\\%s", texName);
|
||||
const bool hasTexture = (texName != nullptr && texName[0] != '\0');
|
||||
|
||||
const auto fieldInt = [rec](uintptr_t off) {
|
||||
return *reinterpret_cast<const int32_t *>(rec + off);
|
||||
};
|
||||
const int dbcW = fieldInt(Offsets::OFF_WMO_TEXTURE_WIDTH);
|
||||
const int dbcH = fieldInt(Offsets::OFF_WMO_TEXTURE_HEIGHT);
|
||||
const int offX = fieldInt(Offsets::OFF_WMO_OFFSET_X);
|
||||
const int offY = fieldInt(Offsets::OFF_WMO_OFFSET_Y);
|
||||
|
||||
const ResolvedOverlay *r = CachedResolve(id, basePath, dbcW, dbcH);
|
||||
// Texture placement + tile grid only exist for rows that carry art.
|
||||
// A hit-rect-only row (areaID + hit rect, no texture — a clickable
|
||||
// subzone region with no distinct BLP) still gets a result row: empty
|
||||
// textureName/path, zero size, and an empty `tiles` table (r == null →
|
||||
// PushTiles emits `{}`), so geometry consumers see it while art-draw
|
||||
// loops (`ipairs(ov.tiles)`) iterate zero times. Stock 1.12 and Octo
|
||||
// both happen to give every overlay a texture, but the DBC schema
|
||||
// allows texture-less rows, so we don't assume.
|
||||
char basePath[0x104];
|
||||
int dbcW = 0, dbcH = 0;
|
||||
const ResolvedOverlay *r = nullptr;
|
||||
if (hasTexture) {
|
||||
// Full path, mirroring the engine's own GetMapOverlayInfo format
|
||||
// (unresolvable map dir falls back to the "World" folder).
|
||||
if (mapDir != nullptr && mapDir[0] != '\0')
|
||||
std::snprintf(basePath, sizeof(basePath),
|
||||
"Interface\\WorldMap\\%s\\%s", mapDir, texName);
|
||||
else
|
||||
std::snprintf(basePath, sizeof(basePath),
|
||||
"Interface\\WorldMap\\World\\%s", texName);
|
||||
dbcW = fieldInt(Offsets::OFF_WMO_TEXTURE_WIDTH);
|
||||
dbcH = fieldInt(Offsets::OFF_WMO_TEXTURE_HEIGHT);
|
||||
r = CachedResolve(id, basePath, dbcW, dbcH);
|
||||
} else {
|
||||
basePath[0] = '\0';
|
||||
}
|
||||
|
||||
outIdx += 1;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(outIdx));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "textureName", texName);
|
||||
Game::Lua::SetFieldString(L, "textureName", hasTexture ? texName : "");
|
||||
Game::Lua::SetFieldString(L, "texturePath", basePath);
|
||||
Game::Lua::SetFieldNumber(L, "textureWidth", dbcW);
|
||||
Game::Lua::SetFieldNumber(L, "textureHeight", dbcH);
|
||||
@@ -474,6 +533,41 @@ int __fastcall Script_GetMapOverlays(void *L) {
|
||||
L, "mapPointX", fieldInt(Offsets::OFF_WMO_MAP_POINT_X));
|
||||
Game::Lua::SetFieldNumber(
|
||||
L, "mapPointY", fieldInt(Offsets::OFF_WMO_MAP_POINT_Y));
|
||||
|
||||
// Hit rectangle (world-map canvas px, ~1002x668) — the tight clickable
|
||||
// bounds of the landmass, present for every row (texture or not). The
|
||||
// subzone-center source: (left+right)/2 / 1002, (top+bottom)/2 / 668.
|
||||
Game::Lua::SetFieldNumber(
|
||||
L, "hitRectTop", fieldInt(Offsets::OFF_WMO_HITRECT_TOP));
|
||||
Game::Lua::SetFieldNumber(
|
||||
L, "hitRectLeft", fieldInt(Offsets::OFF_WMO_HITRECT_LEFT));
|
||||
Game::Lua::SetFieldNumber(
|
||||
L, "hitRectBottom", fieldInt(Offsets::OFF_WMO_HITRECT_BOTTOM));
|
||||
Game::Lua::SetFieldNumber(
|
||||
L, "hitRectRight", fieldInt(Offsets::OFF_WMO_HITRECT_RIGHT));
|
||||
|
||||
// AreaTable ids this overlay reveals (WorldMapOverlay.dbc areaID[4]).
|
||||
// `areaID` is the primary (first nonzero — every overlay has one);
|
||||
// `areaIDs` is the full nonzero list (52 of 707 overlays span 2-3
|
||||
// subzones, e.g. a lake straddling two areas). Same table shape as
|
||||
// `tiles` — build the array, then assign the scalar.
|
||||
int firstArea = 0, aCount = 0;
|
||||
Game::Lua::PushString(L, "areaIDs");
|
||||
Game::Lua::NewTable(L);
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
const int aid = fieldInt(Offsets::OFF_WMO_AREA_ID + k * 4);
|
||||
if (aid == 0)
|
||||
continue;
|
||||
if (firstArea == 0)
|
||||
firstArea = aid;
|
||||
aCount += 1;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(aCount));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(aid));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTable(L, -3); // overlay.areaIDs = { ... }
|
||||
Game::Lua::SetFieldNumber(L, "areaID", firstArea);
|
||||
|
||||
if (r != nullptr) {
|
||||
Game::Lua::SetFieldNumber(L, "tileCols", r->cols);
|
||||
Game::Lua::SetFieldNumber(L, "tileRows", r->rows);
|
||||
@@ -483,18 +577,34 @@ int __fastcall Script_GetMapOverlays(void *L) {
|
||||
PushTiles(L, r, basePath, dbcW, dbcH, offX, offY);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
|
||||
// fileDataIDs — ordered tile texture paths (retail's field name for
|
||||
// the tile list). Vanilla has no numeric fileDataIDs, so these are the
|
||||
// SetTexture-ready paths; redundant with tiles[].file, provided so
|
||||
// retail-ported exploration addons reading info.fileDataIDs work,
|
||||
// while tiles[] carries the per-tile draw geometry Octo needs.
|
||||
Game::Lua::PushString(L, "fileDataIDs");
|
||||
Game::Lua::NewTable(L);
|
||||
if (r != nullptr) {
|
||||
char file[0x120];
|
||||
for (int i = 0; i < r->count; ++i) {
|
||||
std::snprintf(file, sizeof(file), "%s%d", basePath, i + 1);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushString(L, file);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
}
|
||||
Game::Lua::SetTable(L, -3); // overlay.fileDataIDs = { ... }
|
||||
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
namespace {
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Map", "GetMapOverlays",
|
||||
&Script_GetMapOverlays);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Map::Overlays
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Map::Overlays {
|
||||
|
||||
// Which overlays of a zone to include, by the local player's exploration.
|
||||
enum class Filter {
|
||||
All, // every WorldMapOverlay.dbc row for the zone (C_Map.GetMapOverlays)
|
||||
Explored, // only overlays the player has discovered
|
||||
Unexplored, // only overlays the player has NOT discovered
|
||||
};
|
||||
|
||||
// Pushes an array of overlay tables for `wmaRow` (a WorldMapArea.dbc row),
|
||||
// filtered by exploration, onto the Lua stack as a single value. Shared by
|
||||
// `C_Map.GetMapOverlays` (Filter::All) and `C_MapExplorationInfo`'s
|
||||
// explored / unexplored texture getters. The caller resolves the row (via
|
||||
// `Map::Area`) and clears the stack first; this leaves exactly one table.
|
||||
// The per-overlay table shape is documented in Overlays.cpp's header.
|
||||
//
|
||||
// Explored/Unexplored read the player's explored-areas bitfield; before the
|
||||
// player is resident (character select) every overlay reads as unexplored.
|
||||
void PushZoneOverlays(void *L, int wmaRow, Filter filter);
|
||||
|
||||
} // namespace Map::Overlays
|
||||
@@ -0,0 +1,82 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Map.GetMapWorldSize([areaID])` — the world size (in yards) of a zone,
|
||||
// as `width, height`. This is the physical extent the map represents: the
|
||||
// span of the `WorldMapArea.dbc` placement rect. Modern addons use it to
|
||||
// convert map-relative percents to yard distances (e.g. tracking-range
|
||||
// rings, "X yards away" readouts).
|
||||
//
|
||||
// With a numeric `areaID` (AreaTable id — the same identity
|
||||
// `C_Map.GetBestMapForUnit` returns and `C_Map.GetMapOverlays` accepts),
|
||||
// returns that zone's size. With no argument, returns the world map's
|
||||
// currently viewed zone (same resolution `GetMapOverlays()` uses with no
|
||||
// arg). Returns nothing (→ nil, nil) when the zone can't be resolved.
|
||||
//
|
||||
// WoW's world axes are +X north, +Y west; the WorldMapArea rect bounds
|
||||
// them as locTop/locBottom (world X) and locLeft/locRight (world Y). Map
|
||||
// width is the horizontal (east-west) extent and height the vertical
|
||||
// (north-south):
|
||||
// width = locLeft - locRight (world Y span)
|
||||
// height = locTop - locBottom (world X span)
|
||||
// The same spans are the denominators in the world→map percent transform
|
||||
// (see `Map::AreaTriggers`), so a point's `mapX% * width / 100` is its
|
||||
// offset in yards from the zone's east edge, and likewise for height.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "map/Area.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Map::WorldSize {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_GetMapWorldSize(void *L) {
|
||||
const int row = Game::Lua::IsNumber(L, 1)
|
||||
? Map::Area::RowForAreaID(
|
||||
static_cast<uint32_t>(Game::Lua::ToNumber(L, 1)))
|
||||
: Map::Area::CurrentViewRow();
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
if (row <= 0)
|
||||
return 0; // no values -> nil, nil
|
||||
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_AREA_COUNT,
|
||||
static_cast<uint32_t>(row));
|
||||
if (rec == nullptr)
|
||||
return 0;
|
||||
|
||||
const float left = *reinterpret_cast<const float *>(rec + Offsets::OFF_WMA_LOC_LEFT);
|
||||
const float right = *reinterpret_cast<const float *>(rec + Offsets::OFF_WMA_LOC_RIGHT);
|
||||
const float top = *reinterpret_cast<const float *>(rec + Offsets::OFF_WMA_LOC_TOP);
|
||||
const float bottom = *reinterpret_cast<const float *>(rec + Offsets::OFF_WMA_LOC_BOTTOM);
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(left) - right); // width (world Y span)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(top) - bottom); // height (world X span)
|
||||
return 2;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Map", "GetMapWorldSize",
|
||||
&Script_GetMapWorldSize);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Map::WorldSize
|
||||
+2
-11
@@ -33,6 +33,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Location.h"
|
||||
#include "unit/Identity.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -42,7 +43,6 @@ namespace Merchant::Frame {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(
|
||||
void *cache, uint32_t itemID, const uint64_t *guid,
|
||||
void *callback, void *userData, bool requestIfMissing);
|
||||
@@ -108,15 +108,6 @@ int ItemQuality(uint32_t itemID) {
|
||||
|
||||
// Player invMgr lives inline at player + 0x1D38. First uint32 is the
|
||||
// max slot count; +0x04 is the GUID array pointer.
|
||||
const uint8_t *PlayerInvMgr() {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<uint8_t *>(resolve("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
|
||||
}
|
||||
|
||||
// Walk a single bag, invoking `cb(cgItem)` for each non-empty slot.
|
||||
// `cb` returns true to continue, false to stop iteration. Stomps
|
||||
// the Lua stack — caller must own it (`ResolveBag` does the stomping).
|
||||
@@ -161,7 +152,7 @@ int __fastcall Script_GetBuybackItemID(void *L) {
|
||||
if (invSlot == 0)
|
||||
return 0;
|
||||
|
||||
auto *invMgr = PlayerInvMgr();
|
||||
auto *invMgr = Unit::Identity::PlayerInventoryManager();
|
||||
if (invMgr == nullptr)
|
||||
return 0;
|
||||
// First u32 of the invMgr struct is the max slot count.
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -65,7 +66,6 @@ namespace {
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
// `__thiscall(player /*ecx*/, skillLineID) -> slot` (-1 if the player
|
||||
// doesn't have the skill line). See VAR docs on FUN_SKILL_LINE_TO_SLOT.
|
||||
using SkillLineToSlot_t = int(__thiscall *)(void *player, uint32_t skillLineID);
|
||||
@@ -83,13 +83,6 @@ const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
// The local player's CGUnit/CGPlayer object (holds the descriptor at
|
||||
// +OFF_UNIT_DESCRIPTOR and the CGPlayer sub-struct at +OFF_CGPLAYER_INFO).
|
||||
const uint8_t *ResolvePlayer() {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return static_cast<const uint8_t *>(resolve("player"));
|
||||
}
|
||||
|
||||
// True iff `mask` restricts a 1-based `index` (class/race) the player
|
||||
// isn't part of. Mask 0 and 0xFFFFFFFF both mean "no restriction".
|
||||
bool RestrictedOut(uint32_t mask, uint32_t index1Based) {
|
||||
@@ -246,7 +239,7 @@ bool ComputeCanUse(int itemID) {
|
||||
auto *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return false; // not cached → sync false, no load fired
|
||||
auto *player = ResolvePlayer();
|
||||
auto *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return false; // pre-login / no player
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "equipmentset/Locations.h"
|
||||
#include "event/Custom.h"
|
||||
#include "player/StatSignal.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -112,6 +113,9 @@ int __fastcall EquipSlotChanged_cb(uint32_t fieldOffset, uint32_t /*size*/,
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kEvtEquipmentChanged),
|
||||
slot0 + 1,
|
||||
SlotHasItem(guidLo, guidHi, slot0));
|
||||
// Gear swap changes item +healing and Spirit/Armor — invalidate the
|
||||
// GetSpellBonusHealing cache.
|
||||
Player::StatSignal::Notify();
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The `PlayerLocation`-taking `C_PlayerInfo` accessors:
|
||||
//
|
||||
// C_PlayerInfo.GetName(playerLocation) -> name
|
||||
// C_PlayerInfo.GetClass(playerLocation) -> className, classFilename, classID
|
||||
// C_PlayerInfo.GetRace(playerLocation) -> raceID
|
||||
// C_PlayerInfo.GetSex(playerLocation) -> sex (1 neutral / 2 male / 3 female)
|
||||
// C_PlayerInfo.IsConnected([playerLocation]) -> isConnected
|
||||
//
|
||||
// A `PlayerLocation` (see Util/PlayerLocation.lua) is a plain table carrying
|
||||
// either a unit token (`.unit`) or a GUID string (`.guid`). We resolve it to
|
||||
// a 64-bit GUID — unit tokens through the engine's token→GUID table
|
||||
// (`FUN_TOKEN_TO_GUID`, which resolves group members in other zones too, not
|
||||
// just synced CGUnits), GUID strings by parsing — and then read everything
|
||||
// straight from the engine's own data structures. No `Script_*` Lua wrappers
|
||||
// are called; each value comes from the same engine internals the codebase
|
||||
// already uses elsewhere:
|
||||
//
|
||||
// - GetName -> resolve the GUID to a CGObject and call the polymorphic
|
||||
// name getter `FUN_OBJECT_GET_NAME` (same path as
|
||||
// `UnitNameFromGUID`).
|
||||
// - GetRace / -> UNIT_FIELD_BYTES_0: the login-time mirror globals when the
|
||||
// GetClass / location is the local player (valid before the descriptor
|
||||
// GetSex spawns, matching `Script_UnitClass`'s own player path),
|
||||
// otherwise the resolved unit's descriptor bytes. GetClass
|
||||
// maps classID -> className/classFile via `ChrClasses.dbc`
|
||||
// like `C_CreatureInfo.GetClassInfo`. GetSex reports the
|
||||
// API's 2/3 (raw gender byte 0/1 + 2), matching
|
||||
// `GetPlayerInfoByGUID`.
|
||||
// - IsConnected -> a resolvable CGObject means connected; otherwise the
|
||||
// group-member roster's online bit (then slot presence),
|
||||
// exactly what `Script_UnitIsConnected` checks.
|
||||
//
|
||||
// Only the `unit` and `guid` PlayerLocation kinds resolve on 1.12 — the chat
|
||||
// line / who / battlefield-score / community / voice kinds have no
|
||||
// client-side unit resolution. Unsupported or unresolvable locations return
|
||||
// nil.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Player::LocationInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
using TokenToGuid_t = uint64_t(__fastcall *)(const char *token);
|
||||
using ResolveObject_t = void *(__fastcall *)(uint32_t typeMask, const char *dbg,
|
||||
uint32_t guidLo, uint32_t guidHi, int line);
|
||||
using GetName_t = const char *(__thiscall *)(void *obj, int *outFlags);
|
||||
using GroupLookup_t = void *(__fastcall *)(const uint64_t *guid);
|
||||
|
||||
// Reads a string field `t[field]` at table stack index `locIdx` into `out`.
|
||||
// Returns true only for a non-empty string. Stack-neutral.
|
||||
bool ReadStringField(void *L, int locIdx, const char *field, char *out, int size) {
|
||||
Game::Lua::PushString(L, field);
|
||||
Game::Lua::GetTable(L, locIdx); // pops key, pushes value
|
||||
bool ok = false;
|
||||
if (Game::Lua::IsString(L, -1)) {
|
||||
const char *s = Game::Lua::ToString(L, -1);
|
||||
if (s != nullptr && s[0] != '\0') {
|
||||
int i = 0;
|
||||
for (; s[i] != '\0' && i < size - 1; ++i)
|
||||
out[i] = s[i];
|
||||
out[i] = '\0';
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
Game::Lua::SetTop(L, -2); // pop value (or nil)
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Resolves the PlayerLocation table at `locIdx` to a 64-bit GUID. Unit tokens
|
||||
// go through the engine token→GUID table (handles out-of-zone group members);
|
||||
// GUID strings are parsed. Returns false for unsupported kinds or a token
|
||||
// that maps to nothing.
|
||||
bool LocationGuid(void *L, int locIdx, uint64_t *out) {
|
||||
char token[64];
|
||||
if (ReadStringField(L, locIdx, "unit", token, sizeof(token))) {
|
||||
auto toGuid = reinterpret_cast<TokenToGuid_t>(Offsets::FUN_TOKEN_TO_GUID);
|
||||
*out = toGuid(token);
|
||||
return *out != 0;
|
||||
}
|
||||
char guidStr[40];
|
||||
if (ReadStringField(L, locIdx, "guid", guidStr, sizeof(guidStr)))
|
||||
return Guid::Parse(guidStr, out) && *out != 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolves a GUID to its currently-synced CGUnit, or null if not resident.
|
||||
const uint8_t *ResolveUnit(uint64_t guid) {
|
||||
auto resolve = reinterpret_cast<ResolveObject_t>(Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
return static_cast<const uint8_t *>(
|
||||
resolve(Offsets::OBJ_TYPE_UNIT, "ClassicAPI", static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0));
|
||||
}
|
||||
|
||||
// Reads a UNIT_FIELD_BYTES_0 byte: the login-time mirror global when `guid` is
|
||||
// the local player (works before the descriptor spawns), otherwise the
|
||||
// resolved unit's descriptor byte. Returns -1 when the unit isn't resident.
|
||||
int UnitByte(uint64_t guid, uintptr_t playerMirror, uint32_t descByteOffset) {
|
||||
if (guid == Unit::Identity::PlayerGuid())
|
||||
return *reinterpret_cast<const uint8_t *>(playerMirror);
|
||||
const uint8_t *unit = ResolveUnit(guid);
|
||||
if (unit == nullptr)
|
||||
return -1;
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_CGUNIT_OBJECT_FIELDS);
|
||||
if (desc == nullptr)
|
||||
return -1;
|
||||
return *reinterpret_cast<const uint8_t *>(desc + descByteOffset);
|
||||
}
|
||||
|
||||
int __fastcall Script_GetName(void *L) {
|
||||
uint64_t guid = 0;
|
||||
if (!LocationGuid(L, 1, &guid))
|
||||
return 0;
|
||||
const uint8_t *unit = ResolveUnit(guid);
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
auto getName = reinterpret_cast<GetName_t>(Offsets::FUN_OBJECT_GET_NAME);
|
||||
const char *name = getName(const_cast<uint8_t *>(unit), nullptr);
|
||||
if (name == nullptr || name[0] == '\0')
|
||||
return 0;
|
||||
Game::Lua::PushString(L, name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetRace(void *L) {
|
||||
uint64_t guid = 0;
|
||||
if (!LocationGuid(L, 1, &guid))
|
||||
return 0;
|
||||
const int raceID = UnitByte(guid, Offsets::VAR_PLAYER_RACE_BYTE,
|
||||
Offsets::OFF_UNIT_DESCRIPTOR_RACE_BYTE);
|
||||
if (raceID <= 0)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(raceID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetClass(void *L) {
|
||||
uint64_t guid = 0;
|
||||
if (!LocationGuid(L, 1, &guid))
|
||||
return 0;
|
||||
const int classID = UnitByte(guid, Offsets::VAR_PLAYER_CLASS_BYTE,
|
||||
Offsets::OFF_UNIT_DESCRIPTOR_CLASS_BYTE);
|
||||
if (classID <= 0)
|
||||
return 0;
|
||||
// className / classFile from ChrClasses.dbc — same reads as
|
||||
// C_CreatureInfo.GetClassInfo(classID).
|
||||
const char *classFile = DBC::StringField(Offsets::VAR_CHRCLASSES_RECORDS,
|
||||
Offsets::VAR_CHRCLASSES_COUNT,
|
||||
static_cast<uint32_t>(classID),
|
||||
Offsets::OFF_CHRCLASSES_FILENAME);
|
||||
if (classFile == nullptr)
|
||||
return 0; // classID with no ChrClasses row
|
||||
const char *className = DBC::LocalizedField(Offsets::VAR_CHRCLASSES_RECORDS,
|
||||
Offsets::VAR_CHRCLASSES_COUNT,
|
||||
static_cast<uint32_t>(classID),
|
||||
Offsets::OFF_CHRCLASSES_NAMES);
|
||||
|
||||
Game::Lua::PushString(L, className); // NULL → nil via lua_pushstring
|
||||
Game::Lua::PushString(L, classFile);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(classID));
|
||||
return 3;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetSex(void *L) {
|
||||
uint64_t guid = 0;
|
||||
if (!LocationGuid(L, 1, &guid))
|
||||
return 0;
|
||||
const int gender = UnitByte(guid, Offsets::VAR_PLAYER_SEX_BYTE,
|
||||
Offsets::OFF_UNIT_DESCRIPTOR_SEX_BYTE);
|
||||
if (gender < 0)
|
||||
return 0;
|
||||
// UNIT_FIELD_BYTES_0 stores 0 = male / 1 = female; the API (and UnitSex)
|
||||
// reports 2 = male / 3 = female. Same +2 remap as GetPlayerInfoByGUID.
|
||||
Game::Lua::PushNumber(L, static_cast<double>(gender + 2));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsConnected(void *L) {
|
||||
// `playerLocation` is optional — with no table arg it defaults to the
|
||||
// local player (always connected while playing).
|
||||
uint64_t guid = 0;
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE) {
|
||||
if (!LocationGuid(L, 1, &guid))
|
||||
return 0;
|
||||
} else {
|
||||
guid = Unit::Identity::PlayerGuid();
|
||||
}
|
||||
if (guid == 0)
|
||||
return 0;
|
||||
|
||||
bool connected = ResolveUnit(guid) != nullptr; // a synced object is online
|
||||
if (!connected) {
|
||||
// Not resident — consult the group-member roster, mirroring
|
||||
// Script_UnitIsConnected: the stats block's online bit, else slot
|
||||
// presence (a member with no stats block yet still counts as present).
|
||||
auto stats = reinterpret_cast<GroupLookup_t>(
|
||||
Offsets::FUN_GROUP_MEMBER_STATS_LOOKUP)(&guid);
|
||||
if (stats != nullptr) {
|
||||
connected = (*reinterpret_cast<const uint8_t *>(
|
||||
static_cast<const uint8_t *>(stats) +
|
||||
Offsets::OFF_GROUP_MEMBER_STATUS_FLAGS) &
|
||||
Offsets::GROUP_MEMBER_STATUS_ONLINE) != 0;
|
||||
} else {
|
||||
connected = reinterpret_cast<GroupLookup_t>(
|
||||
Offsets::FUN_GROUP_MEMBER_SLOT_LOOKUP)(&guid) != nullptr;
|
||||
}
|
||||
}
|
||||
Game::Lua::PushBool(L, connected);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_PlayerInfo", "GetName", &Script_GetName);
|
||||
Game::Lua::RegisterTableFunction("C_PlayerInfo", "GetClass", &Script_GetClass);
|
||||
Game::Lua::RegisterTableFunction("C_PlayerInfo", "GetRace", &Script_GetRace);
|
||||
Game::Lua::RegisterTableFunction("C_PlayerInfo", "GetSex", &Script_GetSex);
|
||||
Game::Lua::RegisterTableFunction("C_PlayerInfo", "IsConnected", &Script_IsConnected);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Player::LocationInfo
|
||||
@@ -0,0 +1,28 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "player/StatSignal.h"
|
||||
|
||||
namespace Player::StatSignal {
|
||||
|
||||
namespace {
|
||||
// Starts at 1 so a consumer whose cached epoch is default-0 always computes on
|
||||
// its first query.
|
||||
uint32_t g_epoch = 1;
|
||||
} // namespace
|
||||
|
||||
void Notify() { ++g_epoch; }
|
||||
|
||||
uint32_t Epoch() { return g_epoch; }
|
||||
|
||||
} // namespace Player::StatSignal
|
||||
@@ -0,0 +1,38 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// A pull-based "the local player's stat inputs changed" signal. `Notify()` is
|
||||
// called from the aura and equipment hooks whenever the player gains/loses an
|
||||
// aura or swaps gear — the state that feeds derived stats like
|
||||
// `GetSpellBonusHealing` (flat +healing, plus the Spirit/Armor talent
|
||||
// conversions that scale with those descriptor fields).
|
||||
//
|
||||
// Unlike Tick::WorldTick (push / per-frame callback), this is a monotonic
|
||||
// epoch that consumers POLL: a lazy cache recomputes only when `Epoch()`
|
||||
// differs from the value it last computed at, so a per-frame query is O(1)
|
||||
// between real changes and never does work on idle frames.
|
||||
namespace Player::StatSignal {
|
||||
|
||||
// Bump the epoch — called from the aura / equipment hooks on a
|
||||
// player-affecting change.
|
||||
void Notify();
|
||||
|
||||
// Current epoch; increments on each `Notify()`. A consumer caches the value it
|
||||
// last recomputed at and recomputes only when this differs.
|
||||
uint32_t Epoch();
|
||||
|
||||
} // namespace Player::StatSignal
|
||||
@@ -76,6 +76,87 @@ static int __fastcall Script_GetQuestIDForLogIndex(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_QuestLog.GetLogIndexForQuestID(questID)` — the inverse of
|
||||
// `GetQuestIDForLogIndex`: the 1-based quest-log index of `questID`, or nil
|
||||
// if the quest isn't in the log. The index spans the full entry array
|
||||
// (headers included), matching `GetQuestIDForLogIndex` /
|
||||
// `GetQuestLogTitle`. Walks `VAR_QUEST_LOG_ENTRIES`, skipping header rows via
|
||||
// the `+8` header-pointer gate, and returns the first matching entry's index.
|
||||
static int __fastcall Script_GetLogIndexForQuestID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetLogIndexForQuestID(questID)");
|
||||
return 0;
|
||||
}
|
||||
const int target = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (target <= 0)
|
||||
return 0; // nil — no such quest
|
||||
|
||||
const int total = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_QUEST_LOG_ENTRY_COUNT));
|
||||
auto *base = reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_QUEST_LOG_ENTRIES));
|
||||
for (int i = 0; i < total; ++i) {
|
||||
auto *entry = base + i * Offsets::OFF_QUEST_LOG_ENTRY_STRIDE;
|
||||
if (*reinterpret_cast<const void *const *>(
|
||||
entry + Offsets::OFF_QUEST_LOG_ENTRY_HEADER_PTR) != nullptr)
|
||||
continue; // header row
|
||||
const int questID = *reinterpret_cast<const int *>(
|
||||
entry + Offsets::OFF_QUEST_LOG_ENTRY_QUEST_ID);
|
||||
if (questID == target) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1)); // 1-based
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0; // nil — quest not in the log
|
||||
}
|
||||
|
||||
// `C_QuestLog.GetHeaderIndexForQuest(questID)` — the 1-based log index of the
|
||||
// collapsible category header (zone / "Dungeon" / class sort, …) the quest
|
||||
// sits under, or nil if the quest isn't in the log or has no header above it.
|
||||
// The log is laid out header-then-its-quests, so we locate the quest's entry
|
||||
// and walk backwards to the nearest preceding header row (`+8` non-NULL).
|
||||
static int __fastcall Script_GetHeaderIndexForQuest(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetHeaderIndexForQuest(questID)");
|
||||
return 0;
|
||||
}
|
||||
const int target = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (target <= 0)
|
||||
return 0; // nil — no such quest
|
||||
|
||||
const int total = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_QUEST_LOG_ENTRY_COUNT));
|
||||
auto *base = reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_QUEST_LOG_ENTRIES));
|
||||
|
||||
// Locate the quest's entry.
|
||||
int questIndex = -1;
|
||||
for (int i = 0; i < total; ++i) {
|
||||
auto *entry = base + i * Offsets::OFF_QUEST_LOG_ENTRY_STRIDE;
|
||||
if (*reinterpret_cast<const void *const *>(
|
||||
entry + Offsets::OFF_QUEST_LOG_ENTRY_HEADER_PTR) != nullptr)
|
||||
continue; // header row
|
||||
if (*reinterpret_cast<const int *>(
|
||||
entry + Offsets::OFF_QUEST_LOG_ENTRY_QUEST_ID) == target) {
|
||||
questIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (questIndex < 0)
|
||||
return 0; // quest not in the log
|
||||
|
||||
// Walk back to the nearest preceding header.
|
||||
for (int i = questIndex - 1; i >= 0; --i) {
|
||||
auto *entry = base + i * Offsets::OFF_QUEST_LOG_ENTRY_STRIDE;
|
||||
if (*reinterpret_cast<const void *const *>(
|
||||
entry + Offsets::OFF_QUEST_LOG_ENTRY_HEADER_PTR) != nullptr) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1)); // 1-based
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0; // nil — quest sits above any header (shouldn't normally happen)
|
||||
}
|
||||
|
||||
// `C_QuestLog.IsOnQuest(questID)` — true iff `questID` is currently
|
||||
// in the player's quest log (incomplete OR ready-to-turn-in; the log
|
||||
// holds both). Walks `VAR_QUEST_LOG_ENTRIES` and matches against each
|
||||
@@ -148,6 +229,10 @@ static int __fastcall Script_IsUnitOnQuest(void *L) {
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_QuestLog", "GetQuestIDForLogIndex",
|
||||
&Script_GetQuestIDForLogIndex);
|
||||
Game::Lua::RegisterTableFunction("C_QuestLog", "GetLogIndexForQuestID",
|
||||
&Script_GetLogIndexForQuestID);
|
||||
Game::Lua::RegisterTableFunction("C_QuestLog", "GetHeaderIndexForQuest",
|
||||
&Script_GetHeaderIndexForQuest);
|
||||
Game::Lua::RegisterTableFunction("C_QuestLog", "IsOnQuest",
|
||||
&Script_IsOnQuest);
|
||||
Game::Lua::RegisterTableFunction("C_QuestLog", "IsUnitOnQuest",
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetSpellBonusDamage(school)` / `GetSpellBonusHealing()` — the local
|
||||
// player's flat spell-damage (per school) and healing bonus.
|
||||
//
|
||||
// **Spell damage is a direct field read.** The value lives in the CGPlayer
|
||||
// sub-struct at `[player + OFF_CGPLAYER_INFO]` (the +0xE68 blob), NOT the
|
||||
// broadcast UpdateField descriptor at `+0x110` — the descriptor copy of
|
||||
// this field is never sent to the client and reads zero (which sent an
|
||||
// earlier version of this module down a "derive it from gear" path). The
|
||||
// +0xE68 copy is the client's fully-computed value: `MOD_DAMAGE_DONE_POS[7]`
|
||||
// at `+0xFD4`, `MOD_DAMAGE_DONE_NEG[7]` at `+0xFF0`, one int32 per school;
|
||||
// net = POS − NEG. Because it's the finished value, gear + enchants + buffs
|
||||
// + talents + set bonuses are all already baked in.
|
||||
//
|
||||
// This mirrors nampower's `Script_GetSpellPower` (same base + offsets),
|
||||
// cross-verified in-game: a geared mage reads 613 generic across the magic
|
||||
// schools with Fire at 647 (a +34 fire-specific piece on top of the 613).
|
||||
//
|
||||
// **Healing has no such field** — vanilla 1.12 never had `MOD_HEALING_DONE`
|
||||
// (TBC addition; `PLAYER_FIELD_BYTES` follows `_PCT` directly), and nothing
|
||||
// client-side computes a healing total (verified in-game: toggling a pure
|
||||
// +44-healing item moved NO field in the +0xE68 struct). So
|
||||
// `GetSpellBonusHealing` is DERIVED in two parts, both read from `Spell.dbc`:
|
||||
//
|
||||
// 1. Flat healing — Σ `SPELL_AURA_MOD_HEALING_DONE` (aura 135) off gear
|
||||
// equip-effects, item enchants, random suffixes, and active buffs.
|
||||
// 2. Talent/buff stat→healing — the server's own `SpellBaseHealingBonusDone`
|
||||
// sums `SPELL_AURA_MOD_SPELL_HEALING_OF_STAT_PERCENT` (aura 175) ×
|
||||
// total Spirit / 100 over the unit's aura-175 auras. We mirror it: read
|
||||
// total Spirit from `UNIT_FIELD_STAT4` (descriptor +0x250), then sum
|
||||
// aura-175 percents over PASSIVE known spells (talents — Spiritual
|
||||
// Guidance, `Spell.dbc` ranks 14901/15028..15031 = 5/10/15/20/25%) plus
|
||||
// active buffs. Generic (any aura-175 talent/buff, incl. Turtle customs)
|
||||
// and robust — it reads the percent directly, no client-vs-server value
|
||||
// delta.
|
||||
//
|
||||
// An earlier attempt derived (2) as (spell_damage_field − derived_gear_damage)
|
||||
// — the part of the damage field gear doesn't explain. That was FRAGILE: the
|
||||
// field reflects values AFTER SpellMods, but our gear sum used raw DBC values.
|
||||
// Inner Fire (10951) is +60 in the DBC but +78 live (Improved Inner Fire, a
|
||||
// +15%/rank SpellMod), so an 18-point gap leaked into healing when that pure
|
||||
// +damage buff was gained. Reading aura 175 directly (above) sidesteps it.
|
||||
//
|
||||
// Residual: assumes talent ranks supersede (only the learned rank is known —
|
||||
// otherwise summed) and misses set-bonus healing granted via a set spell.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/StatAccum.h"
|
||||
#include "player/StatSignal.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Spell::BonusDamage {
|
||||
|
||||
namespace {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Spell damage — direct read from the CGPlayer +0xE68 sub-struct.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const uint8_t *PlayerFields() {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_CGPLAYER_INFO);
|
||||
}
|
||||
|
||||
// Net spell damage for a 0-based school (0=physical .. 6=arcane). POS is the
|
||||
// full per-school value (generic "+spell damage" is already folded into every
|
||||
// magic school by the server), NEG the signed-magnitude negative accumulator.
|
||||
long SpellDamageForSchool(int school0) {
|
||||
const uint8_t *pf = PlayerFields();
|
||||
if (pf == nullptr)
|
||||
return 0;
|
||||
const int32_t pos = *reinterpret_cast<const int32_t *>(
|
||||
pf + Offsets::OFF_PLAYER_FIELD_MOD_DAMAGE_DONE_POS + school0 * 4);
|
||||
const int32_t neg = *reinterpret_cast<const int32_t *>(
|
||||
pf + Offsets::OFF_PLAYER_FIELD_MOD_DAMAGE_DONE_NEG + school0 * 4);
|
||||
return static_cast<long>(pos) - static_cast<long>(neg);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Healing — derived (no client field exists in vanilla).
|
||||
//
|
||||
// Item flat +healing goes through the shared Item::StatAccum engine (base
|
||||
// record + on-equip spells + random suffix + applied enchants) — exactly the
|
||||
// ITEM_MOD_SPELL_HEALING_DONE_SHORT (aura 135) value GetItemStats reports, so
|
||||
// the item spell/suffix/enchant walk isn't re-implemented here. Buff / talent
|
||||
// healing (flat + Spirit/Armor conversions) is decoded from Spell.dbc below.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Flat +healing contributed by one equipped item, via Item::StatAccum.
|
||||
long ItemFlatHealing(const uint8_t *cgItem) {
|
||||
if (cgItem == nullptr)
|
||||
return 0;
|
||||
Item::StatAccum::Accum acc[Item::StatAccum::kCount];
|
||||
Item::StatAccum::Init(acc);
|
||||
|
||||
const int itemID = Item::ID::FromCGItem(cgItem);
|
||||
if (itemID > 0)
|
||||
Item::StatAccum::AccumulateRecord(
|
||||
acc, Item::StatAccum::FetchRecord(static_cast<uint32_t>(itemID)), +1);
|
||||
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
cgItem + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
if (desc != nullptr) {
|
||||
// Random suffix ("… of Restoration") + applied permanent (slot 0) and
|
||||
// temporary (slot 1) enchants (Healing Power, Mana Oil, …).
|
||||
Item::StatAccum::ApplyRandomSuffix(
|
||||
acc, *reinterpret_cast<const int32_t *>(
|
||||
desc + Offsets::OFF_DESCRIPTOR_RANDOM_PROPERTY), +1);
|
||||
for (int slot = 0; slot <= 1; ++slot)
|
||||
Item::StatAccum::ApplyEnchant(
|
||||
acc, *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_DESCRIPTOR_ENCHANTMENT_ID +
|
||||
slot * Offsets::DESCRIPTOR_ENCHANTMENT_SLOT_STRIDE), +1);
|
||||
}
|
||||
return Item::StatAccum::Value(acc, "ITEM_MOD_SPELL_HEALING_DONE_SHORT");
|
||||
}
|
||||
|
||||
// --- Buff / talent healing auras (flat + Spirit/Armor conversions) ---
|
||||
// One decoder for every healing aura a buff or passive talent can carry,
|
||||
// mirroring the server's SpellBaseHealingBonusDone:
|
||||
// 135 MOD_HEALING_DONE → flat += amount
|
||||
// 175 MOD_SPELL_HEALING_OF_STAT_PERCENT → += Spirit × amount / 100 (Spiritual Guidance)
|
||||
// 199 MOD_SPELL_HEALING_OF_ARMOR_PERCENT → += Armor × amount / 100 (Turtle Ironclad)
|
||||
// Amounts/percents are read straight from Spell.dbc — generic (any talent/buff
|
||||
// using these auras, incl. Turtle customs) and robust (no client-vs-server
|
||||
// value delta). `requirePassive` gates the bitmap walk to always-on talents,
|
||||
// so a *known but not-currently-cast* buff (Divine Spirit) isn't counted as
|
||||
// on; active buffs pass `requirePassive == false`.
|
||||
constexpr int kAuraModHealingDone = 135;
|
||||
constexpr int kAuraModHealingOfStatPercent = 175; // × Spirit
|
||||
constexpr int kAuraModHealingOfArmorPercent = 199; // × Armor (Turtle Ironclad)
|
||||
|
||||
void AddSpellHealingAuras(long &total, int spellID, int32_t spirit,
|
||||
int32_t armor, bool requirePassive) {
|
||||
if (spellID <= 0)
|
||||
return;
|
||||
const uint8_t *sp = DBC::Record(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT,
|
||||
static_cast<uint32_t>(spellID));
|
||||
if (sp == nullptr)
|
||||
return;
|
||||
if (requirePassive &&
|
||||
!(*reinterpret_cast<const uint32_t *>(sp + Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
|
||||
Offsets::SPELL_ATTR_PASSIVE))
|
||||
return;
|
||||
auto aura = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
auto base = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_BASE_POINTS);
|
||||
auto dice = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_BASE_DICE);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
const long amount = static_cast<long>(base[i]) + static_cast<long>(dice[i]);
|
||||
if (aura[i] == kAuraModHealingDone)
|
||||
total += amount;
|
||||
else if (aura[i] == kAuraModHealingOfStatPercent)
|
||||
total += static_cast<long>(spirit) * amount / 100;
|
||||
else if (aura[i] == kAuraModHealingOfArmorPercent)
|
||||
total += static_cast<long>(armor) * amount / 100;
|
||||
}
|
||||
}
|
||||
|
||||
long ComputeHealing() {
|
||||
long total = 0;
|
||||
|
||||
// Items — flat +healing via the shared StatAccum engine.
|
||||
for (int slot = 1; slot <= 19; ++slot)
|
||||
total += ItemFlatHealing(Item::Location::ResolveEquipmentSlot(slot));
|
||||
|
||||
// Player Spirit / Armor for the aura-175 / aura-199 conversions.
|
||||
const uint8_t *desc = Unit::Identity::PlayerDescriptor();
|
||||
const int32_t spirit = desc ? *reinterpret_cast<const int32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_STAT_SPIRIT) : 0;
|
||||
const int32_t armor = desc ? *reinterpret_cast<const int32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_RESISTANCE_ARMOR) : 0;
|
||||
|
||||
// Active buffs — flat healing + Spirit/Armor conversions, one pass.
|
||||
auto *table = reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_BUFF_TABLE));
|
||||
for (int i = 0; i < Offsets::PLAYER_BUFF_TABLE_COUNT; ++i) {
|
||||
const uint8_t *entry = table + i * Offsets::PLAYER_BUFF_ENTRY_STRIDE;
|
||||
if (*reinterpret_cast<const int32_t *>(
|
||||
entry + Offsets::OFF_PLAYER_BUFF_SLOT_CODE) < 0)
|
||||
continue;
|
||||
AddSpellHealingAuras(total, *reinterpret_cast<const int32_t *>(
|
||||
entry + Offsets::OFF_PLAYER_BUFF_SPELL_ID),
|
||||
spirit, armor, /*requirePassive=*/false);
|
||||
}
|
||||
|
||||
// Passive talents — Spirit/Armor conversions (Spiritual Guidance, Ironclad)
|
||||
// and any passive flat healing, via the known-spell bitmap (bounded by
|
||||
// spellCount). The passive gate assumes ranks supersede (only the learned
|
||||
// rank is known), so no per-rank dedup is needed.
|
||||
auto *bitmap = *reinterpret_cast<const uint32_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_SPELL_BITMAP));
|
||||
const int spellCount = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SPELL_RECORD_COUNT));
|
||||
if (bitmap != nullptr && spellCount > 0) {
|
||||
for (int id = 1; id <= spellCount; ++id)
|
||||
if (bitmap[id >> 5] & (1u << (id & 31)))
|
||||
AddSpellHealingAuras(total, id, spirit, armor, /*requirePassive=*/true);
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
// Event-driven cache for the derived healing sum. The walk (StatAccum over 19
|
||||
// items + the buff table + the known-spell bitmap) is expensive, so we
|
||||
// recompute it ONLY when the player's stat inputs change — Player::StatSignal
|
||||
// is bumped from the aura and equipment hooks, so a per-frame
|
||||
// GetSpellBonusHealing poll is O(1) between real gear/buff changes.
|
||||
//
|
||||
// Every input to the sum is now event-driven: the aura, equipment, and
|
||||
// spell-learn/unlearn hooks all bump Player::StatSignal (buffs, gear, and
|
||||
// talent respecs respectively). This coarse world-tick recompute (at most once
|
||||
// per kFallbackTicks frames, ~4s) is a pure backstop against a missed signal —
|
||||
// e.g. a player aura the object-pointer guard in aura/Source didn't match —
|
||||
// bounding staleness rather than being the primary mechanism.
|
||||
constexpr int kFallbackTicks = 256;
|
||||
uint32_t g_cachedEpoch = 0; // 0 = never computed (epoch starts at 1)
|
||||
int g_ticksSinceCompute = 0;
|
||||
long g_cachedHealing = 0;
|
||||
|
||||
void OnTick() { ++g_ticksSinceCompute; }
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
|
||||
long CurrentHealing() {
|
||||
const uint32_t epoch = Player::StatSignal::Epoch();
|
||||
if (epoch != g_cachedEpoch || g_ticksSinceCompute >= kFallbackTicks) {
|
||||
g_cachedHealing = ComputeHealing();
|
||||
g_cachedEpoch = epoch;
|
||||
g_ticksSinceCompute = 0;
|
||||
}
|
||||
return g_cachedHealing;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua bindings.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// `GetSpellBonusDamage(school)` → number. `school` is 1-based (1=Physical
|
||||
// through 7=Arcane). Raises the usage error for a missing / out-of-range
|
||||
// school, matching 3.3.5's Script_GetSpellBonusDamage (0x0060E310).
|
||||
int __fastcall Script_GetSpellBonusDamage(void *L) {
|
||||
const int school =
|
||||
Game::Lua::IsNumber(L, 1) ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : 0;
|
||||
const unsigned idx = static_cast<unsigned>(school - 1);
|
||||
if (idx >= static_cast<unsigned>(Offsets::SPELL_SCHOOL_COUNT)) {
|
||||
Game::Lua::Error(L, "Usage: GetSpellBonusDamage(school)");
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(SpellDamageForSchool(static_cast<int>(idx))));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `GetSpellBonusHealing()` → number. The player's flat healing bonus (derived).
|
||||
int __fastcall Script_GetSpellBonusHealing(void *L) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(CurrentHealing()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetSpellBonusDamage", &Script_GetSpellBonusDamage);
|
||||
Game::Lua::RegisterGlobalFunction("GetSpellBonusHealing", &Script_GetSpellBonusHealing);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Spell::BonusDamage
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "aura/Data.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -37,15 +38,8 @@ namespace Spell::Cancel {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using CancelAuraSend_t = void(__fastcall *)(int spellID);
|
||||
|
||||
const uint8_t *ResolvePlayer() {
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN));
|
||||
return static_cast<const uint8_t *>(fn("player"));
|
||||
}
|
||||
|
||||
// Pre-validates `spellID` against Spell.dbc and ships the cancel
|
||||
// packet. The engine's sender unconditionally derefs `[record+0x1C]`
|
||||
// after a NULL-on-OOR record lookup, so skipping the
|
||||
@@ -71,7 +65,7 @@ int __fastcall Script_CancelSpellByName(void *L) {
|
||||
const char *targetName = Game::Lua::ToString(L, 1);
|
||||
if (targetName == nullptr || *targetName == '\0')
|
||||
return 0;
|
||||
const uint8_t *player = ResolvePlayer();
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
// Walk the player's buff range only (slots 0..31). Modern
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "player/StatSignal.h"
|
||||
#include "spell/Arg.h"
|
||||
#include "spell/Lookup.h"
|
||||
|
||||
@@ -586,6 +587,35 @@ static int __fastcall Script_C_Spell_IsSpellHelpful(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- Known-spell-bitmap change signal ------------------------------------
|
||||
// Learning/unlearning a spell rewrites the bitmap GetSpellBonusHealing scans
|
||||
// for talent conversions (Spiritual Guidance / Ironclad). Co-hook the engine's
|
||||
// learn + unlearn writers and bump Player::StatSignal, so a talent respec
|
||||
// invalidates the healing cache immediately — the same event-driven path as
|
||||
// aura/equipment changes, no polling. Both fire only at login and on
|
||||
// learn/unlearn (never per-frame), so they're safe co-hook targets.
|
||||
using LearnSpell_t = void(__fastcall *)(uint32_t spellID, int notify,
|
||||
uint32_t replacedSpellID);
|
||||
static LearnSpell_t g_origLearnSpell = nullptr;
|
||||
static void __fastcall LearnSpell_h(uint32_t spellID, int notify,
|
||||
uint32_t replacedSpellID) {
|
||||
g_origLearnSpell(spellID, notify, replacedSpellID);
|
||||
Player::StatSignal::Notify();
|
||||
}
|
||||
static const Game::HookAutoRegister _hookLearnSpell{
|
||||
Offsets::FUN_LEARN_SPELL, reinterpret_cast<void *>(&LearnSpell_h),
|
||||
reinterpret_cast<void **>(&g_origLearnSpell)};
|
||||
|
||||
using UnlearnSpell_t = void(__fastcall *)(uint32_t spellID, int param2);
|
||||
static UnlearnSpell_t g_origUnlearnSpell = nullptr;
|
||||
static void __fastcall UnlearnSpell_h(uint32_t spellID, int param2) {
|
||||
g_origUnlearnSpell(spellID, param2);
|
||||
Player::StatSignal::Notify();
|
||||
}
|
||||
static const Game::HookAutoRegister _hookUnlearnSpell{
|
||||
Offsets::FUN_UNLEARN_SPELL, reinterpret_cast<void *>(&UnlearnSpell_h),
|
||||
reinterpret_cast<void **>(&g_origUnlearnSpell)};
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetSpellInfo", &Script_GetSpellInfo);
|
||||
Game::Lua::RegisterGlobalFunction("GetSpellLink", &Script_GetSpellLink);
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace Spell::SkillLine {
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using SkillLineToSlot_t = int(__thiscall *)(void *player, uint32_t skillLineID);
|
||||
|
||||
// Reads UNIT_FIELD_BYTES_0 from the local player's descriptor. Same
|
||||
// pattern Spell::Level::PlayerDescriptor uses. Returns (0, 0) when the
|
||||
@@ -171,9 +172,109 @@ int __fastcall Script_GetSpellSkillLine(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `C_SpellBook.GetSkillLineName(skillLineID) → name`
|
||||
//
|
||||
// Localized `SkillLine.dbc::Name` for a skill-line id — the id → name
|
||||
// half of the bridge addons build by hand. Pure DBC read: returns the
|
||||
// name for ANY valid skill line (Herbalism = 182, Mining = 186, Swords,
|
||||
// Defense, …) whether or not the player has learned it. `nil` for
|
||||
// non-numeric / non-positive input or an id with no SkillLine.dbc row.
|
||||
//
|
||||
// Pairs with `GetSkillLineRank` (the learned-rank lookup): name comes
|
||||
// from the DBC, rank from the player's live skill list.
|
||||
int __fastcall Script_GetSkillLineName(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const int id = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *name =
|
||||
(id > 0) ? DBC::LocalizedField(Offsets::VAR_SKILL_LINE_RECORDS,
|
||||
Offsets::VAR_SKILL_LINE_COUNT,
|
||||
static_cast<uint32_t>(id),
|
||||
Offsets::OFF_SKILL_LINE_NAME)
|
||||
: nullptr;
|
||||
if (name == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushString(L, name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_SpellBook.GetSkillLineRank(skillLineID) → curRank, maxRank, modifier`
|
||||
//
|
||||
// The local player's rank in a skill line, keyed by `SkillLine.dbc` id.
|
||||
// Vanilla's `GetSkillLineInfo(index)` only walks the skill window by
|
||||
// position and returns skills by *name*, so an addon starting from a
|
||||
// SkillLine.dbc id (a quest's `requiredSkill`, "Herbalism" = 182, …) has
|
||||
// to map id → localized name and string-match every skill line to recover
|
||||
// the rank. This reads the player's live skill list keyed by id directly.
|
||||
//
|
||||
// Returns the same values `GetSkillLineInfo` reports for the line:
|
||||
// curRank — current skill value (base, no temp bonus)
|
||||
// maxRank — the line's cap at the player's level
|
||||
// modifier — temporary bonus (weapon oils, buffs; 0 if none)
|
||||
//
|
||||
// Returns `nil` (nothing) when the player hasn't learned the line
|
||||
// (`FUN_SKILL_LINE_TO_SLOT` reports no slot), for bad input, or before
|
||||
// the player object / skill sub-struct is resident (character select).
|
||||
// Reads the same CGPlayer `+0xE68` skill table `Player::CanUseItem`'s
|
||||
// RequiredSkill gate uses; ranks verified there (Tailoring 261/300, …).
|
||||
int __fastcall Script_GetSkillLineRank(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const int skillLineID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (skillLineID <= 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<uint8_t *>(resolve("player"));
|
||||
if (player == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto toSlot = reinterpret_cast<SkillLineToSlot_t>(Offsets::FUN_SKILL_LINE_TO_SLOT);
|
||||
const int slot = toSlot(player, static_cast<uint32_t>(skillLineID));
|
||||
if (slot < 0) { // player doesn't have the skill line
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto *sub = *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_CGPLAYER_INFO);
|
||||
if (sub == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint8_t *rec = sub + Offsets::OFF_SKILL_INFO_TABLE +
|
||||
slot * Offsets::SKILL_INFO_STRIDE;
|
||||
const uint32_t cur =
|
||||
*reinterpret_cast<const uint16_t *>(rec + Offsets::OFF_SKILL_INFO_CUR);
|
||||
const uint32_t max =
|
||||
*reinterpret_cast<const uint16_t *>(rec + Offsets::OFF_SKILL_INFO_MAX);
|
||||
const uint32_t modifier =
|
||||
*reinterpret_cast<const uint16_t *>(rec + Offsets::OFF_SKILL_INFO_BONUS);
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(cur));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(max));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(modifier));
|
||||
return 3;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_SpellBook", "GetSpellSkillLine",
|
||||
&Script_GetSpellSkillLine);
|
||||
Game::Lua::RegisterTableFunction("C_SpellBook", "GetSkillLineName",
|
||||
&Script_GetSkillLineName);
|
||||
Game::Lua::RegisterTableFunction("C_SpellBook", "GetSkillLineRank",
|
||||
&Script_GetSkillLineRank);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_TaxiMap.GetTaxiNodesForMap(mapID)` (static) and
|
||||
// `C_TaxiMap.GetAllTaxiNodes([uiMapID])` (live) — backports of the retail
|
||||
// taxi-map node enumerators, plus the `Enum.FlightPathFaction` /
|
||||
// `Enum.FlightPathState` tables so retail code ports unchanged.
|
||||
//
|
||||
// - **GetTaxiNodesForMap** reads `TaxiNodes.dbc` directly: every flight point
|
||||
// on a continent (or all continents with no arg), faction-tagged,
|
||||
// regardless of discovery — the static DB a zone-map addon (pfQuest's
|
||||
// `pfDB.meta.flight`) wants. No flight master needed. Non-flight rows are filtered out — the DBC also holds transports/
|
||||
// boats/zeppelins (no flight mount) and non-selectable markers like
|
||||
// Northshire Abbey (no connecting flight path); a real flight master has a
|
||||
// mount AND is a TaxiPath endpoint.
|
||||
// - **GetAllTaxiNodes** wraps the open flight master's live reachable-node
|
||||
// list (the legacy NumTaxiNodes/TaxiNode* globals' backing state): the
|
||||
// nodes you can actually fly to right now, with per-node reachability and
|
||||
// the slot index `TakeTaxiNode` takes. Empty when no taxi map is open.
|
||||
//
|
||||
// Ship both: static/DBC for databases + overlays, live/session for a custom
|
||||
// flight-map UI. See docs/API.md for the field-by-field contract and which
|
||||
// fields are retail vs ClassicAPI extensions.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "map/Area.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Taxi::Map {
|
||||
|
||||
namespace {
|
||||
|
||||
// Enum.FlightPathFaction (retail values).
|
||||
enum { FACTION_NEUTRAL = 0, FACTION_HORDE = 1, FACTION_ALLIANCE = 2 };
|
||||
// Enum.FlightPathState (retail values).
|
||||
enum { STATE_CURRENT = 0, STATE_REACHABLE = 1, STATE_UNREACHABLE = 2 };
|
||||
|
||||
float FloatField(const uint8_t *rec, int off) {
|
||||
return *reinterpret_cast<const float *>(rec + off);
|
||||
}
|
||||
int IntField(const uint8_t *rec, int off) {
|
||||
return *reinterpret_cast<const int *>(rec + off);
|
||||
}
|
||||
|
||||
// --- name-based zone resolution -------------------------------------------
|
||||
//
|
||||
// A taxi node's name is "Location, Zone" (e.g. "Chillwind Camp, Western
|
||||
// Plaguelands"), so the zone is authoritative in the data itself — which
|
||||
// beats any geometric world->zone guess at genuine border interleaving that
|
||||
// the WorldMapArea/overlay boxes can't separate (Chillwind sits by Strahnbrad,
|
||||
// deep in Alterac's overlay box, but is a WPL node). We resolve the name
|
||||
// suffix against AreaTable and fall back to the geometric resolver only when a
|
||||
// node has no ", Zone" suffix or the zone name doesn't match.
|
||||
|
||||
char AsciiLower(char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; }
|
||||
|
||||
bool IEqual(const char *a, const char *b) {
|
||||
while (*a && *b) {
|
||||
if (AsciiLower(*a) != AsciiLower(*b))
|
||||
return false;
|
||||
++a;
|
||||
++b;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
// True if `name` begins with `prefix` followed by a space — a word-prefix, so
|
||||
// the abbreviated taxi suffixes ("Redridge", "Arathi") match the full
|
||||
// AreaTable names ("Redridge Mountains", "Arathi Highlands").
|
||||
bool IStartsWord(const char *name, const char *prefix) {
|
||||
while (*prefix) {
|
||||
if (!*name || AsciiLower(*name) != AsciiLower(*prefix))
|
||||
return false;
|
||||
++name;
|
||||
++prefix;
|
||||
}
|
||||
return *name == ' ';
|
||||
}
|
||||
|
||||
// Pointer into `name` at the zone part (after the last ", "), or null when the
|
||||
// name has no ", " separator.
|
||||
const char *ZoneSuffix(const char *name) {
|
||||
const char *best = nullptr;
|
||||
if (name != nullptr)
|
||||
for (const char *p = name; p[0] != '\0'; ++p)
|
||||
if (p[0] == ',' && p[1] == ' ')
|
||||
best = p + 2;
|
||||
return best;
|
||||
}
|
||||
|
||||
// Copies the location part (before the FIRST ", ") of `name` into `out`,
|
||||
// null-terminated. Returns `out`, or null when `name` has no ", " separator.
|
||||
// This is the "Orgrimmar" in "Orgrimmar, Durotar" — a capital's own city map
|
||||
// when it's a mapped area, distinct from its outdoor region.
|
||||
const char *LocationPrefix(const char *name, char *out, int outSize) {
|
||||
if (name == nullptr)
|
||||
return nullptr;
|
||||
for (int i = 0; name[i] != '\0'; ++i) {
|
||||
if (name[i] == ',' && name[i + 1] == ' ') {
|
||||
int n = (i < outSize - 1) ? i : outSize - 1;
|
||||
for (int j = 0; j < n; ++j)
|
||||
out[j] = name[j];
|
||||
out[n] = '\0';
|
||||
return out;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// AreaTable areaID for a zone name: exact (case-insensitive) match preferred,
|
||||
// else the first word-prefix match. 0 when nothing matches.
|
||||
int ResolveZoneByName(const char *zone) {
|
||||
if (zone == nullptr || *zone == '\0')
|
||||
return 0;
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_AREATABLE_COUNT);
|
||||
int prefixHit = 0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const char *nm = DBC::LocalizedField(Offsets::VAR_AREATABLE_RECORDS,
|
||||
Offsets::VAR_AREATABLE_COUNT,
|
||||
static_cast<uint32_t>(id),
|
||||
Offsets::OFF_AREATABLE_NAMES);
|
||||
if (nm == nullptr)
|
||||
continue;
|
||||
if (IEqual(nm, zone))
|
||||
return id; // exact wins outright
|
||||
if (prefixHit == 0 && IStartsWord(nm, zone))
|
||||
prefixHit = id;
|
||||
}
|
||||
return prefixHit;
|
||||
}
|
||||
|
||||
// Pushes { x = px, y = py } as a subtable field `key` of the table on top.
|
||||
void PushPosition(void *L, const char *key, double px, double py) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "x", px);
|
||||
Game::Lua::SetFieldNumber(L, "y", py);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Walks TaxiPath.dbc into two per-node-id flags:
|
||||
// endpoint[] — the node is either end of some flight path. A real flight
|
||||
// master always is; the fake rows (Northshire Abbey, spurious
|
||||
// duplicate hub rows, standalone markers like "Eastern
|
||||
// Plaguelands"/"Naxxramas") connect to nothing.
|
||||
// dest[] — the node is a path DESTINATION (in-degree > 0), i.e. you can
|
||||
// fly TO it. In-degree 0 marks a departure-only node reachable
|
||||
// only by other means: druid Teleport (Nighthaven, Moonglade),
|
||||
// a boat dock (Stormwind Harbor), etc.
|
||||
// ids >= `size` are ignored (the vanilla table maxes ~122, so the fixed buffer
|
||||
// is ample). Returns false when the path table isn't loaded.
|
||||
constexpr int kMaxTaxiNodeID = 2048;
|
||||
bool BuildTaxiGraph(bool *endpoint, bool *dest, int size) {
|
||||
const int count = *reinterpret_cast<const int *>(Offsets::VAR_TAXIPATH_COUNT);
|
||||
if (count <= 0)
|
||||
return false;
|
||||
for (int i = 1; i <= count; ++i) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_TAXIPATH_RECORDS,
|
||||
Offsets::VAR_TAXIPATH_COUNT,
|
||||
static_cast<uint32_t>(i));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
const int from = IntField(rec, Offsets::OFF_TAXIPATH_FROM);
|
||||
const int to = IntField(rec, Offsets::OFF_TAXIPATH_TO);
|
||||
if (from >= 0 && from < size)
|
||||
endpoint[from] = true;
|
||||
if (to >= 0 && to < size) {
|
||||
endpoint[to] = true;
|
||||
dest[to] = true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// `C_TaxiMap.GetTaxiNodesForMap([mapID])` — see module comment. A numeric
|
||||
// `mapID` filters to that continent; omitted / non-number returns every
|
||||
// flight master on every continent.
|
||||
int __fastcall Script_GetTaxiNodesForMap(void *L) {
|
||||
const bool filterMap = Game::Lua::IsNumber(L, 1);
|
||||
const int wantMap =
|
||||
filterMap ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : -1;
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
// Real flight masters are TaxiPath endpoints; `dest` marks the ones a
|
||||
// path leads to (in-degree > 0) for the `reachable` field.
|
||||
bool endpoint[kMaxTaxiNodeID] = {};
|
||||
bool dest[kMaxTaxiNodeID] = {};
|
||||
BuildTaxiGraph(endpoint, dest, kMaxTaxiNodeID);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_TAXINODES_COUNT);
|
||||
int outIdx = 0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_TAXINODES_RECORDS,
|
||||
Offsets::VAR_TAXINODES_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
const int mapID = IntField(rec, Offsets::OFF_TAXINODE_MAP_ID);
|
||||
if (filterMap && mapID != wantMap)
|
||||
continue;
|
||||
|
||||
// Two filters isolate real flight masters from the DBC's non-flight
|
||||
// rows: (1) at least one flight mount — drops transports/boats/
|
||||
// zeppelins/instance markers (both mounts 0); (2) the node is a
|
||||
// TaxiPath endpoint — drops Northshire Abbey, spurious duplicate hub
|
||||
// rows, and standalone markers that connect to no flight path.
|
||||
const int horde = IntField(rec, Offsets::OFF_TAXINODE_HORDE_MOUNT);
|
||||
const int alliance = IntField(rec, Offsets::OFF_TAXINODE_ALLIANCE_MOUNT);
|
||||
if (horde == 0 && alliance == 0)
|
||||
continue;
|
||||
if (id >= kMaxTaxiNodeID || !endpoint[id])
|
||||
continue;
|
||||
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_TAXINODES_RECORDS,
|
||||
Offsets::VAR_TAXINODES_COUNT,
|
||||
static_cast<uint32_t>(id),
|
||||
Offsets::OFF_TAXINODE_NAME);
|
||||
// The one path-connected, mounted row that still isn't a flight
|
||||
// master: Blizzard's internal "Generic, World target" zeppelin-route
|
||||
// marker. It's the sole such placeholder across all continents.
|
||||
if (name != nullptr && std::strncmp(name, "Generic", 7) == 0)
|
||||
continue;
|
||||
|
||||
const int faction = (horde != 0 && alliance != 0) ? FACTION_NEUTRAL
|
||||
: (alliance != 0) ? FACTION_ALLIANCE
|
||||
: FACTION_HORDE;
|
||||
|
||||
const float x = FloatField(rec, Offsets::OFF_TAXINODE_X);
|
||||
const float y = FloatField(rec, Offsets::OFF_TAXINODE_Y);
|
||||
const float z = FloatField(rec, Offsets::OFF_TAXINODE_Z);
|
||||
|
||||
outIdx += 1;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(outIdx));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "nodeID", id);
|
||||
Game::Lua::SetFieldString(L, "name", name ? name : "");
|
||||
Game::Lua::SetFieldNumber(L, "faction", faction);
|
||||
// reachable: a flight path leads to this node (in-degree > 0). False
|
||||
// marks a departure-only node reachable only by other means — druid
|
||||
// Teleport (Nighthaven, Moonglade) or a boat dock (Stormwind Harbor);
|
||||
// hide those from a non-class-specific "where can I fly" map.
|
||||
Game::Lua::SetFieldBool(L, "reachable", id < kMaxTaxiNodeID && dest[id]);
|
||||
Game::Lua::SetFieldNumber(L, "mapID", mapID);
|
||||
Game::Lua::SetFieldNumber(L, "x", x);
|
||||
Game::Lua::SetFieldNumber(L, "y", y);
|
||||
Game::Lua::SetFieldNumber(L, "z", z);
|
||||
|
||||
// position: 0-1 on the continent map (retail-accurate). Falls back to
|
||||
// {0,0} when the continent has no usable WorldMapArea row (instance
|
||||
// maps); zone-map addons use areaID + mapX/mapY below instead.
|
||||
double px = 0.0, py = 0.0;
|
||||
::Map::Area::ContinentPercent(mapID, x, y, &px, &py);
|
||||
PushPosition(L, "position", px, py);
|
||||
|
||||
// Derived zone anchor (areaID + 0-100 zone-relative percent), from the
|
||||
// node's own name — authoritative, and right at borders geometry can't
|
||||
// separate. Two candidate zones from the name, each accepted only if
|
||||
// its WorldMapArea rect actually contains the point:
|
||||
// 1. the LOCATION ("Orgrimmar" in "Orgrimmar, Durotar") — a capital's
|
||||
// own city map, so the flight master pins on the city, not the
|
||||
// surrounding region;
|
||||
// 2. else the ZONE suffix ("Western Plaguelands") — the outdoor zone
|
||||
// for a normal node (whose location is an unmapped subzone).
|
||||
// Falls back to the geometric landmass resolver when neither name part
|
||||
// resolves to a containing zone (no ", Zone" suffix, unknown name).
|
||||
int areaID = 0;
|
||||
double mapX = 0.0, mapY = 0.0;
|
||||
bool haveZone = false;
|
||||
char locBuf[128];
|
||||
const int locArea = ResolveZoneByName(LocationPrefix(name, locBuf, sizeof(locBuf)));
|
||||
const int zoneArea = ResolveZoneByName(ZoneSuffix(name));
|
||||
if (locArea > 0 && ::Map::Area::PercentInZone(locArea, x, y, &mapX, &mapY)) {
|
||||
areaID = locArea; // capital / mapped location -> its own city map
|
||||
haveZone = true;
|
||||
} else if (zoneArea > 0 &&
|
||||
::Map::Area::PercentInZone(zoneArea, x, y, &mapX, &mapY)) {
|
||||
areaID = zoneArea; // outdoor zone from the name suffix
|
||||
haveZone = true;
|
||||
} else {
|
||||
haveZone = ::Map::Area::ZonePercent(mapID, x, y, &areaID, &mapX, &mapY);
|
||||
}
|
||||
if (haveZone) {
|
||||
Game::Lua::SetFieldNumber(L, "areaID", areaID);
|
||||
Game::Lua::SetFieldNumber(L, "mapX", mapX);
|
||||
Game::Lua::SetFieldNumber(L, "mapY", mapY);
|
||||
}
|
||||
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
using TaxiNodeType_t = const char *(__fastcall *)(uint32_t slot0Based);
|
||||
|
||||
int StateFromType(const char *type) {
|
||||
if (type == nullptr)
|
||||
return STATE_UNREACHABLE;
|
||||
if (std::strcmp(type, "CURRENT") == 0)
|
||||
return STATE_CURRENT;
|
||||
if (std::strcmp(type, "REACHABLE") == 0)
|
||||
return STATE_REACHABLE;
|
||||
return STATE_UNREACHABLE; // DISTANT / NONE / other
|
||||
}
|
||||
|
||||
// `C_TaxiMap.GetAllTaxiNodes([uiMapID])` — see module comment. The uiMapID
|
||||
// arg is accepted for retail signature parity but ignored (vanilla has a
|
||||
// single active taxi map — the open flight master's).
|
||||
int __fastcall Script_GetAllTaxiNodes(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_TAXI_NODE_COUNT);
|
||||
if (count <= 0)
|
||||
return 1; // no taxi map open
|
||||
|
||||
auto typeFn = reinterpret_cast<TaxiNodeType_t>(Offsets::FUN_TAXI_NODE_TYPE);
|
||||
const auto *arrayBase =
|
||||
reinterpret_cast<const uint8_t *>(Offsets::VAR_TAXI_NODE_ARRAY);
|
||||
|
||||
for (int slot = 0; slot < count; ++slot) {
|
||||
const uint8_t *slotRec = arrayBase + slot * Offsets::TAXI_NODE_STRIDE;
|
||||
const uint8_t *dbcRec = *reinterpret_cast<const uint8_t *const *>(
|
||||
slotRec + Offsets::OFF_TAXI_SLOT_RECORD);
|
||||
const float x = FloatField(slotRec, Offsets::OFF_TAXI_SLOT_X);
|
||||
const float y = FloatField(slotRec, Offsets::OFF_TAXI_SLOT_Y);
|
||||
|
||||
int nodeID = 0;
|
||||
const char *name = nullptr;
|
||||
if (dbcRec != nullptr) {
|
||||
nodeID = IntField(dbcRec, 0); // TaxiNodes.dbc id @ +0
|
||||
name = DBC::LocalizedField(Offsets::VAR_TAXINODES_RECORDS,
|
||||
Offsets::VAR_TAXINODES_COUNT,
|
||||
static_cast<uint32_t>(nodeID),
|
||||
Offsets::OFF_TAXINODE_NAME);
|
||||
}
|
||||
const int state = StateFromType(typeFn(static_cast<uint32_t>(slot)));
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot + 1));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "slotIndex", slot + 1); // TakeTaxiNode arg
|
||||
Game::Lua::SetFieldString(L, "name", name ? name : "");
|
||||
Game::Lua::SetFieldNumber(L, "nodeID", nodeID);
|
||||
Game::Lua::SetFieldNumber(L, "state", state);
|
||||
PushPosition(L, "position", x, y); // flight-map 0-1, matches retail
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
static const Game::Lua::EnumIntegerEntry kFaction[] = {
|
||||
{"Neutral", FACTION_NEUTRAL},
|
||||
{"Horde", FACTION_HORDE},
|
||||
{"Alliance", FACTION_ALLIANCE},
|
||||
};
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "FlightPathFaction", kFaction, 3);
|
||||
|
||||
static const Game::Lua::EnumIntegerEntry kState[] = {
|
||||
{"Current", STATE_CURRENT},
|
||||
{"Reachable", STATE_REACHABLE},
|
||||
{"Unreachable", STATE_UNREACHABLE},
|
||||
};
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "FlightPathState", kState, 3);
|
||||
|
||||
Game::Lua::RegisterTableFunction("C_TaxiMap", "GetTaxiNodesForMap",
|
||||
&Script_GetTaxiNodesForMap);
|
||||
Game::Lua::RegisterTableFunction("C_TaxiMap", "GetAllTaxiNodes",
|
||||
&Script_GetAllTaxiNodes);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Taxi::Map
|
||||
@@ -0,0 +1,87 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `UnitCreatureFamilyID(unit)` — the numeric CreatureFamily id (the
|
||||
// `CreatureFamily.dbc` row: 1 = Wolf, 3 = Cat, 4 = Bear, …) for a unit,
|
||||
// or `nil` when the unit has no family.
|
||||
//
|
||||
// The stock `UnitCreatureFamily(unit)` returns only the *localized name*
|
||||
// ("Wolf", "Chat"), which is awkward to compare and locale-dependent. This
|
||||
// extension exposes the raw id addons actually want for stable checks
|
||||
// (tameable-pet logic, family-specific ability tables, etc.) — the same
|
||||
// value that indexes `CreatureFamily.dbc`.
|
||||
//
|
||||
// This is exactly the id the engine's `Script_UnitCreatureFamily`
|
||||
// (`0x0051A310`) reads before its DBC name lookup: its family helper
|
||||
// `FUN_006055e0` returns `[unit + 0xB30] ? [row + 0x1C] : 0`, then the id is
|
||||
// bounds-checked against `CreatureFamily.dbc` and used to push the localized
|
||||
// name. We stop at the id. `nil` for:
|
||||
// - Unresolvable unit token (`target` with nothing targeted, empty
|
||||
// `partyN` slot, etc.).
|
||||
// - Players and non-beast NPCs — family id is 0 (`UnitCreatureFamily`
|
||||
// returns nil for these too).
|
||||
// - NPCs whose creature data hasn't synced yet (no +0xB30 row).
|
||||
//
|
||||
// Raises a Lua error on a missing / non-string `unit` argument — same
|
||||
// semantics as `UnitCreatureFamily` / `UnitSubName`.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::CreatureFamily {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
int __fastcall Script_UnitCreatureFamilyID(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitCreatureFamilyID(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
if (token == nullptr)
|
||||
return 0;
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
|
||||
auto *cacheRow = *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_UNIT_CREATURE_CACHE_ROW);
|
||||
if (cacheRow == nullptr)
|
||||
return 0; // unsynced NPC / player unit
|
||||
|
||||
const int familyID = *reinterpret_cast<const int *>(
|
||||
cacheRow + Offsets::OFF_CREATURE_CACHE_FAMILY);
|
||||
if (familyID <= 0)
|
||||
return 0; // no family (players, non-beast NPCs)
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(familyID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitCreatureFamilyID",
|
||||
&Script_UnitCreatureFamilyID);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Unit::CreatureFamily
|
||||
@@ -0,0 +1,81 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `UnitCreatureTypeID(unit)` — the numeric CreatureType id (the
|
||||
// `CreatureType.dbc` row: 1 = Beast, 3 = Demon, 7 = Humanoid, …) for a
|
||||
// unit, or `nil` when it has none.
|
||||
//
|
||||
// The stock `UnitCreatureType(unit)` returns only the *localized name*
|
||||
// ("Humanoid", "Humanoïde"), which is awkward to compare and locale-
|
||||
// dependent. This extension exposes the raw id — the twin of
|
||||
// `UnitCreatureFamilyID`.
|
||||
//
|
||||
// It's exactly the value the engine's `Script_UnitCreatureType`
|
||||
// (`0x0051A280`) resolves before its DBC name lookup, so we just call that
|
||||
// same inner helper (`FUN_UNIT_CREATURE_TYPE`). The helper handles all three
|
||||
// engine paths itself — a display-override, the creature-cache type, and the
|
||||
// player-race type (players resolve to Humanoid via their race) — so a
|
||||
// straight call matches `UnitCreatureType` for every unit kind. `nil` for:
|
||||
// - Unresolvable unit token (`target` with nothing targeted, etc.).
|
||||
// - A unit with no resolvable type (helper returns 0).
|
||||
//
|
||||
// Raises a Lua error on a missing / non-string `unit` argument — same
|
||||
// semantics as `UnitCreatureType` / `UnitCreatureFamilyID`.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::CreatureType {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using CreatureType_t = int(__fastcall *)(void *unit);
|
||||
|
||||
int __fastcall Script_UnitCreatureTypeID(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitCreatureTypeID(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
if (token == nullptr)
|
||||
return 0;
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
void *unit = resolve(token);
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
|
||||
auto creatureType = reinterpret_cast<CreatureType_t>(
|
||||
Offsets::FUN_UNIT_CREATURE_TYPE);
|
||||
const int typeID = creatureType(unit);
|
||||
if (typeID <= 0)
|
||||
return 0; // no resolvable type
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(typeID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitCreatureTypeID",
|
||||
&Script_UnitCreatureTypeID);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Unit::CreatureType
|
||||
@@ -119,6 +119,13 @@ const uint8_t *PlayerObject() {
|
||||
/*line*/ 0));
|
||||
}
|
||||
|
||||
const uint8_t *PlayerInventoryManager() {
|
||||
const uint8_t *player = PlayerObject();
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
|
||||
}
|
||||
|
||||
const uint8_t *PlayerDescriptor() {
|
||||
const uint8_t *player = PlayerObject();
|
||||
if (player == nullptr)
|
||||
|
||||
@@ -44,6 +44,17 @@ const uint8_t *PlayerObject();
|
||||
// re-deriving the player object + descriptor offset in each module.
|
||||
const uint8_t *PlayerDescriptor();
|
||||
|
||||
// The local player's inventory manager (`PlayerObject() +
|
||||
// OFF_PLAYER_INVENTORY_MANAGER`), or null. Layout: slot count at `+0x00`,
|
||||
// flat GUID array at `+OFF_INVMGR_GUID_ARRAY`; it's the `this` that
|
||||
// `GetItemBySlot` expects. This is the SAME pointer the throwing token
|
||||
// resolver (`FUN_RESOLVE_UNIT_TOKEN("player")`) yields — both resolve the
|
||||
// local player's GUID through the object manager (verified: the token
|
||||
// resolver is `TokenToGUID → FUN_OBJECT_RESOLVE_BY_GUID`, differing only in
|
||||
// the type-mask, which both the player object passes) — but non-throwing
|
||||
// (null pre-world). Prefer this over re-deriving `player + 0x1D38` per module.
|
||||
const uint8_t *PlayerInventoryManager();
|
||||
|
||||
// The 64-bit GUID of a resolved CGUnit/CGObject pointer, read through the
|
||||
// pointer at `+0x08` to an instance block whose first 8 bytes are the GUID
|
||||
// (verified in `Script_GetInventoryItemLink`; see `OFF_UNIT_GUID_PTR`).
|
||||
|
||||
Reference in New Issue
Block a user