mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 12:27:35 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd33288c9 | ||
|
|
bf7e476c10 | ||
|
|
8f3ed96d51 | ||
|
|
e1ffaab6b4 | ||
|
|
c13b1c8eb7 | ||
|
|
bedc5d6740 | ||
|
|
bdef428ff7 | ||
|
|
25b1a1a95c | ||
|
|
5e23332880 | ||
|
|
eb36b3e09e | ||
|
|
16452519a6 | ||
|
|
33c5f3041d | ||
|
|
e8a03d584b | ||
|
|
adba73a6d8 |
@@ -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
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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,13 +19,13 @@ 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.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `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` |
|
||||
| [Coroutine](docs/API.md#coroutine) | `coroutine.create`, `coroutine.resume`, `coroutine.yield`, `coroutine.status`, `coroutine.wrap` |
|
||||
| [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` |
|
||||
@@ -41,10 +41,11 @@ 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.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` |
|
||||
| [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.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapOverlays`, `C_Map.GetMapWorldSize` |
|
||||
@@ -54,12 +55,11 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [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` |
|
||||
| [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.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [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` |
|
||||
| [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` |
|
||||
|
||||
@@ -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
|
||||
+489
-174
@@ -22,6 +22,9 @@ build instructions.
|
||||
- [`C_AddOns.DoesAddOnExist(indexOrName)`](#c_addonsdoesaddonexistindexorname)
|
||||
- [`C_AddOns.GetAddOnOptionalDependencies(indexOrName)`](#c_addonsgetaddonoptionaldependenciesindexorname)
|
||||
|
||||
- [AuctionHouse](#auctionhouse)
|
||||
- [`C_AuctionHouse.PostItem(itemLocation, duration, quantity, numStacks, bid, buyout)`](#c_auctionhousepostitemitemlocation-duration-quantity-numstacks-bid-buyout)
|
||||
|
||||
- [CharacterList](#characterlist)
|
||||
- [`GetSavedCharacterOrder(realm)` / `SetSavedCharacterOrder(realm, order)` — GlueXML only](#getsavedcharacterorderrealm--setsavedcharacterorderrealm-order--gluexml-only)
|
||||
|
||||
@@ -40,6 +43,7 @@ build instructions.
|
||||
|
||||
- [Container](#container)
|
||||
- [`C_Container.GetContainerItemID(bagIndex, slotIndex)`](#c_containergetcontaineritemidbagindex-slotindex)
|
||||
- [`C_Container.GetContainerItemInfo(containerIndex, slotIndex)`](#c_containergetcontaineriteminfocontainerindex-slotindex)
|
||||
- [`C_Container.HasContainerItem(bagIndex, slotIndex)`](#c_containerhascontaineritembagindex-slotindex)
|
||||
- [`GetItemCooldown(itemInfo)` / `C_Container.GetItemCooldown(itemID)`](#getitemcooldowniteminfo--c_containergetitemcooldownitemid)
|
||||
- [`C_Container.GetContainerItemDurability(containerIndex, slotIndex)`](#c_containergetcontaineritemdurabilitycontainerindex-slotindex)
|
||||
@@ -53,14 +57,6 @@ build instructions.
|
||||
- [`C_Container.SwapItems(srcBag, srcSlot, dstBag, dstSlot)`](#c_containerswapitemssrcbag-srcslot-dstbag-dstslot)
|
||||
- [`C_Container.MoveItem(srcBag, srcSlot, dstBag, dstSlot, count)`](#c_containermoveitemsrcbag-srcslot-dstbag-dstslot-count)
|
||||
|
||||
- [Coroutine](#coroutine)
|
||||
- [`coroutine.create(fn)`](#coroutinecreatefn)
|
||||
- [`coroutine.resume(co, ...)`](#coroutineresumeco-)
|
||||
- [`coroutine.yield(...)`](#coroutineyield)
|
||||
- [`coroutine.status(co)`](#coroutinestatusco)
|
||||
- [`coroutine.wrap(fn)`](#coroutinewrapfn)
|
||||
- [Async pattern (RunAsync + C_Timer.After)](#async-pattern-runasync--c_timerafter)
|
||||
|
||||
- [Creature](#creature)
|
||||
- [`C_CreatureInfo.GetCreatureID(guid)`](#c_creatureinfogetcreatureidguid)
|
||||
- [`C_CreatureInfo.GetCreatureInfoByID(creatureID)`](#c_creatureinfogetcreatureinfobyidcreatureid)
|
||||
@@ -105,6 +101,7 @@ build instructions.
|
||||
- [`HEARTHSTONE_BOUND` event](#hearthstone_bound-event)
|
||||
- [Player input-state events (`PLAYER_STARTED_MOVING` / `LOOKING` / `TURNING` + `STOPPED_*`)](#player-input-state-events)
|
||||
- [`GLOBAL_MOUSE_DOWN` / `GLOBAL_MOUSE_UP` events](#global_mouse_down--global_mouse_up-events)
|
||||
- [`AUCTION_MULTISELL_START` / `AUCTION_MULTISELL_UPDATE` / `AUCTION_MULTISELL_FAILURE` events](#c_auctionhousepostitemitemlocation-duration-quantity-numstacks-bid-buyout)
|
||||
- [`EQUIPMENT_SETS_CHANGED` event](#equipment_sets_changed-event)
|
||||
- [`EQUIPMENT_SWAP_PENDING` event](#equipment_swap_pending-event)
|
||||
- [`EQUIPMENT_SWAP_FINISHED` event](#equipment_swap_finished-event)
|
||||
@@ -217,6 +214,7 @@ build instructions.
|
||||
- [`C_Item.GetItemFamily(item)`](#c_itemgetitemfamilyitem)
|
||||
- [`C_Item.GetItemGUID(itemLocation)`](#c_itemgetitemguiditemlocation)
|
||||
- [`C_Item.GetItemID(itemLocation)`](#c_itemgetitemiditemlocation)
|
||||
- [`C_Item.GetItemInfo(itemInfo)`](#c_itemgetiteminfoiteminfo)
|
||||
- [`C_Item.GetItemInfoInstant(item)`](#c_itemgetiteminfoinstantitem)
|
||||
- [`C_Item.GetItemInventorySlotInfo(inventorySlot)`](#c_itemgetiteminventoryslotinfoinventoryslot)
|
||||
- [`C_Item.GetItemInventorySlotKey(inventorySlot)`](#c_itemgetiteminventoryslotkeyinventoryslot)
|
||||
@@ -280,6 +278,18 @@ build instructions.
|
||||
- [`C_LossOfControl.GetActiveLossOfControlDataCount()`](#c_lossofcontrolgetactivelossofcontroldatacount)
|
||||
- [`C_LossOfControl.GetActiveLossOfControlData(index)`](#c_lossofcontrolgetactivelossofcontroldataindex)
|
||||
|
||||
- [Lua](#lua)
|
||||
- [`select(index, ...)`](#selectindex-)
|
||||
- [`table.wipe(t)`](#tablewipet)
|
||||
- [`string.match` / `string.gmatch`](#stringmatch--stringgmatch)
|
||||
- [`math.fmod(x, y)`](#mathfmodx-y)
|
||||
- [`coroutine.create(fn)`](#coroutinecreatefn)
|
||||
- [`coroutine.resume(co, ...)`](#coroutineresumeco-)
|
||||
- [`coroutine.yield(...)`](#coroutineyield)
|
||||
- [`coroutine.status(co)`](#coroutinestatusco)
|
||||
- [`coroutine.wrap(fn)`](#coroutinewrapfn)
|
||||
- [Async pattern (RunAsync + C_Timer.After)](#async-pattern-runasync--c_timerafter)
|
||||
|
||||
- [Macros](#macros)
|
||||
- [Numeric spellIDs in `/cast` and `CastSpellByName`](#numeric-spellids-in-cast-and-castspellbyname)
|
||||
- [`CastSpellNoToggle` as a macro cast line](#castspellnotoggle-as-a-macro-cast-line)
|
||||
@@ -339,6 +349,8 @@ build instructions.
|
||||
- [`C_PlayerInfo.GetName / GetClass / GetRace / GetSex / IsConnected(playerLocation)`](#c_playerinfogetname--getclass--getrace--getsex--isconnectedplayerlocation)
|
||||
- [Quest](#quest)
|
||||
- [`C_QuestLog.GetQuestIDForLogIndex(index)`](#c_questlogGetQuestIDForLogIndexindex)
|
||||
- [`C_QuestLog.GetLogIndexForQuestID(questID)`](#c_questloggetlogindexforquestidquestid)
|
||||
- [`C_QuestLog.GetHeaderIndexForQuest(questID)`](#c_questloggetheaderindexforquestquestid)
|
||||
- [`C_QuestLog.RequestLoadQuestByID(questID)`](#c_questlogrequestloadquestbyidquestid)
|
||||
- [`C_QuestLog.IsOnQuest(questID)`](#c_questlogisonquestquestid)
|
||||
- [`C_QuestLog.IsUnitOnQuest(unit, questID)`](#c_questlogisunitonquestunit-questid)
|
||||
@@ -370,6 +382,8 @@ build instructions.
|
||||
- [`C_Spell.IsSpellPassive(spellID)`](#c_spellisspellpassivespellid)
|
||||
- [`IsPlayerSpell(spellID)`](#isplayerspellspellid)
|
||||
- [`IsSpellKnown(spellID, [isPet])`](#isspellknownspellid-ispet)
|
||||
- [`GetSpellBonusDamage(school)`](#getspellbonusdamageschool)
|
||||
- [`GetSpellBonusHealing()`](#getspellbonushealing)
|
||||
- [`IsUsableSpell(spell)` / `IsUsableSpell(slot, bookType)`](#isusablespellspell--isusablespellslot-booktype)
|
||||
- [`C_Spell.IsSpellUsable(spellID)`](#c_spellisspellusablespellid)
|
||||
- [`C_Spell.GetSpellCooldown(spellIdentifier)`](#c_spellgetspellcooldownspellidentifier)
|
||||
@@ -414,9 +428,6 @@ build instructions.
|
||||
- [`GetShapeshiftFormID()`](#getshapeshiftformid)
|
||||
- [`CancelShapeshiftForm()`](#cancelshapeshiftform)
|
||||
|
||||
- [Table](#table)
|
||||
- [`table.wipe(t)`](#tablewipet)
|
||||
|
||||
- [Talent](#talent)
|
||||
- [`GetTalentSpellID(tabIndex, talentIndex, [rank])`](#gettalentspellidtabindex-talentindex-rank)
|
||||
- [`GetTalentIDByIndex(tabIndex, talentIndex)`](#gettalentidbyindextabindex-talentindex)
|
||||
@@ -800,6 +811,66 @@ C_AddOns.GetAddOnOptionalDependencies("MyAddon") -- "Atlas", "pfQuest"
|
||||
C_AddOns.GetAddOnOptionalDependencies("Atlas") -- (nothing — no OptionalDeps)
|
||||
```
|
||||
|
||||
## AuctionHouse
|
||||
|
||||
### `C_AuctionHouse.PostItem(itemLocation, duration, quantity, numStacks, bid, buyout)`
|
||||
|
||||
Posts `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` only ever posts one whole item object, so this
|
||||
handles the splitting and the sequence of posts for you.
|
||||
|
||||
- `itemLocation` — a `{ bagID = B, slotIndex = S }` table identifying the
|
||||
source stack (0 = backpack, 1–4 = equipped bags; `slotIndex` 1-based).
|
||||
- `duration` — `1`/`2`/`3` for 2h/8h/24h (vanilla's three durations); the raw
|
||||
minute values `120`/`480`/`1440` are also accepted.
|
||||
- `quantity` — items per auction (1–255).
|
||||
- `numStacks` — number of auctions to post.
|
||||
- `bid` — minimum bid **per stack**, in copper (≥ 1).
|
||||
- `buyout` — buyout **per stack**, in copper (`0` / omitted = no buyout).
|
||||
|
||||
Returns `true` when the job is accepted and posting has started, or
|
||||
`false, reason` on immediate rejection (AH not open, source empty, not enough
|
||||
items for `numStacks × quantity`, no free general bag slot to split into, a
|
||||
job already in progress, or bad arguments).
|
||||
|
||||
**It's asynchronous.** Vanilla's `CMSG_AUCTION_SELL_ITEM` has no count field
|
||||
(unlike WotLK, where the server splits) — the server posts the *entire* item
|
||||
object a GUID points at. So to post a partial `quantity` the DLL first splits
|
||||
that amount into a bag slot (a server round-trip), then posts the resulting
|
||||
stack (another round-trip), repeating for each stack. Progress is reported
|
||||
through events rather than the return value:
|
||||
|
||||
| Event | Payload | When |
|
||||
|-------|---------|------|
|
||||
| `AUCTION_MULTISELL_START` | — | job accepted, posting begins |
|
||||
| `AUCTION_MULTISELL_UPDATE` | `postedCount, totalCount` | after each stack lands (final one has `postedCount == totalCount`) |
|
||||
| `AUCTION_MULTISELL_FAILURE` | — | aborted (source changed under it, no free slot, AH closed, or a step timed out ~10s) |
|
||||
|
||||
Splitting reuses **one** general-purpose bag slot (each post empties it for
|
||||
the next split); the final stack posts the source object directly with no
|
||||
split. Posting a whole existing stack (`quantity` == the source count,
|
||||
`numStacks` 1) needs no free slot at all. Every individual post goes through
|
||||
the engine's own `StartAuction`, so its validation (damaged-item reject,
|
||||
buyout normalization, duration check) applies to each stack.
|
||||
|
||||
```lua
|
||||
-- Watch progress
|
||||
local f = CreateFrame("Frame")
|
||||
for _, e in ipairs({"AUCTION_MULTISELL_START", "AUCTION_MULTISELL_UPDATE",
|
||||
"AUCTION_MULTISELL_FAILURE"}) do f:RegisterEvent(e) end
|
||||
f:SetScript("OnEvent", function() print(event, arg1, arg2) end)
|
||||
|
||||
-- At an open auction house: post 3 stacks of 5, 2h, 100 bid / 200 buyout each
|
||||
C_AuctionHouse.PostItem({ bagID = 0, slotIndex = 1 }, 1, 5, 3, 100, 200)
|
||||
```
|
||||
|
||||
Limitations (v1): the source is the single stack at `itemLocation` (stacks
|
||||
aren't aggregated across bags — `quantity × numStacks` must fit in that one
|
||||
stack); temp splits use general-purpose bags only, so family-restricted items
|
||||
(arrows/shards that live in specialty bags) aren't supported; one job runs at
|
||||
a time.
|
||||
|
||||
## CharacterList
|
||||
|
||||
### `GetSavedCharacterOrder(realm)` / `SetSavedCharacterOrder(realm, order)` — GlueXML only
|
||||
@@ -1063,6 +1134,47 @@ for slot = 1, 16 do
|
||||
end
|
||||
```
|
||||
|
||||
### `C_Container.GetContainerItemInfo(containerIndex, slotIndex)`
|
||||
|
||||
Returns a `ContainerItemInfo` table for the item in the given bag slot, or
|
||||
`nil` if the slot is empty / the indices are out of range. The modern
|
||||
structured-table form (namespaced + table-returning since Patch 10.0.2) of
|
||||
vanilla's flat global `GetContainerItemInfo`, which only returned
|
||||
`texture, itemCount, locked, quality, readable`.
|
||||
|
||||
- `containerIndex = 0` — main backpack; `1..4` — equipped bag slots.
|
||||
- `slotIndex` — 1-based.
|
||||
|
||||
Table fields:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `iconFileID` | string | Icon **path** (1.12 has no fileID system, same as `GetItemIcon`). Absent if the item's static data isn't cached yet. |
|
||||
| `stackCount` | number | Current stack size. |
|
||||
| `isLocked` | boolean | Item is in a pending transaction (pickup/trade/mail in flight). |
|
||||
| `quality` | number\|nil | `Enum.ItemQuality` (0=Poor … 5=Legendary); `nil` until the item is cached. |
|
||||
| `isReadable` | boolean | Has readable page text (books/letters). |
|
||||
| `hasLoot` | boolean | Static LOOTABLE flag (best-effort — vanilla can't track post-loot emptiness client-side). |
|
||||
| `hyperlink` | string | Fully-decorated per-instance item link (enchant + random suffix), same as `GetContainerItemLink`. |
|
||||
| `isFiltered` | boolean | Always `false` — vanilla has no bag search-filter system. |
|
||||
| `hasNoValue` | boolean | `true` when the item's vendor sell price is 0. |
|
||||
| `itemID` | number | Item ID. |
|
||||
| `isBound` | boolean | Soulbound. |
|
||||
| `itemName` | string | Per-instance decorated display name (falls back to the base name). |
|
||||
|
||||
Live per-instance fields (`stackCount`, `isLocked`, `isBound`, `itemID`,
|
||||
`hyperlink`) are always present; cache-derived fields (`iconFileID`,
|
||||
`quality`, `hasLoot`, `hasNoValue`, `itemName`) are omitted/`nil` until the
|
||||
item's static data arrives (listen for `GET_ITEM_INFO_RECEIVED`). Passive —
|
||||
does not warm the item cache.
|
||||
|
||||
```lua
|
||||
local info = C_Container.GetContainerItemInfo(0, 1)
|
||||
if info then
|
||||
print(info.itemName, info.stackCount, info.quality, info.hyperlink)
|
||||
end
|
||||
```
|
||||
|
||||
### `C_Container.HasContainerItem(bagIndex, slotIndex)`
|
||||
|
||||
Returns `true` if the given bag slot currently holds an item, `false`
|
||||
@@ -1494,146 +1606,6 @@ constraint as `SwapItems`.
|
||||
|
||||
Send is fire-and-forget (same as `SwapItems`).
|
||||
|
||||
## Coroutine
|
||||
|
||||
Restores Lua 5.0's stripped `coroutine.*` library. The C-level
|
||||
coroutine machinery (`lua_resume`, `lua_yield`, `lua_newthread`,
|
||||
and the `thread` type at tag 8) is linked into the engine, but
|
||||
the script-facing library was never registered as a global table.
|
||||
ClassicAPI rewires the standard five entries on `coroutine`,
|
||||
matching Lua 5.0 semantics with two WoW-specific quirks called
|
||||
out below.
|
||||
|
||||
### `coroutine.create(fn)`
|
||||
|
||||
Creates a new coroutine with `fn` as its body and returns the
|
||||
resulting `thread`. `fn` must be a **Lua** function — passing a
|
||||
C function (any engine global like `GetTime` or `UnitName`)
|
||||
raises `bad argument #1 to 'create' (Lua function expected)`.
|
||||
|
||||
The new coroutine starts suspended; nothing runs until
|
||||
`coroutine.resume` is called on it. The args passed to the first
|
||||
resume become the body's arguments.
|
||||
|
||||
### `coroutine.resume(co, ...)`
|
||||
|
||||
Resumes a suspended coroutine, passing the trailing args to either
|
||||
the body (first resume) or back as the return values of the
|
||||
`coroutine.yield` that paused the coroutine.
|
||||
|
||||
Returns:
|
||||
|
||||
- `true, ...values` on success, where `values` are whatever the
|
||||
coroutine yielded (mid-execution) or returned (on completion).
|
||||
- `false, errMsg` on error. Two error sources:
|
||||
- Coroutine isn't suspended — e.g. resuming a dead one;
|
||||
`errMsg` is a fixed string like `"cannot resume dead coroutine"`.
|
||||
- The body hit a Lua VM error; `errMsg` is the formatted error
|
||||
string with file:line prefix.
|
||||
|
||||
**Caveat — `error()` doesn't propagate.** WoW replaces Lua's
|
||||
`error()` global with a soft handler that calls
|
||||
`geterrorhandler()(msg)` and returns normally instead of
|
||||
longjmp'ing. So `error("msg")` inside a coroutine body looks like
|
||||
a clean return to `resume` — the message gets printed to chat as
|
||||
a side effect and `resume` returns `true` with no values. Real VM
|
||||
errors (calling `nil`, indexing `nil`, etc.) still propagate
|
||||
through the standard `false, msg` path.
|
||||
|
||||
### `coroutine.yield(...)`
|
||||
|
||||
Suspends the current coroutine. Whatever's passed to `yield`
|
||||
becomes the return values of the matching `coroutine.resume`
|
||||
call. When the coroutine is next resumed, the args passed to
|
||||
`resume` become the return values of `yield`.
|
||||
|
||||
Raises an error if called outside a coroutine
|
||||
(`attempt to yield across metamethod/C-call boundary`) or from
|
||||
a C function (`cannot yield a C function`).
|
||||
|
||||
### `coroutine.status(co)`
|
||||
|
||||
Returns the coroutine's state as a string:
|
||||
|
||||
| Value | Meaning |
|
||||
|-------|---------|
|
||||
| `"running"` | `co` is the currently-running coroutine (i.e. the same thread that called `status`). |
|
||||
| `"suspended"` | `co` is paused — either initial (never resumed) or mid-yield. |
|
||||
| `"dead"` | `co`'s body has returned or errored. |
|
||||
|
||||
The standard Lua `"normal"` state — a coroutine that resumed a
|
||||
deeper coroutine and is now an ancestor on the call chain — is
|
||||
collapsed into `"suspended"`. Detecting it cleanly requires
|
||||
walking the global thread list, and the case is near-useless for
|
||||
addon-level logic.
|
||||
|
||||
### `coroutine.wrap(fn)`
|
||||
|
||||
Equivalent to `coroutine.create(fn)` followed by returning a
|
||||
closure that calls `coroutine.resume(co, ...)` on each invocation
|
||||
— but propagates errors as raised exceptions (via `lua_error`)
|
||||
instead of returning `false, msg`. Lets you use a coroutine as
|
||||
if it were an ordinary function:
|
||||
|
||||
```lua
|
||||
local gen = coroutine.wrap(function()
|
||||
for i = 1, 5 do coroutine.yield(i) end
|
||||
end)
|
||||
print(gen()) -- 1
|
||||
print(gen()) -- 2
|
||||
print(gen()) -- 3
|
||||
```
|
||||
|
||||
Calling the wrapped function after the body has completed raises
|
||||
`cannot resume dead coroutine`. The canonical "iterator that
|
||||
terminates cleanly through `for ... in`" idiom is to yield an
|
||||
explicit `nil` at the end so the for-loop's nil sentinel fires:
|
||||
|
||||
```lua
|
||||
local function range(from, to)
|
||||
return coroutine.wrap(function()
|
||||
for i = from, to do coroutine.yield(i) end
|
||||
coroutine.yield(nil)
|
||||
end)
|
||||
end
|
||||
|
||||
for n in range(1, 5) do print(n) end
|
||||
```
|
||||
|
||||
### Async pattern (`RunAsync` + `C_Timer.After`)
|
||||
|
||||
Coroutines pair naturally with `C_Timer.After` for chunked
|
||||
"do some work, wait a frame, do more" loops. The canonical
|
||||
scheduler:
|
||||
|
||||
```lua
|
||||
local function RunAsync(fn)
|
||||
local co = coroutine.create(fn)
|
||||
local function step()
|
||||
local ok, delay = coroutine.resume(co)
|
||||
if not ok or coroutine.status(co) == "dead" then return end
|
||||
C_Timer.After(delay or 0, step)
|
||||
end
|
||||
step()
|
||||
end
|
||||
|
||||
RunAsync(function()
|
||||
for i = 1, 100 do
|
||||
DoExpensiveWork(i)
|
||||
coroutine.yield(0) -- next frame
|
||||
end
|
||||
coroutine.yield(0.5) -- pause half a second
|
||||
DoFinalWork()
|
||||
end)
|
||||
```
|
||||
|
||||
`coroutine.yield(n)` returns `n` to the scheduler, which schedules
|
||||
the next `step` after that many seconds. `yield(0)` is the
|
||||
"resume on the next frame" form (since `C_Timer.After(0, ...)`
|
||||
fires on the next OnUpdate tick). `RunNextFrame(cb)` in
|
||||
[`AddOns/!!!ClassicAPI/Util/FunctionUtil.lua`](../AddOns/!!!ClassicAPI/Util/FunctionUtil.lua)
|
||||
wraps the same primitive for the non-coroutine case.
|
||||
|
||||
## Creature
|
||||
|
||||
### `C_CreatureInfo.GetCreatureID(guid)`
|
||||
@@ -5015,6 +4987,50 @@ if id then
|
||||
end
|
||||
```
|
||||
|
||||
### `C_Item.GetItemInfo(itemInfo)`
|
||||
|
||||
The full modern `GetItemInfo` tuple, sourced from the client-side item cache
|
||||
plus the class/subclass/inventory-type DBC name lookups. The namespaced
|
||||
counterpart to the stock global `GetItemInfo` (which returns only vanilla's
|
||||
short tuple); this returns the wide modern set backported addons expect.
|
||||
|
||||
Accepts a numeric `itemID`, an `"item:NNN"` string, or a full chat link.
|
||||
|
||||
Returns 18 values:
|
||||
|
||||
| # | Return | Notes |
|
||||
|---|--------|-------|
|
||||
| 1 | `itemName` | |
|
||||
| 2 | `itemLink` | basic `item:ID` hyperlink (colored, bracketed name) |
|
||||
| 3 | `itemQuality` | 0–5 |
|
||||
| 4 | `itemLevel` | |
|
||||
| 5 | `itemMinLevel` | required level |
|
||||
| 6 | `itemType` | localized class name (e.g. "Weapon") |
|
||||
| 7 | `itemSubType` | localized subclass name (e.g. "Sword") |
|
||||
| 8 | `itemStackCount` | max stack size |
|
||||
| 9 | `itemEquipLoc` | `INVTYPE_*` token, `""` for non-equippable |
|
||||
| 10 | `itemTexture` | icon **path** (1.12 has no fileID system) |
|
||||
| 11 | `sellPrice` | vendor sell price in copper |
|
||||
| 12 | `classID` | |
|
||||
| 13 | `subclassID` | |
|
||||
| 14 | `bindType` | 0 none / 1 BoP / 2 BoE / 3 BoU / 4 quest |
|
||||
| 15 | `expansionID` | always `0` (classic) |
|
||||
| 16 | `setID` | item-set ID, `nil` if none |
|
||||
| 17 | `isCraftingReagent` | always `false` (no such flag in 1.12 data) |
|
||||
| 18 | `itemDescription` | item flavor/description text (`""` if none) |
|
||||
|
||||
**Asynchronous on a cache miss:** if the item isn't cached yet it returns
|
||||
nothing (nil) and warms the cache; the value lands on a retry after
|
||||
`GET_ITEM_INFO_RECEIVED` fires — same contract as the stock `GetItemInfo`
|
||||
and modern clients. Fields 4–18 are the ones the vanilla global never
|
||||
returned.
|
||||
|
||||
```lua
|
||||
local name, link, quality, ilvl, minLevel, itype, isub, stack, equipLoc,
|
||||
tex, sell, classID, subID, bind, expac, setID, reagent, desc
|
||||
= C_Item.GetItemInfo(6948) -- Hearthstone
|
||||
```
|
||||
|
||||
### `C_Item.GetItemInfoInstant(item)`
|
||||
|
||||
Modern-style accessor for the always-available subset of item info — the
|
||||
@@ -6734,6 +6750,249 @@ Fidelity vs. retail:
|
||||
The `LOSS_OF_CONTROL_ADDED` / `LOSS_OF_CONTROL_UPDATE` events fire as these
|
||||
effects change — see [Events](#loss_of_control_added--loss_of_control_update-events).
|
||||
|
||||
## Lua
|
||||
|
||||
Standard-library functions that later Lua versions (or Blizzard's FrameXML)
|
||||
added and that 1.12's Lua 5.0 is missing — restored by ClassicAPI so
|
||||
backported addons find them. Most are single-function additions (several are
|
||||
just the 5.0→5.1 renames — `string.gmatch`←`gfind`, `math.fmod`←`math.mod`);
|
||||
`coroutine.*` restores the whole stripped coroutine library.
|
||||
|
||||
### `select(index, ...)`
|
||||
|
||||
The Lua 5.1 vararg helper, backported to 1.12's Lua 5.0. Two forms:
|
||||
|
||||
- `select('#', ...)` → the number of extra arguments passed after the
|
||||
index.
|
||||
- `select(n, ...)` → all arguments from position `n` onward. `n` may be
|
||||
negative to count back from the end (`-1` is the last argument).
|
||||
|
||||
Errors on a non-numeric, non-`'#'` first argument or an index of 0 /
|
||||
below the negative range (matching Lua 5.1's `luaL_checkint` +
|
||||
`index out of range` behavior). Registered on **both** Lua states, so it's
|
||||
available to in-game addons and glue-screen (login/char-select) code alike.
|
||||
|
||||
```lua
|
||||
select('#', 'a', 'b', 'c') -- 3
|
||||
select(2, 'a', 'b', 'c') -- 'b', 'c'
|
||||
select(-1, 'a', 'b', 'c') -- 'c'
|
||||
|
||||
-- Idiomatic vararg count inside a function:
|
||||
local function log(...)
|
||||
for i = 1, select('#', ...) do
|
||||
print(i, (select(i, ...)))
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Ported from Lua 5.1's `luaB_select` (lbaselib.c). The numeric form pushes
|
||||
nothing — the varargs are already on the stack, so it just returns the
|
||||
count of the slice from `n+1` onward.
|
||||
|
||||
### `table.wipe(t)`
|
||||
|
||||
Removes every key from `t`, leaving it empty but preserving its
|
||||
internal hash and array capacity. Returns `t` for chaining.
|
||||
|
||||
```lua
|
||||
local cache = {}
|
||||
-- ... fill it up ...
|
||||
table.wipe(cache) -- cache is now {}
|
||||
local also_t = table.wipe(t2) -- also_t === t2 after wipe
|
||||
```
|
||||
|
||||
Port of Blizzard's 3.3.5 implementation at VA `0x00852180` — same
|
||||
pattern, just using 1.12's Lua 5.0 entry points instead of 5.1's.
|
||||
Both Lua versions' `lua_next` walks the hash node array linearly,
|
||||
so the canonical `lua_next` + `rawset(k, nil)` "during-iteration
|
||||
removal" pattern works in practice even though it's technically
|
||||
undefined per the Lua reference manual.
|
||||
|
||||
Errors on non-table input.
|
||||
|
||||
### `string.match` / `string.gmatch`
|
||||
|
||||
The Lua 5.1 pattern helpers, backported to 1.12's Lua 5.0 (which ships only
|
||||
`find` / `gfind` / `gsub`).
|
||||
|
||||
- **`string.match(s, pattern [, init])`** — returns the captures of the
|
||||
first match, the whole match when the pattern has no captures, or `nil`
|
||||
when it doesn't match. Same pattern engine as `string.find` — it's `find`
|
||||
returning the captures instead of the `start, end` indices, so it reuses
|
||||
the engine's own matcher rather than reimplementing it.
|
||||
- **`string.gmatch(s, pattern)`** — the match iterator, for
|
||||
`for x in string.gmatch(s, pat) do ... end`. This is exactly Lua 5.0's
|
||||
`string.gfind` (renamed in 5.1); it's registered as a direct alias.
|
||||
|
||||
```lua
|
||||
local y, m, d = string.match("2024-01-15", "(%d+)-(%d+)-(%d+)") -- "2024","01","15"
|
||||
string.match("no digits", "%d+") -- nil
|
||||
|
||||
for word in string.gmatch("a,bb,ccc", "[^,]+") do print(word) end -- a / bb / ccc
|
||||
```
|
||||
|
||||
> **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 and userdata (both `lua_setmetatable` and the VM's own
|
||||
> `luaT_gettmbyobj` hard-return "no metamethod" for strings), so string values
|
||||
> 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 1.12 constraint —
|
||||
> always write `string.match(s, p)`, never `s:match(p)`.
|
||||
|
||||
### `math.fmod(x, y)`
|
||||
|
||||
The floating-point remainder of `x / y` (the quotient truncated toward zero),
|
||||
same as C's `fmod`. A pure 5.0→5.1 rename: Lua 5.0 ships the identical C
|
||||
function as `math.mod`, and 5.1 renamed the Lua-facing name to `math.fmod`.
|
||||
Registered as a direct alias — `math.mod` remains available too, so code
|
||||
written for either name works.
|
||||
|
||||
```lua
|
||||
math.fmod(7, 3) -- 1
|
||||
math.fmod(-7, 3) -- -1 (result takes the sign of the dividend)
|
||||
math.fmod(5.5, 2) -- 1.5
|
||||
```
|
||||
|
||||
### `coroutine.*`
|
||||
|
||||
Restores Lua 5.0's stripped `coroutine.*` library. The C-level
|
||||
coroutine machinery (`lua_resume`, `lua_yield`, `lua_newthread`,
|
||||
and the `thread` type at tag 8) is linked into the engine, but
|
||||
the script-facing library was never registered as a global table.
|
||||
ClassicAPI rewires the standard five entries on `coroutine`,
|
||||
matching Lua 5.0 semantics with two WoW-specific quirks called
|
||||
out below.
|
||||
|
||||
#### `coroutine.create(fn)`
|
||||
|
||||
Creates a new coroutine with `fn` as its body and returns the
|
||||
resulting `thread`. `fn` must be a **Lua** function — passing a
|
||||
C function (any engine global like `GetTime` or `UnitName`)
|
||||
raises `bad argument #1 to 'create' (Lua function expected)`.
|
||||
|
||||
The new coroutine starts suspended; nothing runs until
|
||||
`coroutine.resume` is called on it. The args passed to the first
|
||||
resume become the body's arguments.
|
||||
|
||||
#### `coroutine.resume(co, ...)`
|
||||
|
||||
Resumes a suspended coroutine, passing the trailing args to either
|
||||
the body (first resume) or back as the return values of the
|
||||
`coroutine.yield` that paused the coroutine.
|
||||
|
||||
Returns:
|
||||
|
||||
- `true, ...values` on success, where `values` are whatever the
|
||||
coroutine yielded (mid-execution) or returned (on completion).
|
||||
- `false, errMsg` on error. Two error sources:
|
||||
- Coroutine isn't suspended — e.g. resuming a dead one;
|
||||
`errMsg` is a fixed string like `"cannot resume dead coroutine"`.
|
||||
- The body hit a Lua VM error; `errMsg` is the formatted error
|
||||
string with file:line prefix.
|
||||
|
||||
**Caveat — `error()` doesn't propagate.** WoW replaces Lua's
|
||||
`error()` global with a soft handler that calls
|
||||
`geterrorhandler()(msg)` and returns normally instead of
|
||||
longjmp'ing. So `error("msg")` inside a coroutine body looks like
|
||||
a clean return to `resume` — the message gets printed to chat as
|
||||
a side effect and `resume` returns `true` with no values. Real VM
|
||||
errors (calling `nil`, indexing `nil`, etc.) still propagate
|
||||
through the standard `false, msg` path.
|
||||
|
||||
#### `coroutine.yield(...)`
|
||||
|
||||
Suspends the current coroutine. Whatever's passed to `yield`
|
||||
becomes the return values of the matching `coroutine.resume`
|
||||
call. When the coroutine is next resumed, the args passed to
|
||||
`resume` become the return values of `yield`.
|
||||
|
||||
Raises an error if called outside a coroutine
|
||||
(`attempt to yield across metamethod/C-call boundary`) or from
|
||||
a C function (`cannot yield a C function`).
|
||||
|
||||
#### `coroutine.status(co)`
|
||||
|
||||
Returns the coroutine's state as a string:
|
||||
|
||||
| Value | Meaning |
|
||||
|-------|---------|
|
||||
| `"running"` | `co` is the currently-running coroutine (i.e. the same thread that called `status`). |
|
||||
| `"suspended"` | `co` is paused — either initial (never resumed) or mid-yield. |
|
||||
| `"dead"` | `co`'s body has returned or errored. |
|
||||
|
||||
The standard Lua `"normal"` state — a coroutine that resumed a
|
||||
deeper coroutine and is now an ancestor on the call chain — is
|
||||
collapsed into `"suspended"`. Detecting it cleanly requires
|
||||
walking the global thread list, and the case is near-useless for
|
||||
addon-level logic.
|
||||
|
||||
#### `coroutine.wrap(fn)`
|
||||
|
||||
Equivalent to `coroutine.create(fn)` followed by returning a
|
||||
closure that calls `coroutine.resume(co, ...)` on each invocation
|
||||
— but propagates errors as raised exceptions (via `lua_error`)
|
||||
instead of returning `false, msg`. Lets you use a coroutine as
|
||||
if it were an ordinary function:
|
||||
|
||||
```lua
|
||||
local gen = coroutine.wrap(function()
|
||||
for i = 1, 5 do coroutine.yield(i) end
|
||||
end)
|
||||
print(gen()) -- 1
|
||||
print(gen()) -- 2
|
||||
print(gen()) -- 3
|
||||
```
|
||||
|
||||
Calling the wrapped function after the body has completed raises
|
||||
`cannot resume dead coroutine`. The canonical "iterator that
|
||||
terminates cleanly through `for ... in`" idiom is to yield an
|
||||
explicit `nil` at the end so the for-loop's nil sentinel fires:
|
||||
|
||||
```lua
|
||||
local function range(from, to)
|
||||
return coroutine.wrap(function()
|
||||
for i = from, to do coroutine.yield(i) end
|
||||
coroutine.yield(nil)
|
||||
end)
|
||||
end
|
||||
|
||||
for n in range(1, 5) do print(n) end
|
||||
```
|
||||
|
||||
#### Async pattern (`RunAsync` + `C_Timer.After`)
|
||||
|
||||
Coroutines pair naturally with `C_Timer.After` for chunked
|
||||
"do some work, wait a frame, do more" loops. The canonical
|
||||
scheduler:
|
||||
|
||||
```lua
|
||||
local function RunAsync(fn)
|
||||
local co = coroutine.create(fn)
|
||||
local function step()
|
||||
local ok, delay = coroutine.resume(co)
|
||||
if not ok or coroutine.status(co) == "dead" then return end
|
||||
C_Timer.After(delay or 0, step)
|
||||
end
|
||||
step()
|
||||
end
|
||||
|
||||
RunAsync(function()
|
||||
for i = 1, 100 do
|
||||
DoExpensiveWork(i)
|
||||
coroutine.yield(0) -- next frame
|
||||
end
|
||||
coroutine.yield(0.5) -- pause half a second
|
||||
DoFinalWork()
|
||||
end)
|
||||
```
|
||||
|
||||
`coroutine.yield(n)` returns `n` to the scheduler, which schedules
|
||||
the next `step` after that many seconds. `yield(0)` is the
|
||||
"resume on the next frame" form (since `C_Timer.After(0, ...)`
|
||||
fires on the next OnUpdate tick). `RunNextFrame(cb)` in
|
||||
[`AddOns/!!!ClassicAPI/Util/FunctionUtil.lua`](../AddOns/!!!ClassicAPI/Util/FunctionUtil.lua)
|
||||
wraps the same primitive for the non-coroutine case.
|
||||
|
||||
## Macros
|
||||
|
||||
Engine-level extensions to how macros are parsed and dispatched. These
|
||||
@@ -8156,6 +8415,37 @@ for i = 1, GetNumQuestLogEntries() do
|
||||
end
|
||||
```
|
||||
|
||||
### `C_QuestLog.GetLogIndexForQuestID(questID)`
|
||||
|
||||
The inverse of `GetQuestIDForLogIndex`: returns the 1-based quest log index
|
||||
of `questID`, or `nil` if the quest isn't in the log. The index spans the
|
||||
full entry list (headers included), so it can be passed straight to
|
||||
`GetQuestLogTitle(index)` / `GetQuestLogLeaderBoardID(...)` and the other
|
||||
index-based quest-log accessors.
|
||||
|
||||
```lua
|
||||
local index = C_QuestLog.GetLogIndexForQuestID(questID)
|
||||
if index then
|
||||
local title = GetQuestLogTitle(index)
|
||||
-- ...
|
||||
end
|
||||
```
|
||||
|
||||
### `C_QuestLog.GetHeaderIndexForQuest(questID)`
|
||||
|
||||
Returns the 1-based log index of the collapsible **header** (zone /
|
||||
`"Dungeon"` / class-sort category, …) that `questID` sits under, or `nil` if
|
||||
the quest isn't in the log. The quest log is laid out header-then-its-quests,
|
||||
so this locates the quest and walks back to the nearest preceding header row.
|
||||
Pass the result to `GetQuestLogTitle(headerIndex)` for the category name.
|
||||
|
||||
```lua
|
||||
local headerIndex = C_QuestLog.GetHeaderIndexForQuest(questID)
|
||||
if headerIndex then
|
||||
local zone = GetQuestLogTitle(headerIndex) -- e.g. "Westfall"
|
||||
end
|
||||
```
|
||||
|
||||
### `C_QuestLog.RequestLoadQuestByID(questID)`
|
||||
|
||||
Asks the engine to fetch the static data for `questID` (title, description,
|
||||
@@ -8997,6 +9287,54 @@ function does the same spellbook walk in its inner helper at
|
||||
`0x0053B4E0` (player array `[0x00BE6D88]`, pet array `[0x00BE7D98]`,
|
||||
same shape just different addresses).
|
||||
|
||||
### `GetSpellBonusDamage(school)`
|
||||
|
||||
Returns the local player's flat spell-damage bonus (spell power) for a
|
||||
given magic school, as a number. `school` is **1-based**: `1` Physical,
|
||||
`2` Holy, `3` Fire, `4` Nature, `5` Frost, `6` Shadow, `7` Arcane. Raises
|
||||
a usage error for a missing / out-of-range school; returns `0` before
|
||||
you're in the world.
|
||||
|
||||
```lua
|
||||
GetSpellBonusDamage(3) -- your +Fire spell damage
|
||||
GetSpellBonusDamage(6) -- your +Shadow spell damage
|
||||
```
|
||||
|
||||
### `GetSpellBonusHealing()`
|
||||
|
||||
Returns the local player's flat healing bonus, as a number.
|
||||
|
||||
```lua
|
||||
GetSpellBonusHealing() -- your +Healing
|
||||
```
|
||||
|
||||
> **Damage is an exact field read; healing is derived.**
|
||||
>
|
||||
> `GetSpellBonusDamage` reads the client's fully-computed value directly
|
||||
> from the CGPlayer sub-struct (`PLAYER_FIELD_MOD_DAMAGE_DONE_POS − _NEG`
|
||||
> per school). That field isn't in the *broadcast* descriptor — the
|
||||
> vanilla server never sends it there, which is why it looks absent — but
|
||||
> the client keeps a computed copy for its own use, with gear, enchants,
|
||||
> buffs, talents, and set bonuses all baked in. So the value is exact and
|
||||
> complete. (Same source nampower's `GetSpellPower` reads.)
|
||||
>
|
||||
> `GetSpellBonusHealing` has no such field — vanilla 1.12 never had a
|
||||
> healing-done field at all (confirmed in-game: toggling a pure +healing
|
||||
> item moved no field in the player struct) — so it's **derived** in two
|
||||
> parts, both from `Spell.dbc`: (1) flat healing = the sum of every
|
||||
> `SPELL_AURA_MOD_HEALING_DONE` off gear equip-effects, item enchants
|
||||
> (permanent + weapon oils), random suffixes, and active buffs; plus (2)
|
||||
> talent/buff stat-conversions, mirroring the server's own
|
||||
> `SpellBaseHealingBonusDone` — the sum of `SPELL_AURA_MOD_SPELL_HEALING_OF`
|
||||
> `_STAT_PERCENT` (aura 175) × total Spirit / 100 over your passive talents
|
||||
> (Priest *Spiritual Guidance*) and active buffs. Part 2 reads the talent
|
||||
> percent directly, so it's **generic** (any aura-175 talent/buff, including
|
||||
> Turtle customs) and exact — e.g. Spiritual Guidance 5/5 with 182 Spirit
|
||||
> adds `int(182 × 25 / 100) = 45`.
|
||||
>
|
||||
> Residual: assumes talent ranks supersede (only the learned rank is known)
|
||||
> and misses set-bonus healing granted via a set-completion spell.
|
||||
|
||||
### `IsUsableSpell(spell)` / `IsUsableSpell(slot, bookType)`
|
||||
|
||||
Returns `(usable, noMana)` for a spell, matching the modern
|
||||
@@ -10229,29 +10567,6 @@ Mirrors 3.3.5a's `Script_CancelShapeshiftForm` inner (`FUN_00726CE0`),
|
||||
which does the same effect-array scan + form-id match before issuing
|
||||
its cancel packet. Vanilla just lacks the public Lua surface.
|
||||
|
||||
## Table
|
||||
|
||||
### `table.wipe(t)`
|
||||
|
||||
Removes every key from `t`, leaving it empty but preserving its
|
||||
internal hash and array capacity. Returns `t` for chaining.
|
||||
|
||||
```lua
|
||||
local cache = {}
|
||||
-- ... fill it up ...
|
||||
table.wipe(cache) -- cache is now {}
|
||||
local also_t = table.wipe(t2) -- also_t === t2 after wipe
|
||||
```
|
||||
|
||||
Port of Blizzard's 3.3.5 implementation at VA `0x00852180` — same
|
||||
pattern, just using 1.12's Lua 5.0 entry points instead of 5.1's.
|
||||
Both Lua versions' `lua_next` walks the hash node array linearly,
|
||||
so the canonical `lua_next` + `rawset(k, nil)` "during-iteration
|
||||
removal" pattern works in practice even though it's technically
|
||||
undefined per the Lua reference manual.
|
||||
|
||||
Errors on non-table input.
|
||||
|
||||
## Talent
|
||||
|
||||
### `GetTalentSpellID(tabIndex, talentIndex, [rank])`
|
||||
|
||||
+104
@@ -798,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
|
||||
@@ -868,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
|
||||
@@ -1804,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
|
||||
@@ -1812,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.
|
||||
@@ -3130,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
|
||||
@@ -3734,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
|
||||
@@ -4896,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.
|
||||
@@ -5039,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{
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
+105
-17
@@ -41,7 +41,16 @@
|
||||
// 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).
|
||||
// Bank and equipment are out of scope, matching retail.
|
||||
//
|
||||
// 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
|
||||
@@ -67,17 +76,19 @@ namespace {
|
||||
constexpr const char *kEvtNewItems = "BAG_NEW_ITEMS_UPDATED";
|
||||
const Event::Custom::AutoReserve _r{kEvtNewItems};
|
||||
|
||||
// Bags cap at backpack (16) + four equipped bags; even the largest Turtle
|
||||
// bags stay well under this. Overflow silently drops — a flagged-new item
|
||||
// past the cap just won't glow, never a crash.
|
||||
constexpr int kMaxItems = 256;
|
||||
// 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 bag-resident GUIDs at the last scan
|
||||
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;
|
||||
@@ -167,6 +178,81 @@ int ScanBags(const uint8_t *player, uint64_t *out) {
|
||||
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), "");
|
||||
}
|
||||
@@ -204,9 +290,11 @@ void OnTick() {
|
||||
if (player == nullptr)
|
||||
return; // not resolvable yet — retry next tick
|
||||
|
||||
// Everything currently in the bags becomes the baseline; nothing owned at
|
||||
// login is "new". Any change up to this point is intentionally absorbed.
|
||||
// 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;
|
||||
}
|
||||
@@ -224,25 +312,25 @@ void OnBagChanged() {
|
||||
if (player == nullptr)
|
||||
return;
|
||||
|
||||
uint64_t current[kMaxItems];
|
||||
const int currentCount = ScanBags(player, current);
|
||||
uint64_t currentBags[kMaxItems];
|
||||
const int bagCount = ScanBags(player, currentBags);
|
||||
|
||||
bool changed = false;
|
||||
// Drop flags for items no longer in the bags (used, sold, banked).
|
||||
// Drop flags for items no longer in the bags (used, sold, equipped, banked).
|
||||
for (int i = 0; i < g_newCount;) {
|
||||
if (!Contains(current, currentCount, g_new[i])) {
|
||||
if (!Contains(currentBags, bagCount, g_new[i])) {
|
||||
g_new[i] = g_new[--g_newCount];
|
||||
changed = true;
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
// Flag anything not present at the previous scan.
|
||||
for (int i = 0; i < currentCount; ++i) {
|
||||
if (!Contains(g_seen, g_seenCount, current[i]))
|
||||
changed |= AddUnique(g_new, &g_newCount, current[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(current, currentCount);
|
||||
CopyToSeen(currentBags, bagCount);
|
||||
AppendNonBagOwned(g_seen, &g_seenCount);
|
||||
|
||||
if (changed)
|
||||
FireChanged();
|
||||
|
||||
+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);
|
||||
|
||||
|
||||
@@ -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,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
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user