mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 04:17:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc0bf769c4 | ||
|
|
ad13440743 | ||
|
|
999a5846c4 | ||
|
|
9b5d12c8a1 | ||
|
|
d1bab7a3a4 | ||
|
|
e657bac266 | ||
|
|
0248661bb7 | ||
|
|
56fef8ec08 | ||
|
|
09ac68e674 | ||
|
|
19ad6e6514 | ||
|
|
41bb687608 | ||
|
|
d9cc3f9d7e | ||
|
|
6b19c8bbb5 | ||
|
|
09e57c3d97 | ||
|
|
2f3c67be1a | ||
|
|
f94af6cb73 | ||
|
|
1d370ada65 | ||
|
|
35c78ea6be | ||
|
|
ad83eb9575 | ||
|
|
8995e82627 |
@@ -4,7 +4,8 @@
|
||||
## Version: DEV
|
||||
## Author: Brues
|
||||
|
||||
GlobalStrings.lua
|
||||
locales\locales.xml
|
||||
|
||||
Compat.lua
|
||||
Util\Print.lua
|
||||
Util\Constants.lua
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
|
||||
ITEMS_EQUIPPED = "%d |4item:items; equipped";
|
||||
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
|
||||
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
|
||||
ITEM_MISSING = "%s missing";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
if GetLocale() ~= "deDE" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Fokus setzen";
|
||||
BINDING_NAME_TARGETFOCUS = "Fokus anvisieren";
|
||||
FOCUSTARGET = "Ziel als Fokus festlegen";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4IGegenstand:Gegenstände;";
|
||||
ITEMS_EQUIPPED = "%d |4Gegenstand:Gegenstände; ausgerüstet";
|
||||
ITEMS_IN_INVENTORY = "%d |4Gegenstand:Gegenstände; im Inventar";
|
||||
ITEM_SLOTS_IGNORED = "%d |4Platz:Plätze; ignoriert";
|
||||
ITEM_MISSING = "%s fehlt";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4Gegenstand fehlt:Gegenstände fehlen;";
|
||||
TARGETFOCUS = "Fokussierte Einheit anvisieren";
|
||||
@@ -0,0 +1,12 @@
|
||||
if GetLocale() ~= "enUS" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
|
||||
ITEMS_EQUIPPED = "%d |4item:items; equipped";
|
||||
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
|
||||
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
|
||||
ITEM_MISSING = "%s missing";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
@@ -0,0 +1,12 @@
|
||||
if GetLocale() ~= "esES" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Establecer objetivo";
|
||||
BINDING_NAME_TARGETFOCUS = "Seleccionar enfocado";
|
||||
FOCUSTARGET = "Establecer objetivo";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4objeto:objetos;";
|
||||
ITEMS_EQUIPPED = "Equipado: %d |4objeto:objetos;";
|
||||
ITEMS_IN_INVENTORY = "En inventario: %d |4objeto:objetos;";
|
||||
ITEM_SLOTS_IGNORED = "Ignorado: %d |4casilla:casillas;";
|
||||
ITEM_MISSING = "Falta: %s";
|
||||
ITEMS_NOT_IN_INVENTORY = "Faltan: %d |4objeto:objetos;";
|
||||
TARGETFOCUS = "Fijar unidad objetivo";
|
||||
@@ -0,0 +1,12 @@
|
||||
if GetLocale() ~= "frFR" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Définir cible";
|
||||
BINDING_NAME_TARGETFOCUS = "Reprendre cible";
|
||||
FOCUSTARGET = "Définir la cible de focalisation";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4objet:objets;";
|
||||
ITEMS_EQUIPPED = "%d |4objet:objets; équipés";
|
||||
ITEMS_IN_INVENTORY = "%d |4objet:objets; dans l'inventaire";
|
||||
ITEM_SLOTS_IGNORED = "%d |4emplacement ignoré:emplacements ignorés;";
|
||||
ITEM_MISSING = "Objet manquant : %s";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4objet:objets; manquants";
|
||||
TARGETFOCUS = "Cibler l'unité focalisée";
|
||||
@@ -0,0 +1,9 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/..\FrameXML\UI.xsd">
|
||||
<Script file="deDE.lua"/>
|
||||
<Script file="enUS.lua"/>
|
||||
<Script file="esES.lua"/>
|
||||
<Script file="frFR.lua"/>
|
||||
<Script file="ruRU.lua"/>
|
||||
<Script file="zhCN.lua"/>
|
||||
<Script file="zhTW.lua"/>
|
||||
</Ui>
|
||||
@@ -0,0 +1,12 @@
|
||||
if GetLocale() ~= "ruRU" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Запомненная цель";
|
||||
BINDING_NAME_TARGETFOCUS = "Восстановить цель";
|
||||
FOCUSTARGET = "Запомнить цель";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
|
||||
ITEMS_EQUIPPED = "%d |4предмет экипирован:предмета экипировано:предметов экипировано;";
|
||||
ITEMS_IN_INVENTORY = "%d |4предмет:предмета:предметов; в сумках";
|
||||
ITEM_SLOTS_IGNORED = "%d |4ячейка пропущена:ячейки пропущены:ячейки пропущены;";
|
||||
ITEM_MISSING = "Предмет для ячейки %s отсутствует";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4предмет отсутствует:предмета отсутствует:предметов отсутствуют;";
|
||||
TARGETFOCUS = "Восстановить цель";
|
||||
@@ -0,0 +1,12 @@
|
||||
if GetLocale() ~= "zhCN" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "焦点目标";
|
||||
BINDING_NAME_TARGETFOCUS = "目标焦点";
|
||||
FOCUSTARGET = "设定焦点目标";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
ITEMS_EQUIPPED = "已装备%d件物品";
|
||||
ITEMS_IN_INVENTORY = "背包中有%d件物品";
|
||||
ITEM_SLOTS_IGNORED = "忽略%d个栏位";
|
||||
ITEM_MISSING = "%s缺失";
|
||||
ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
TARGETFOCUS = "选中焦点目标";
|
||||
@@ -0,0 +1,12 @@
|
||||
if GetLocale() ~= "zhTW" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "鎖定目標";
|
||||
BINDING_NAME_TARGETFOCUS = "選定鎖定目標";
|
||||
FOCUSTARGET = "設定鎖定目標";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
ITEMS_EQUIPPED = "裝備%d件物品";
|
||||
ITEMS_IN_INVENTORY = "%d件物品在物品欄中";
|
||||
ITEM_SLOTS_IGNORED = "忽略%d個欄位";
|
||||
ITEM_MISSING = "缺少%s物品";
|
||||
ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
TARGETFOCUS = "選定鎖定單位";
|
||||
+21
-1
@@ -47,9 +47,29 @@ add_custom_command(
|
||||
COMMENT "Embedding !!!ClassicAPI addon into ${EMBEDDED_CLASSICAPI_HEADER}"
|
||||
VERBATIM)
|
||||
|
||||
# Bundle `src/bindings/Bindings.xml` (our TARGETING-group additions)
|
||||
# into a generated header. `src/bindings/Inject.cpp` extracts the
|
||||
# inner `<Binding>` elements from the embedded buffer and splices them
|
||||
# into the engine's `Interface\FrameXML\Bindings.xml` at load time.
|
||||
set(EMBEDDED_BINDINGS_SRC "${CMAKE_CURRENT_SOURCE_DIR}/src/bindings/Bindings.xml")
|
||||
set(EMBEDDED_BINDINGS_HEADER "${CMAKE_BINARY_DIR}/generated/embedded_bindings.h")
|
||||
add_custom_command(
|
||||
OUTPUT "${EMBEDDED_BINDINGS_HEADER}"
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DINPUT_FILE=${EMBEDDED_BINDINGS_SRC}
|
||||
-DOUT_HEADER=${EMBEDDED_BINDINGS_HEADER}
|
||||
-DSYMBOL=kEmbeddedBindingsXml
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_file.cmake"
|
||||
DEPENDS
|
||||
"${EMBEDDED_BINDINGS_SRC}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_file.cmake"
|
||||
COMMENT "Embedding bindings XML into ${EMBEDDED_BINDINGS_HEADER}"
|
||||
VERBATIM)
|
||||
|
||||
add_library(${PROJECT_NAME} SHARED
|
||||
${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES}
|
||||
${EMBEDDED_CLASSICAPI_HEADER})
|
||||
${EMBEDDED_CLASSICAPI_HEADER}
|
||||
${EMBEDDED_BINDINGS_HEADER})
|
||||
|
||||
# Dev-build sentinel — represents "newer than every plausible
|
||||
# release". Encoded as v9999.99.99 = 9999 * 10000 + 99 * 100 + 99 =
|
||||
|
||||
@@ -35,16 +35,16 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura` |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.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.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellName`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellName`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [Table](docs/API.md#table) | `table.wipe` |
|
||||
@@ -132,6 +132,19 @@ functions, just behavior the stock 1.12 engine didn't have. See the
|
||||
| `nameplate1`..`nameplateN` | Unit behind the Nth visible nameplate, in creation-order. Works with every `UnitX` function — `UnitName`, `UnitGUID`, `UnitClass`, `UnitHealth`, etc. Suffix chains (`nameplate1target`, `nameplate1targettarget`) compose. See [NamePlate / Unit tokens](docs/API.md#unit-tokens-nameplaten). |
|
||||
| `focus` / `focustarget` | Sticky target set via [`FocusUnit`](docs/API.md#focusunitunit), cleared via [`ClearFocus`](docs/API.md#clearfocus). Same `UnitX` coverage as `nameplateN`. Fires [`PLAYER_FOCUS_CHANGED`](docs/API.md#player_focus_changed-event) on transition. See [Focus](docs/API.md#focus). |
|
||||
|
||||
### Bindings
|
||||
|
||||
Injected into the engine's **Targeting Functions** group at FrameXML
|
||||
Bindings.xml load time, so they appear in the keybind UI alongside
|
||||
native targeting bindings instead of orphaned at the bottom.
|
||||
|
||||
| Binding | Action |
|
||||
|---------|--------|
|
||||
| `FOCUSTARGET` | `FocusUnit("target")` — pin current target as focus |
|
||||
| `TARGETFOCUS` | `TargetUnit("focus")` — switch target to the focus |
|
||||
|
||||
See [Focus / Bindings](docs/API.md#bindings-focustarget--targetfocus) for the implementation note.
|
||||
|
||||
## Installation
|
||||
|
||||
Use [VanillaFixes](https://github.com/hannesmann/vanillafixes) to load the
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# embed_file.cmake — bundle a single file into a generated C++ header.
|
||||
#
|
||||
# Invoked from CMakeLists.txt via `cmake -P` as a custom command. Reads
|
||||
# INPUT_FILE and emits OUT_HEADER containing a constexpr byte array
|
||||
# named SYMBOL plus a `SYMBOL ## Size` size constant. Both live at file
|
||||
# scope; the header is meant to be included exactly once.
|
||||
#
|
||||
# Usage:
|
||||
# cmake -DINPUT_FILE=... -DOUT_HEADER=... -DSYMBOL=... -P embed_file.cmake
|
||||
|
||||
if(NOT INPUT_FILE)
|
||||
message(FATAL_ERROR "embed_file.cmake: INPUT_FILE required")
|
||||
endif()
|
||||
if(NOT OUT_HEADER)
|
||||
message(FATAL_ERROR "embed_file.cmake: OUT_HEADER required")
|
||||
endif()
|
||||
if(NOT SYMBOL)
|
||||
message(FATAL_ERROR "embed_file.cmake: SYMBOL required")
|
||||
endif()
|
||||
|
||||
file(READ "${INPUT_FILE}" CONTENT_HEX HEX)
|
||||
string(LENGTH "${CONTENT_HEX}" HEX_LEN)
|
||||
math(EXPR FILE_SIZE "${HEX_LEN} / 2")
|
||||
|
||||
# Convert hex string to comma-separated byte literals. 16 bytes per
|
||||
# line keeps the generated header readable.
|
||||
set(BYTES "")
|
||||
set(COL 0)
|
||||
set(POS 0)
|
||||
while(POS LESS HEX_LEN)
|
||||
string(SUBSTRING "${CONTENT_HEX}" ${POS} 2 BYTE)
|
||||
if(COL EQUAL 0)
|
||||
string(APPEND BYTES " ")
|
||||
endif()
|
||||
string(APPEND BYTES "0x${BYTE},")
|
||||
math(EXPR POS "${POS} + 2")
|
||||
math(EXPR COL "${COL} + 1")
|
||||
if(COL EQUAL 16)
|
||||
string(APPEND BYTES "\n")
|
||||
set(COL 0)
|
||||
else()
|
||||
string(APPEND BYTES " ")
|
||||
endif()
|
||||
endwhile()
|
||||
if(NOT COL EQUAL 0)
|
||||
string(APPEND BYTES "\n")
|
||||
endif()
|
||||
|
||||
set(HEADER_CONTENT "// Auto-generated by embed_file.cmake — do not edit.
|
||||
// Source: ${INPUT_FILE}
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
static constexpr unsigned char ${SYMBOL}[] = {
|
||||
${BYTES}};
|
||||
static constexpr size_t ${SYMBOL}Size = sizeof(${SYMBOL});
|
||||
")
|
||||
|
||||
file(WRITE "${OUT_HEADER}" "${HEADER_CONTENT}")
|
||||
message(STATUS "embed_file: wrote ${OUT_HEADER} (${FILE_SIZE} bytes)")
|
||||
+902
-754
File diff suppressed because it is too large
Load Diff
+132
@@ -18,6 +18,18 @@ enum Offsets {
|
||||
FUN_INVALID_FUNCTION_PTR_CHECK = 0x42A320,
|
||||
FUN_LOAD_SCRIPT_FUNCTIONS = 0x490250,
|
||||
|
||||
// Fatal-error dispatcher. `__fastcall(uint code)`. Writes `code`
|
||||
// to `DAT_00882738` and chains into the process-teardown path;
|
||||
// `FUN_00403BC0` reads the code at exit and shows a localized
|
||||
// popup keyed off it. Code `10` = "interface files are corrupt"
|
||||
// — the engine fires this from FrameXML's `FUN_0048FBF0` hash
|
||||
// mismatch and per-addon `FUN_0051F240` hash mismatch (plus
|
||||
// other call sites we haven't pinned down by byte pattern).
|
||||
// Hooked by `Security::FrameXMLBypass` to suppress code 10 so
|
||||
// our in-memory file modifications (Bindings.xml splice +
|
||||
// embedded `!!!ClassicAPI` addon) don't trigger termination.
|
||||
FUN_FATAL_ERROR = 0x00401560,
|
||||
|
||||
// Master glue Lua init — clean linear caller of all 5 glue batch
|
||||
// trampolines (60 main + 11 char-select + 24 char-create + 10 realm
|
||||
// + 4 frame globals = 109 total). Body: 0x0046ABB0..0x0046ABD2,
|
||||
@@ -1056,6 +1068,26 @@ enum Offsets {
|
||||
// With `requestIfMissing=false`, returns NULL if not in cache (no server
|
||||
// round-trip) — exactly the "instant" semantics we want.
|
||||
FUN_DBCACHE_ITEMSTATS_GET_RECORD = 0x0055BA30,
|
||||
// Item-cache response handler — the function called from
|
||||
// `0x00555140` (success path for `SMSG_ITEM_QUERY_SINGLE_RESPONSE`)
|
||||
// and `0x00555160` (failure path). Parses one or more itemIDs out
|
||||
// of the packet, looks up (or creates) the cache entry per ID,
|
||||
// copies the item data block via `FUN_007c9640` to `entry+0x18`,
|
||||
// sets `[entry+0x1F0]=1` (the loaded byte), then walks the entry's
|
||||
// pending callback list at `[entry+0x1FC]` and invokes each with
|
||||
// `(userData, success)`. High-bit-on itemID in the packet signals
|
||||
// a per-item failure for the batch.
|
||||
//
|
||||
// We hook this to observe any item-cache fill the engine performs
|
||||
// — covers the engine's natural inventory prefetch and any
|
||||
// addon-triggered queries without having to register our own
|
||||
// engine callbacks. Lets `RequestLoadItemData` be purely passive:
|
||||
// we track itemIDs, the engine (or whoever) issues the query, we
|
||||
// see the fill via this hook and fire `ITEM_DATA_LOAD_RESULT`.
|
||||
//
|
||||
// Signature: `__thiscall(void *cache, void *packetReader, int flag)`.
|
||||
// `cache` is the same `VAR_ITEMDB_CACHE` we already use.
|
||||
FUN_ITEMSTATS_CACHE_RESPONSE = 0x0055BDB0,
|
||||
// ItemStats_C field offsets we read. Full struct layout in
|
||||
// VanillaHelpers's `Game.h` (`struct ItemStats_C`); we only need these.
|
||||
OFF_ITEMSTATS_CLASS = 0x00,
|
||||
@@ -1488,6 +1520,35 @@ enum Offsets {
|
||||
VAR_SKILL_LINE_COUNT = 0x00C0D930,
|
||||
OFF_SKILL_LINE_NAME = 0x0C,
|
||||
|
||||
// Player's known-skills table — separate from the DBC. Populated
|
||||
// by the engine on login from server data; entries are pointers
|
||||
// to per-skill records. Each record's `+0x00` is the
|
||||
// `SkillLine.dbc` row (or `0` for header / category rows), and
|
||||
// `+0x10` is the current skill rank (other rank/modifier fields
|
||||
// follow). Source: `Script_GetSkillLineInfo` at `0x004D3610`,
|
||||
// which walks this array indexed by the Lua arg.
|
||||
VAR_PLAYER_SKILL_LIST = 0x00B7318C,
|
||||
VAR_PLAYER_SKILL_COUNT = 0x00B731B8,
|
||||
OFF_PLAYER_SKILL_LINE_ID = 0x00,
|
||||
OFF_PLAYER_SKILL_RANK = 0x10,
|
||||
|
||||
// Per-itemClass proficiency bitmap maintained by the engine.
|
||||
// 17 u32 entries (indexed by Item.dbc `m_class`, 0..16). Each
|
||||
// entry is a bitmask where bit N is set if the player has
|
||||
// proficiency in subclass N of that item class — i.e.
|
||||
// `table[4] & (1 << 3)` = "can wear Mail" (armor=class 4,
|
||||
// mail=subclass 3).
|
||||
//
|
||||
// Populated by SMSG_SET_PROFICIENCY's handler at
|
||||
// `FUN_005E7B70`, which writes `table[itemClass] = mask`
|
||||
// verbatim from the packet. Same source the engine itself uses
|
||||
// for equip eligibility (the tooltip "Mail" red-text logic
|
||||
// and the cursor-drop-on-paperdoll check both read this). Note:
|
||||
// covers **weapons too** (class 2), making it strictly more
|
||||
// general than walking the per-skill-line table for individual
|
||||
// armor proficiencies.
|
||||
VAR_PROFICIENCY_TABLE = 0x00C4D4A0,
|
||||
|
||||
// Spell.dbc `BaseLevel` — the level a spell becomes available
|
||||
// (trainer offers it, quest reward grants it, etc.). Distinct
|
||||
// from `SpellLevel` at +0x74 which is the effective level used
|
||||
@@ -2012,6 +2073,67 @@ enum Offsets {
|
||||
// to cast next", so we check this slot too.
|
||||
VAR_QUEUED_CAST_SPELL = 0x00CECAA8,
|
||||
|
||||
// Spell-placement state bitmask. Non-zero when the engine is
|
||||
// waiting on user input for an in-progress cast — the AoE reticle,
|
||||
// unit-target cursor, trade-target cursor, etc. Bits encode what
|
||||
// kind of input is needed; the engine's click handlers read this
|
||||
// to decide whether a click was relevant. Read by
|
||||
// `Script_SpellIsTargeting` via `FUN_006E48A0` (returns
|
||||
// `state != 0`). Bits the engine OR-sets via `FUN_006E5250` when
|
||||
// entering placement (subset relevant to us):
|
||||
// - `0x20` source world location needed
|
||||
// - `0x40` dest world location needed
|
||||
// The `0x60` mask therefore identifies "ground-target placement"
|
||||
// (vs. unit-target, trade-target, etc.). See `Spell::AtCursor`.
|
||||
VAR_SPELL_PLACEMENT_STATE = 0x00CECAC0,
|
||||
SPELL_PLACEMENT_GROUND_MASK = 0x60,
|
||||
|
||||
// `FUN_006E60F0(float *xyz)` — commits the in-flight ground-target
|
||||
// placement at the given world position. Reads three contiguous
|
||||
// floats from `xyz`; checks the placement-state mask
|
||||
// (`VAR_SPELL_PLACEMENT_STATE`) to decide whether this is a
|
||||
// source-coord (`0x20`) or dest-coord (`0x40`) commit; clears the
|
||||
// matching bit and stores the coords into engine globals which the
|
||||
// cast-finalize path then reads. Self-gates when neither bit is
|
||||
// set — safe no-op for unit-target placement. Called by the engine
|
||||
// itself when a player left-clicks ground while the reticle is up.
|
||||
FUN_COMMIT_PLACEMENT_COORDS = 0x006E60F0,
|
||||
|
||||
// `FUN_006E4900()` — cancels in-flight placement mode. Internally
|
||||
// checks `VAR_SPELL_PLACEMENT_STATE != 0` and, if so, fires the
|
||||
// cast-cancel path via `FUN_006E4940(0x1C)`. Safe to call when no
|
||||
// placement is active (no-ops). Wraps the action behind
|
||||
// `Script_SpellStopTargeting`.
|
||||
FUN_STOP_PLACEMENT = 0x006E4900,
|
||||
|
||||
// `*VAR_WORLDFRAME_CLICK_INFO` is a pointer to the engine's
|
||||
// CWorldFrame "click info" struct — the data block where the input
|
||||
// dispatcher stashes "what is the cursor pointed at right now"
|
||||
// after every mouse motion. Allocated at world enter, populated
|
||||
// continuously by the rendering pipeline (viewport bounds and
|
||||
// camera position at fixed offsets) and the OS message pump
|
||||
// (cursor screen pos). NULL when not in world.
|
||||
// - `+0x350` (uint32) click type after last raycast:
|
||||
// `0` = no hit
|
||||
// `1` = world / terrain hit
|
||||
// `2` = unit / object hit
|
||||
// - `+0x360..+0x368` (3 floats) hit world position (when type == 1)
|
||||
VAR_WORLDFRAME_CLICK_INFO = 0x00B4B2BC,
|
||||
OFF_CLICK_INFO_TYPE = 0x350,
|
||||
OFF_CLICK_INFO_COORDS = 0x360,
|
||||
|
||||
// `FUN_00481F00(void *clickInfo)` — refreshes the cursor's "what
|
||||
// am I pointed at?" state on demand. Internally reads cursor
|
||||
// screen pos from input state at `[clickInfo+0xA0]+0x1118/+0x111C`,
|
||||
// calls the screen→world ray builder at `FUN_004813B0` (uses
|
||||
// viewport bounds at `[clickInfo+0x390..+0x39C]` plus camera
|
||||
// position at `[clickInfo+0x65B8]+8/+0xC/+0x10`), then raycasts
|
||||
// via `FUN_0069BFF0` (terrain + WMO intersection). Writes click
|
||||
// type to `[clickInfo+0x350]` and hit coords to
|
||||
// `[clickInfo+0x360..+0x368]`. Safe to call from arbitrary code
|
||||
// as long as the player is in-world.
|
||||
FUN_REFRESH_CURSOR_RAYCAST = 0x00481F00,
|
||||
|
||||
// `Script_CastSpellByName` — engine's Lua wrapper for the
|
||||
// `CastSpellByName(name [, onSelf])` global. `int __fastcall(void *L)`
|
||||
// — standard Lua C function ABI. Reads `name` from stack[1] (string)
|
||||
@@ -2021,6 +2143,16 @@ enum Offsets {
|
||||
// results). Callable directly from C++ — push args on stack, call.
|
||||
FUN_SCRIPT_CAST_SPELL_BY_NAME = 0x004B4AB0,
|
||||
|
||||
// Inner spell-cast dispatcher — `__fastcall(slot, bookType, targetGuidLo,
|
||||
// targetGuidHi)`. `Script_CastSpellByName` calls this after resolving
|
||||
// the spell name to a spellbook slot. For ground-target spells, the
|
||||
// engine enters placement mode (sets `VAR_SPELL_PLACEMENT_STATE`)
|
||||
// and returns; placement is resolved later by mouse-click or
|
||||
// programmatically via `FUN_COMMIT_PLACEMENT_COORDS`. Passing (0, 0)
|
||||
// for the GUID is the "no implicit target" form — engine falls back
|
||||
// to the player's current selected target.
|
||||
FUN_SPELL_CAST_DISPATCH = 0x004B3300,
|
||||
|
||||
// Macro "primary spell" parser — `__fastcall(MacroEntry *entry)`.
|
||||
// Walks the macro body (stored at `entry + OFF_MACRO_BODY`) line
|
||||
// by line, looking for `/cast <name>`, `/castsequence <name>`,
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "bindings/Inject.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -248,6 +249,14 @@ void DecideSource() {
|
||||
int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
size_t *outSize, size_t extraBytes,
|
||||
int flag1, int flag2) {
|
||||
// FrameXML Bindings.xml gets an inline splice for our TARGETING
|
||||
// additions — see `bindings/Inject.cpp`. Returns true with the
|
||||
// patched buffer ready for the engine's binding parser to consume.
|
||||
if (Bindings::Inject::TryHandle(unused, path, outBuf, outSize,
|
||||
extraBytes, flag1, flag2, FileRead_o)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *suffix = StripAddonPrefix(path);
|
||||
if (suffix == nullptr) {
|
||||
// Path isn't under `Interface\AddOns\!!!ClassicAPI\` — straight
|
||||
|
||||
+12
-1
@@ -115,13 +115,24 @@ int PushFieldOrNil(void *L, FieldByName_t accessor) {
|
||||
// `C_AddOns.GetAddOnName(indexOrName)` — the addon's directory name.
|
||||
// For numeric input, returns the engine's canonical casing. For
|
||||
// string input, returns the input verbatim once existence is
|
||||
// confirmed.
|
||||
// confirmed; nil for an unregistered name.
|
||||
//
|
||||
// String-input existence isn't free: `ResolveAddOnName` echoes the
|
||||
// string for the field accessors below (`GetAddOnTitle`, etc.) which
|
||||
// short-circuit cleanly on unknown names via the engine's own NULL-
|
||||
// return paths. `GetAddOnName` doesn't call any engine accessor, so
|
||||
// it'd otherwise echo any string. Probe the registry explicitly.
|
||||
int __fastcall Script_GetAddOnName(void *L) {
|
||||
const uint8_t *entry = ResolveAddOnName(L);
|
||||
if (entry == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_STRING &&
|
||||
!AddOnExistsByName(reinterpret_cast<const char *>(entry))) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushString(L, reinterpret_cast<const char *>(entry));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<!-- Spliced into the engine's Interface\FrameXML\Bindings.xml at
|
||||
load time by src/bindings/Inject.cpp. The `header` attribute on
|
||||
the root tag tells the splicer which engine section to extend —
|
||||
it picks the appropriate insertion point so the bindings appear
|
||||
at the tail of that group in the keybind UI. The engine's own
|
||||
XML parser ignores attributes on the root tag, so this is
|
||||
metadata for the splicer only. -->
|
||||
<Bindings header="TARGETING">
|
||||
<Binding name="FOCUSTARGET">
|
||||
FocusUnit("target");
|
||||
</Binding>
|
||||
<Binding name="TARGETFOCUS">
|
||||
TargetUnit("focus");
|
||||
</Binding>
|
||||
</Bindings>
|
||||
@@ -0,0 +1,297 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Inject ClassicAPI-owned bindings into a chosen engine section.
|
||||
//
|
||||
// Why a file-read hook rather than an addon-side Bindings.xml: vanilla
|
||||
// stores every binding in a single linearly-indexed table, and the
|
||||
// keybind UI walks that table in index order with `HEADER_*` pseudo-
|
||||
// entries splitting groups. Addon Bindings.xml files get parsed
|
||||
// AFTER FrameXML, so even with `header="TARGETING"` an addon binding
|
||||
// receives a display index higher than every engine binding — it
|
||||
// renders orphaned at the very bottom of the keybind list rather than
|
||||
// under TARGETING.
|
||||
//
|
||||
// To land inside a specific section, the binding's display index has
|
||||
// to fall between that section's `HEADER_*` entry and the next one.
|
||||
// The cleanest way to arrange that is to splice the bindings into the
|
||||
// FrameXML file the engine itself parses, so they take their indices
|
||||
// from the same running counter every native binding does.
|
||||
//
|
||||
// What gets spliced is driven by `src/bindings/Bindings.xml`. The
|
||||
// root `<Bindings header="X">` tag names the engine section to extend
|
||||
// (e.g. `"TARGETING"`); the inner `<Binding>` children are spliced
|
||||
// verbatim. The splicer locates `header="X"` in the engine file
|
||||
// (which appears on the first binding of section X), then the next
|
||||
// `header="..."` after that (start of the following section), and
|
||||
// inserts our content just before that next-section marker — so our
|
||||
// entries land at the tail of section X. If X is the last section in
|
||||
// the file, the splice goes just before `</Bindings>`.
|
||||
//
|
||||
// The engine's binding parser is `FUN_004b6f70`. It calls
|
||||
// `FUN_FILE_READ` (`0x00648620`) to load the file, then walks `<Binding>`
|
||||
// elements assigning each the next display index from a running
|
||||
// counter. By the time our injected bindings are processed, the
|
||||
// engine has already emitted the section's `HEADER_*` and a sequence
|
||||
// of native entries; ours append to that sequence and the next
|
||||
// section's header follows.
|
||||
|
||||
#include "Inject.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include "embedded_bindings.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace Bindings::Inject {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kFrameXMLBindingsPath = "Interface\\FrameXML\\Bindings.xml";
|
||||
|
||||
// Parsed view of `src/bindings/Bindings.xml`:
|
||||
// - `header` is the value of `header="..."` on the root `<Bindings>`
|
||||
// tag. Tells the splicer which engine section to extend.
|
||||
// - `innerData / innerSize` point at the bytes between the
|
||||
// `<Bindings ...>` opening and `</Bindings>` closing tags.
|
||||
//
|
||||
// All-zero fields mean "embedded buffer is malformed" — defensive,
|
||||
// shouldn't happen since the source is build-time-validated. Splice
|
||||
// is skipped (engine sees its original Bindings.xml unchanged).
|
||||
struct EmbeddedConfig {
|
||||
char header[64];
|
||||
const char *innerData;
|
||||
size_t innerSize;
|
||||
};
|
||||
|
||||
// Advances `p` past any `<!-- ... -->` comment block starting at the
|
||||
// current position. Returns the new position (past the closing `-->`)
|
||||
// if a comment was skipped, or `p` unchanged if no comment was at
|
||||
// the current position. Caps at `end` if the comment is unterminated.
|
||||
const char *SkipCommentIfAny(const char *p, const char *end) {
|
||||
if (p + 4 > end) return p;
|
||||
if (std::memcmp(p, "<!--", 4) != 0) return p;
|
||||
p += 4;
|
||||
while (p + 3 <= end) {
|
||||
if (std::memcmp(p, "-->", 3) == 0)
|
||||
return p + 3;
|
||||
++p;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
// Linear scan for `marker` in `[start, end)`, skipping any `<!--...-->`
|
||||
// comment blocks (where unrelated occurrences of XML tags might live).
|
||||
const char *FindOutsideComment(const char *start, const char *end, const char *marker) {
|
||||
const size_t markerLen = std::strlen(marker);
|
||||
const char *p = start;
|
||||
while (p + markerLen <= end) {
|
||||
const char *afterComment = SkipCommentIfAny(p, end);
|
||||
if (afterComment != p) {
|
||||
p = afterComment;
|
||||
continue;
|
||||
}
|
||||
if (std::memcmp(p, marker, markerLen) == 0)
|
||||
return p;
|
||||
++p;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Parses `header="X"` out of the byte range `[tagStart, tagEnd)` (the
|
||||
// opening `<Bindings ...>` tag). Copies the value into `out` (size
|
||||
// `outSize`) and returns true on success.
|
||||
bool ExtractHeaderAttr(const char *tagStart, const char *tagEnd,
|
||||
char *out, size_t outSize) {
|
||||
static constexpr const char kAttr[] = "header=\"";
|
||||
constexpr size_t kAttrLen = sizeof(kAttr) - 1;
|
||||
for (const char *p = tagStart; p + kAttrLen < tagEnd; ++p) {
|
||||
if (std::memcmp(p, kAttr, kAttrLen) != 0) continue;
|
||||
const char *valStart = p + kAttrLen;
|
||||
const char *valEnd = valStart;
|
||||
while (valEnd < tagEnd && *valEnd != '"') ++valEnd;
|
||||
const size_t len = static_cast<size_t>(valEnd - valStart);
|
||||
if (len == 0 || len + 1 > outSize) return false;
|
||||
std::memcpy(out, valStart, len);
|
||||
out[len] = '\0';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
EmbeddedConfig ResolveEmbeddedConfig() {
|
||||
EmbeddedConfig out = {};
|
||||
const char *content = reinterpret_cast<const char *>(kEmbeddedBindingsXml);
|
||||
const char *end = content + kEmbeddedBindingsXmlSize;
|
||||
|
||||
// Locate `<Bindings` opening tag (skipping any comment blocks
|
||||
// that mention the tag literally for documentation purposes).
|
||||
const char *open = FindOutsideComment(content, end, "<Bindings");
|
||||
if (open == nullptr) return out;
|
||||
|
||||
// Find the closing `>` of the opening tag.
|
||||
const char *tagEnd = open;
|
||||
while (tagEnd < end && *tagEnd != '>') ++tagEnd;
|
||||
if (tagEnd == end) return out;
|
||||
|
||||
// Pull out the `header="..."` attribute. Required — this tells
|
||||
// the splicer which engine section to extend.
|
||||
if (!ExtractHeaderAttr(open, tagEnd, out.header, sizeof(out.header)))
|
||||
return out;
|
||||
|
||||
const char *innerStart = tagEnd + 1;
|
||||
const char *close = FindOutsideComment(innerStart, end, "</Bindings>");
|
||||
if (close == nullptr || close < innerStart) {
|
||||
out.header[0] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
out.innerData = innerStart;
|
||||
out.innerSize = static_cast<size_t>(close - innerStart);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Computes the byte offset in the engine's Bindings.xml buffer where
|
||||
// our entries should be spliced — i.e., the tail of the section
|
||||
// identified by `headerName`. Strategy:
|
||||
//
|
||||
// 1. Find `header="<HEADER>"` in the engine's buffer (skipping
|
||||
// comments). This is the attribute the engine itself uses on
|
||||
// the FIRST binding of each section, so this finds where that
|
||||
// section *starts*.
|
||||
// 2. Find the NEXT `header="` after that. That marks the start of
|
||||
// the FOLLOWING section.
|
||||
// 3. Back up from that next-header position to the `<` of the
|
||||
// `<Binding` tag containing it — that's our splice point.
|
||||
// 4. If there's no next header (last section in the file), splice
|
||||
// just before `</Bindings>`.
|
||||
//
|
||||
// Returns `nullptr` if step 1 fails (header missing from engine file
|
||||
// — e.g., a heavily-modified FrameXML). Caller falls back to passing
|
||||
// the original buffer through unchanged.
|
||||
const char *FindSplicePosition(const char *content, size_t size,
|
||||
const char *headerName) {
|
||||
const char *end = content + size;
|
||||
|
||||
char headerNeedle[80];
|
||||
std::snprintf(headerNeedle, sizeof(headerNeedle),
|
||||
"header=\"%s\"", headerName);
|
||||
|
||||
const char *sectionStart = FindOutsideComment(content, end, headerNeedle);
|
||||
if (sectionStart == nullptr) return nullptr;
|
||||
|
||||
// Walk past the matched attribute, then look for the next
|
||||
// `header="` anywhere later in the file.
|
||||
const char *afterCurrent = sectionStart + std::strlen(headerNeedle);
|
||||
const char *nextHeader = FindOutsideComment(afterCurrent, end, "header=\"");
|
||||
|
||||
if (nextHeader != nullptr) {
|
||||
// Back up to the `<` of the `<Binding` tag that owns this
|
||||
// next-header attribute — that `<` is where the next section
|
||||
// visually starts, and we splice immediately before it.
|
||||
const char *p = nextHeader;
|
||||
while (p > content && *p != '<') --p;
|
||||
return p;
|
||||
}
|
||||
|
||||
// Last section in the file — splice just before `</Bindings>`.
|
||||
return FindOutsideComment(content, end, "</Bindings>");
|
||||
}
|
||||
|
||||
char NormalizeChar(char c) {
|
||||
if (c == '/') return '\\';
|
||||
if (c >= 'A' && c <= 'Z') return static_cast<char>(c + 32);
|
||||
return c;
|
||||
}
|
||||
|
||||
bool PathEqualsCI(const char *a, const char *b) {
|
||||
while (*a != '\0' && *b != '\0') {
|
||||
if (NormalizeChar(*a) != NormalizeChar(*b)) return false;
|
||||
++a; ++b;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
using SMemAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
|
||||
using SMemFree_t = void(__stdcall *)(void *buf, const char *file, int line, int flags);
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
size_t extraBytes, int flag1, int flag2, FileReadFn original) {
|
||||
if (path == nullptr || !PathEqualsCI(path, kFrameXMLBindingsPath))
|
||||
return false;
|
||||
|
||||
// Pull the real file through the original reader with the caller's
|
||||
// requested `extraBytes` honored — the original allocates a Storm
|
||||
// buffer of `fileSize + extraBytes` and zero-fills the tail.
|
||||
void *origBuf = nullptr;
|
||||
size_t origSize = 0;
|
||||
const int readOk = original(unused, path, &origBuf, &origSize,
|
||||
extraBytes, flag1, flag2);
|
||||
if (readOk == 0 || origBuf == nullptr)
|
||||
return false;
|
||||
|
||||
static const EmbeddedConfig config = ResolveEmbeddedConfig();
|
||||
if (config.innerData == nullptr || config.innerSize == 0 ||
|
||||
config.header[0] == '\0') {
|
||||
// Embedded XML is missing the `<Bindings header="...">` root
|
||||
// or the `</Bindings>` close tag. Pass the original through.
|
||||
if (outBuf != nullptr) *outBuf = origBuf;
|
||||
if (outSize != nullptr) *outSize = origSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *content = static_cast<const char *>(origBuf);
|
||||
const char *splice = FindSplicePosition(content, origSize, config.header);
|
||||
if (splice == nullptr) {
|
||||
// Requested section isn't present in this engine file (heavily
|
||||
// modified FrameXML or different build). Pass through unchanged
|
||||
// rather than risking corruption.
|
||||
if (outBuf != nullptr) *outBuf = origBuf;
|
||||
if (outSize != nullptr) *outSize = origSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
const size_t splicePos = static_cast<size_t>(splice - content);
|
||||
const size_t newSize = origSize + config.innerSize;
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
auto SMemFree = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
|
||||
void *newBuf = SMemAlloc(newSize + extraBytes, __FILE__, __LINE__, 0);
|
||||
if (newBuf == nullptr) {
|
||||
// Allocation failed — fall back to the original buffer so the
|
||||
// engine still loads its own bindings.
|
||||
if (outBuf != nullptr) *outBuf = origBuf;
|
||||
if (outSize != nullptr) *outSize = origSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto *dest = static_cast<char *>(newBuf);
|
||||
std::memcpy(dest, content, splicePos);
|
||||
std::memcpy(dest + splicePos, config.innerData, config.innerSize);
|
||||
std::memcpy(dest + splicePos + config.innerSize,
|
||||
content + splicePos, origSize - splicePos);
|
||||
if (extraBytes > 0)
|
||||
std::memset(dest + newSize, 0, extraBytes);
|
||||
|
||||
SMemFree(origBuf, __FILE__, __LINE__, 0);
|
||||
|
||||
if (outBuf != nullptr) *outBuf = newBuf;
|
||||
if (outSize != nullptr) *outSize = newSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Bindings::Inject
|
||||
@@ -0,0 +1,37 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Bindings::Inject {
|
||||
|
||||
// `FUN_FILE_READ` shape — Storm-style: __stdcall, returns 1 on success
|
||||
// with the buffer allocated via SMemAlloc into `*outBuf` (size in
|
||||
// `*outSize`, plus `extraBytes` of trailing zero padding).
|
||||
using FileReadFn = int(__stdcall *)(int unused, const char *path,
|
||||
void **outBuf, size_t *outSize,
|
||||
size_t extraBytes,
|
||||
int flag1, int flag2);
|
||||
|
||||
// Path-matches `Interface\FrameXML\Bindings.xml`. On match: fetches
|
||||
// the real file via `original`, splices our ClassicAPI bindings into
|
||||
// the TARGETING block, and returns `true` with `*outBuf` / `*outSize`
|
||||
// set to a fresh Storm buffer the caller can SMemFree normally.
|
||||
// Returns `false` for any other path — caller should fall through to
|
||||
// its own logic.
|
||||
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
size_t extraBytes, int flag1, int flag2, FileReadFn original);
|
||||
|
||||
} // namespace Bindings::Inject
|
||||
+129
-38
@@ -331,6 +331,19 @@ int __fastcall Script_ClearIgnoredSlotsForSave(void *L) {
|
||||
// would race with). Reads the per-CGItem instance flag at
|
||||
// `OFF_ITEM_CLIENT_LOCK` bit 0 — same bit `C_Item.IsLocked` checks
|
||||
// and the engine's own pickup/unlock paths manipulate.
|
||||
//
|
||||
// Gates on `Locations::FindGUID` BEFORE consulting the lock flag:
|
||||
// for items the player has deleted/mailed/traded away, the engine's
|
||||
// CGItem cache often retains a stale entry that `ResolveByGUID`
|
||||
// still hands back, but the inventory walk no longer finds the GUID
|
||||
// anywhere. Reading the lock flag off the stale CGItem reflects
|
||||
// whatever bits were last set on it (frequently the "locked"
|
||||
// in-transaction state from when it was being deleted/swapped),
|
||||
// which would falsely report the set as "contains locked items"
|
||||
// and prevent `UseEquipmentSet` from running through addon
|
||||
// wrappers that gate on this check. The inventory presence check
|
||||
// matches modern semantics: a missing item is *missing*, not
|
||||
// *locked*.
|
||||
int __fastcall Script_EquipmentSetContainsLockedItems(void *L) {
|
||||
const uint32_t setID = ArgSetID(L, 1);
|
||||
const Set *s = Data::FindByID(setID);
|
||||
@@ -341,6 +354,8 @@ int __fastcall Script_EquipmentSetContainsLockedItems(void *L) {
|
||||
const uint64_t g = s->items[i];
|
||||
if (g == GUID_EMPTY || g == GUID_IGNORED)
|
||||
continue;
|
||||
if (Locations::FindGUID(g) == 0)
|
||||
continue; // not in inventory — treat as missing, not locked
|
||||
const uint8_t *item = Locations::ResolveItemByGUID(g);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
@@ -424,60 +439,136 @@ int __fastcall Script_UseEquipmentSet(void *L) {
|
||||
}
|
||||
int nextFreeSlot = 0;
|
||||
|
||||
// Pre-snapshot each set item's location against the ORIGINAL
|
||||
// state, before any swap packets fly. This mirrors how 4.3.4's
|
||||
// `Script_UseEquipmentSet` (`0x009A15D0`) builds its action list:
|
||||
// resolve all 19 source locations up front, then execute. The
|
||||
// engine's CGItem / InvMgr state lags behind our packet sends
|
||||
// (waits for server ACK), so any `FindGUID` mid-execution would
|
||||
// return stale answers and we'd re-resolve items into the wrong
|
||||
// place.
|
||||
int sourceLoc[SLOT_COUNT];
|
||||
for (int i = 0; i < SLOT_COUNT; ++i) {
|
||||
const uint64_t g = guids[i];
|
||||
if (g == GUID_EMPTY || g == GUID_IGNORED) {
|
||||
sourceLoc[i] = 0;
|
||||
continue;
|
||||
}
|
||||
sourceLoc[i] = Locations::FindGUID(g);
|
||||
}
|
||||
|
||||
// Virtual paperdoll occupant model — seeded from the engine's
|
||||
// initial paperdoll GUID array, then updated in-place after each
|
||||
// swap so phase 1 can chain rotations and phase 3 can spot
|
||||
// already-empty slots.
|
||||
uint64_t virtualPaperdoll[SLOT_COUNT];
|
||||
Locations::SnapshotPaperdoll(virtualPaperdoll);
|
||||
|
||||
// Locates the current virtual slot of a GUID on the paperdoll,
|
||||
// or 0 if it's not currently there. Used during phase 1 so
|
||||
// chained paperdoll-to-paperdoll moves operate on the *current*
|
||||
// virtual position, not the stale pre-snapshot.
|
||||
auto currentPaperdollSlot = [&](uint64_t g) -> int {
|
||||
if (g == 0)
|
||||
return 0;
|
||||
for (int slot = 1; slot <= SLOT_COUNT; ++slot) {
|
||||
if (virtualPaperdoll[slot - 1] == g)
|
||||
return slot;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// ── Phase 1: paperdoll → paperdoll ─────────────────────────────
|
||||
// Move items already on the paperdoll into their target slots.
|
||||
// Source and destination are both paperdoll, so atomic swaps
|
||||
// never touch bag state — phase 2's bag lookups stay accurate.
|
||||
for (int i = 0; i < SLOT_COUNT; ++i) {
|
||||
const int targetSlot = i + 1;
|
||||
const uint64_t g = guids[i];
|
||||
|
||||
// GUID_IGNORED: leave whatever the player has in this slot
|
||||
// as-is. Distinct from GUID_EMPTY (= 0), which means "this
|
||||
// slot should be empty after the swap" — handled below.
|
||||
if (g == GUID_IGNORED)
|
||||
if (g == GUID_EMPTY || g == GUID_IGNORED)
|
||||
continue;
|
||||
|
||||
if (g == GUID_EMPTY) {
|
||||
// Set wants this slot empty. If it already is, no-op.
|
||||
// Otherwise unequip whatever's there into the next bag
|
||||
// slot we reserved during the pre-loop snapshot. If
|
||||
// we've exhausted the snapshot, silently skip — matches
|
||||
// the modern API which surfaces no per-slot reason.
|
||||
const uint8_t *equipped =
|
||||
Item::Location::ResolveEquipmentSlot(targetSlot);
|
||||
if (equipped == nullptr)
|
||||
continue;
|
||||
if (nextFreeSlot >= freeSlotCount)
|
||||
continue;
|
||||
const FreeSlot &fs = freeSlots[nextFreeSlot++];
|
||||
Item::Swap::ToBag(equipped, targetSlot, fs.bag, fs.slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
const int loc = Locations::FindGUID(g);
|
||||
const int loc = sourceLoc[i];
|
||||
if (loc == 0)
|
||||
continue; // missing — nothing to do
|
||||
|
||||
// Already in its target slot? Skip.
|
||||
if ((loc & LOC_BAGS) == 0 && (loc & LOC_BANK) == 0 &&
|
||||
(loc & LOC_SLOT_MASK) == targetSlot)
|
||||
continue;
|
||||
|
||||
// In bank (main or bank-bag) — server rejects equip-from-bank.
|
||||
if (loc & LOC_BANK)
|
||||
continue;
|
||||
continue; // server rejects equip-from-bank
|
||||
if (loc & LOC_BAGS)
|
||||
continue; // phase 2
|
||||
|
||||
const int srcSlot = currentPaperdollSlot(g);
|
||||
if (srcSlot == 0 || srcSlot == targetSlot)
|
||||
continue; // missing or already there
|
||||
|
||||
const uint8_t *item = Locations::ResolveItemByGUID(g);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
|
||||
if (loc & LOC_BAGS) {
|
||||
const int srcBag = (loc >> LOC_BAG_SHIFT) & 0xFF;
|
||||
const int srcSlot = loc & LOC_SLOT_MASK;
|
||||
Item::Swap::FromBag(item, srcBag, srcSlot, targetSlot);
|
||||
} else {
|
||||
const int srcSlot = loc & LOC_SLOT_MASK;
|
||||
Item::Swap::FromPaperdoll(item, srcSlot, targetSlot);
|
||||
const uint64_t displaced = virtualPaperdoll[targetSlot - 1];
|
||||
if (Item::Swap::FromPaperdoll(item, srcSlot, targetSlot)) {
|
||||
virtualPaperdoll[targetSlot - 1] = g;
|
||||
virtualPaperdoll[srcSlot - 1] = displaced;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 2: bag → paperdoll ───────────────────────────────────
|
||||
// Move items from bag slots onto the paperdoll. Each atomic swap
|
||||
// displaces the current paperdoll occupant into the bag slot the
|
||||
// incoming item vacated. Anything we still wanted on the paperdoll
|
||||
// was already placed by phase 1, so phase 2 displacements can't
|
||||
// strand a future set item in a bag.
|
||||
for (int i = 0; i < SLOT_COUNT; ++i) {
|
||||
const int targetSlot = i + 1;
|
||||
const uint64_t g = guids[i];
|
||||
if (g == GUID_EMPTY || g == GUID_IGNORED)
|
||||
continue;
|
||||
if (virtualPaperdoll[targetSlot - 1] == g)
|
||||
continue; // already at target (probably by phase 1)
|
||||
|
||||
const int loc = sourceLoc[i];
|
||||
if (loc == 0)
|
||||
continue;
|
||||
if (loc & LOC_BANK)
|
||||
continue;
|
||||
if ((loc & LOC_BAGS) == 0)
|
||||
continue; // not from a bag — phase 1 handled, or item missing
|
||||
|
||||
const uint8_t *item = Locations::ResolveItemByGUID(g);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
|
||||
const int srcBag = (loc >> LOC_BAG_SHIFT) & 0xFF;
|
||||
const int srcSlot = loc & LOC_SLOT_MASK;
|
||||
if (Item::Swap::FromBag(item, srcBag, srcSlot, targetSlot)) {
|
||||
virtualPaperdoll[targetSlot - 1] = g;
|
||||
// The displaced item lands in (srcBag, srcSlot). We
|
||||
// don't track bag-side virtual state — by phase 3 we
|
||||
// only care about the paperdoll occupant.
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 3: unequip slots the set wants empty ─────────────────
|
||||
// For each `GUID_EMPTY` slot, send the current paperdoll occupant
|
||||
// (if any) to a pre-snapshotted free bag slot. Runs last so any
|
||||
// displaced items from phases 1/2 that landed in the slot get
|
||||
// unequipped here too.
|
||||
for (int i = 0; i < SLOT_COUNT; ++i) {
|
||||
const int targetSlot = i + 1;
|
||||
if (guids[i] != GUID_EMPTY)
|
||||
continue;
|
||||
const uint64_t occupant = virtualPaperdoll[targetSlot - 1];
|
||||
if (occupant == 0)
|
||||
continue;
|
||||
if (nextFreeSlot >= freeSlotCount)
|
||||
continue;
|
||||
const uint8_t *equipped = Locations::ResolveItemByGUID(occupant);
|
||||
if (equipped == nullptr)
|
||||
continue;
|
||||
const FreeSlot &fs = freeSlots[nextFreeSlot++];
|
||||
if (Item::Swap::ToBag(equipped, targetSlot, fs.bag, fs.slot))
|
||||
virtualPaperdoll[targetSlot - 1] = 0;
|
||||
}
|
||||
|
||||
const int evt = Event::Custom::Lookup(kSwapFinishedEvent);
|
||||
if (evt >= 0)
|
||||
Event::Custom::Fire(evt, "%d%d", 1, static_cast<int>(setID));
|
||||
|
||||
@@ -177,4 +177,15 @@ const uint8_t *ResolveItemByGUID(uint64_t guid) {
|
||||
return Item::Location::ResolveByGUID(guid);
|
||||
}
|
||||
|
||||
void SnapshotPaperdoll(uint64_t *out) {
|
||||
for (int i = 0; i < SLOT_COUNT; ++i)
|
||||
out[i] = 0;
|
||||
auto *invMgr = ResolvePlayerInvMgr();
|
||||
auto *guids = InvMgrGuidArray(invMgr);
|
||||
if (guids == nullptr)
|
||||
return;
|
||||
for (int i = 0; i < SLOT_COUNT; ++i)
|
||||
out[i] = guids[i];
|
||||
}
|
||||
|
||||
} // namespace EquipmentSet::Locations
|
||||
|
||||
@@ -42,4 +42,16 @@ int FindGUID(uint64_t targetGuid);
|
||||
// isn't known to the client. Side-effect free.
|
||||
const uint8_t *ResolveItemByGUID(uint64_t guid);
|
||||
|
||||
// Copies the current paperdoll GUIDs into `out[0..SLOT_COUNT-1]`,
|
||||
// where `out[i]` is the GUID at 1-based equipment slot `i+1`.
|
||||
// Empty / unequipped slots write 0. Reads from the player invMgr's
|
||||
// flat GUID array directly — no Lua stack, no engine state changes.
|
||||
//
|
||||
// Used by `UseEquipmentSet` to seed a "virtual paperdoll" model that
|
||||
// stays current across in-flight swaps. The engine's CGItem state
|
||||
// lags behind packet sends (server has to ACK each swap), so any
|
||||
// subsequent `FindGUID` after the first packet would return stale
|
||||
// pre-swap locations. Tracking moves locally bypasses that lag.
|
||||
void SnapshotPaperdoll(uint64_t *out);
|
||||
|
||||
} // namespace EquipmentSet::Locations
|
||||
|
||||
@@ -74,6 +74,22 @@ inline void Fire(int eventID, const char *format, Args... args) {
|
||||
fn(eventID, format, args...);
|
||||
}
|
||||
|
||||
// Convenience for the modern WoW `(id: number, success: bool)` event
|
||||
// shape (ITEM_DATA_LOAD_RESULT, GET_ITEM_INFO_RECEIVED,
|
||||
// QUEST_DATA_LOAD_RESULT, …). The engine's printf-style dispatcher has
|
||||
// no `%b`, so a naive `%d%d` with `0` for failure surfaces `arg2 = 0` —
|
||||
// truthy in Lua, breaking the canonical `if success then` branch. We
|
||||
// push `1` for success and `nil` for failure instead, leaning on the
|
||||
// engine's `lua_pushstring(L, NULL) → lua_pushnil` tail-jump so the
|
||||
// dispatcher itself emits the nil without any pre-staging. Lua handlers
|
||||
// can then use the natural idiom: nil is falsy, `1` is truthy.
|
||||
inline void FireIdSuccess(int eventID, int id, bool success) {
|
||||
if (success)
|
||||
Fire(eventID, "%d%d", id, 1);
|
||||
else
|
||||
Fire(eventID, "%d%s", id, static_cast<const char *>(nullptr));
|
||||
}
|
||||
|
||||
// Internal: try to claim a slot for every reservation that's still
|
||||
// unclaimed (`slot < 0`). Called from the `Frame::RegisterEvent` hook
|
||||
// in DllMain — every time Lua calls `frame:RegisterEvent(...)`, we
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
// per-frame `OnMouseDown` scripts — these fire even when the click
|
||||
// misses any UI frame.
|
||||
//
|
||||
// `IsMouseButtonDown([button])` — query the current held state of a
|
||||
// specific button by name or 1-based ID, or "any button held" when
|
||||
// called with no args. State is maintained from the same hook stream
|
||||
// as the events (single source of truth — events firing and the
|
||||
// state being readable happen at the same point in the pipeline).
|
||||
//
|
||||
// Same `WH_GETMESSAGE` thread-message hook pattern as
|
||||
// [Input::Modifier](Modifier.cpp): we intercept the WM_*BUTTON*
|
||||
// messages on the engine's main thread *before* dispatch. The hook
|
||||
@@ -28,6 +34,9 @@
|
||||
#include "Game.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace Input::GlobalMouse {
|
||||
@@ -37,6 +46,19 @@ namespace {
|
||||
constexpr const char *kDownEvent = "GLOBAL_MOUSE_DOWN";
|
||||
constexpr const char *kUpEvent = "GLOBAL_MOUSE_UP";
|
||||
|
||||
// Held-state bitmap — bit `i` set means the button with 1-based
|
||||
// `IsMouseButtonDown` ID `i+1` is currently held. Modern API IDs:
|
||||
// 1 = LeftButton, 2 = RightButton, 3 = MiddleButton,
|
||||
// 4 = Button4 (XBUTTON1), 5 = Button5 (XBUTTON2).
|
||||
constexpr int BIT_LEFT_BUTTON = 0;
|
||||
constexpr int BIT_RIGHT_BUTTON = 1;
|
||||
constexpr int BIT_MIDDLE_BUTTON = 2;
|
||||
constexpr int BIT_BUTTON4 = 3;
|
||||
constexpr int BIT_BUTTON5 = 4;
|
||||
constexpr uint32_t MASK_ANY_BUTTON = 0x1F;
|
||||
|
||||
uint32_t g_mouseButtonMask = 0;
|
||||
|
||||
HHOOK g_msgHook = nullptr;
|
||||
|
||||
// Maps WM_*BUTTON{DOWN,UP} → ("LeftButton"|..., isDown). Returns
|
||||
@@ -60,16 +82,60 @@ const char *DecodeButton(UINT msg, WPARAM wParam, bool *outDown) {
|
||||
}
|
||||
}
|
||||
|
||||
// Maps a "LeftButton"/.../"Button5" name to its bit index. -1 for
|
||||
// any other string.
|
||||
int NameToBitIdx(const char *name) {
|
||||
if (name == nullptr)
|
||||
return -1;
|
||||
if (std::strcmp(name, "LeftButton") == 0) return BIT_LEFT_BUTTON;
|
||||
if (std::strcmp(name, "RightButton") == 0) return BIT_RIGHT_BUTTON;
|
||||
if (std::strcmp(name, "MiddleButton") == 0) return BIT_MIDDLE_BUTTON;
|
||||
if (std::strcmp(name, "Button4") == 0) return BIT_BUTTON4;
|
||||
if (std::strcmp(name, "Button5") == 0) return BIT_BUTTON5;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ProcessMouseMessage(UINT msg, WPARAM wParam) {
|
||||
bool down = false;
|
||||
const char *name = DecodeButton(msg, wParam, &down);
|
||||
if (name == nullptr)
|
||||
return;
|
||||
|
||||
const int bitIdx = NameToBitIdx(name);
|
||||
if (bitIdx >= 0) {
|
||||
const uint32_t bit = 1u << bitIdx;
|
||||
if (down) g_mouseButtonMask |= bit;
|
||||
else g_mouseButtonMask &= ~bit;
|
||||
}
|
||||
|
||||
const int slot = Event::Custom::Lookup(down ? kDownEvent : kUpEvent);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%s", name);
|
||||
}
|
||||
|
||||
int __fastcall Script_IsMouseButtonDown(void *L) {
|
||||
bool down;
|
||||
if (Game::Lua::GetTop(L) == 0) {
|
||||
// No-arg form: any button held.
|
||||
down = (g_mouseButtonMask & MASK_ANY_BUTTON) != 0;
|
||||
} else {
|
||||
int bitIdx = -1;
|
||||
if (Game::Lua::IsNumber(L, 1)) {
|
||||
const int id = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (id >= 1 && id <= 5)
|
||||
bitIdx = id - 1;
|
||||
} else if (Game::Lua::IsString(L, 1)) {
|
||||
bitIdx = NameToBitIdx(Game::Lua::ToString(L, 1));
|
||||
}
|
||||
// Unrecognized button (bad id, unknown name) → false, matching
|
||||
// the modern API's "did this specific button happen to be held"
|
||||
// semantic. Bad input doesn't promote to the any-button check.
|
||||
down = (bitIdx >= 0) && (g_mouseButtonMask & (1u << bitIdx)) != 0;
|
||||
}
|
||||
Game::Lua::PushBool(L, down);
|
||||
return 1;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK GetMsgHook(int code, WPARAM wParam, LPARAM lParam) {
|
||||
// Only process when the message is being removed from the queue
|
||||
// (`PM_REMOVE` / `GetMessage`); `PM_NOREMOVE` peeks would deliver
|
||||
@@ -88,7 +154,11 @@ void InstallHook() {
|
||||
GetCurrentThreadId());
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() { InstallHook(); }
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("IsMouseButtonDown",
|
||||
&Script_IsMouseButtonDown);
|
||||
InstallHook();
|
||||
}
|
||||
|
||||
const Event::Custom::AutoReserve _reserveDown{kDownEvent};
|
||||
const Event::Custom::AutoReserve _reserveUp{kUpEvent};
|
||||
|
||||
+153
-29
@@ -17,6 +17,7 @@
|
||||
#include "item/Arg.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/Location.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -52,16 +53,14 @@ static constexpr const char *kGetItemInfoReceived = "GET_ITEM_INFO_RECEIVED";
|
||||
static const Event::Custom::AutoReserve _reserveItemDataLoadResult{kItemDataLoadResult};
|
||||
static const Event::Custom::AutoReserve _reserveGetItemInfoReceived{kGetItemInfoReceived};
|
||||
|
||||
static void FireGetItemInfoReceived(int itemID, int success) {
|
||||
const int slot = Event::Custom::Lookup(kGetItemInfoReceived);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%d%d", itemID, success);
|
||||
static void FireGetItemInfoReceived(int itemID, bool success) {
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kGetItemInfoReceived),
|
||||
itemID, success);
|
||||
}
|
||||
|
||||
static void FireItemDataLoadResult(int itemID, int success) {
|
||||
const int slot = Event::Custom::Lookup(kItemDataLoadResult);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%d%d", itemID, success);
|
||||
static void FireItemDataLoadResult(int itemID, bool success) {
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kItemDataLoadResult),
|
||||
itemID, success);
|
||||
}
|
||||
|
||||
// Engine callback for **implicit** cache fills (transparent warmup
|
||||
@@ -76,16 +75,7 @@ static void FireItemDataLoadResult(int itemID, int success) {
|
||||
// callback would crash the engine, so stdcall is verified by induction.
|
||||
static void __stdcall ItemLoadCallback_Implicit(void *userData, int success) {
|
||||
const auto itemID = static_cast<int>(reinterpret_cast<uintptr_t>(userData));
|
||||
FireGetItemInfoReceived(itemID, static_cast<int>(success != 0));
|
||||
}
|
||||
|
||||
// Engine callback for **explicit** cache fills (player-side
|
||||
// `C_Item.RequestLoadItemData(ByID)` calls). Fires ITEM_DATA_LOAD_RESULT
|
||||
// only — the implicit GET_ITEM_INFO_RECEIVED path is for ambient cache
|
||||
// fills, not for callers who explicitly asked.
|
||||
static void __stdcall ItemLoadCallback_Explicit(void *userData, int success) {
|
||||
const auto itemID = static_cast<int>(reinterpret_cast<uintptr_t>(userData));
|
||||
FireItemDataLoadResult(itemID, static_cast<int>(success != 0));
|
||||
FireGetItemInfoReceived(itemID, success != 0);
|
||||
}
|
||||
|
||||
using ItemLoadCallback_t = void(__stdcall *)(void *userData, int success);
|
||||
@@ -103,6 +93,15 @@ using ItemLoadCallback_t = void(__stdcall *)(void *userData, int success);
|
||||
// VanillaHelpers documented as `requestIfMissing` — that slot is reused
|
||||
// as a local immediately after entry, never read. The trigger for the
|
||||
// request path is `callback != NULL`.
|
||||
//
|
||||
// The 6th arg IS used elsewhere though: when non-zero, the engine
|
||||
// walks the entry's existing pending-callback list and skips the
|
||||
// append if a node with matching `(callback, userData)` is already
|
||||
// queued. We always pass `1` so that bursts of `WarmCache` calls for
|
||||
// the same itemID (e.g. Bagnon enumerating the same slot repeatedly
|
||||
// while the SMSG_ITEM_QUERY_SINGLE_RESPONSE is in flight) collapse to
|
||||
// a single callback registration — otherwise the response fires our
|
||||
// `GET_ITEM_INFO_RECEIVED` once per call.
|
||||
static const uint8_t *CacheFetch(uint32_t itemID, ItemLoadCallback_t callback) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
@@ -110,7 +109,7 @@ static const uint8_t *CacheFetch(uint32_t itemID, ItemLoadCallback_t callback) {
|
||||
void *cb = reinterpret_cast<void *>(callback);
|
||||
void *userData =
|
||||
callback ? reinterpret_cast<void *>(static_cast<uintptr_t>(itemID)) : nullptr;
|
||||
return fn(cache, itemID, &zeroGuid, cb, userData, 0);
|
||||
return fn(cache, itemID, &zeroGuid, cb, userData, 1);
|
||||
}
|
||||
|
||||
// Resolves an item-location table at stack idx to the itemID by walking
|
||||
@@ -148,18 +147,143 @@ static int __fastcall Script_IsItemDataCached(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Explicit-request path: kicks off the cache fill via the explicit
|
||||
// callback (which fires ITEM_DATA_LOAD_RESULT). If the item is already
|
||||
// cached, the engine won't invoke our callback — synthesize the event
|
||||
// so addons get the same notification regardless of cache state,
|
||||
// matching modern `C_Item.RequestLoadItemData(ByID)` semantics.
|
||||
static void RequestAndMaybeNotify(uint32_t itemID) {
|
||||
const bool wasCached = (CacheFetch(itemID, nullptr) != nullptr);
|
||||
CacheFetch(itemID, &ItemLoadCallback_Explicit);
|
||||
if (wasCached)
|
||||
FireItemDataLoadResult(static_cast<int>(itemID), 1);
|
||||
// Pending explicit-request tracking. We deliberately do NOT eagerly
|
||||
// create cache entries when an addon calls `C_Item.RequestLoadItemData`
|
||||
// at addon-load / PLAYER_ENTERING_WORLD time — empirically (pfQuest),
|
||||
// pre-creating the entry blocks the engine's natural prefetch path
|
||||
// from issuing a fresh `SMSG_ITEM_QUERY_SINGLE`. The entry stays in a
|
||||
// permanently-pending state because nothing ever sends the query.
|
||||
//
|
||||
// Instead we hook the engine's item-cache response handler at
|
||||
// `FUN_ITEMSTATS_CACHE_RESPONSE` (the function that fills cache
|
||||
// entries on `SMSG_ITEM_QUERY_SINGLE_RESPONSE`). After the engine
|
||||
// finishes filling any entries and walking its own callback lists,
|
||||
// we sweep our pending set and fire `ITEM_DATA_LOAD_RESULT(itemID, 1)`
|
||||
// for any tracked items that now resolve. The engine's natural
|
||||
// prefetch (for player-inventory items) takes care of the actual
|
||||
// `SMSG_ITEM_QUERY_SINGLE` for us.
|
||||
//
|
||||
// For items the engine *doesn't* naturally prefetch (e.g. quest
|
||||
// reward IDs the player has never owned), we escalate after
|
||||
// `kEscalateTicks` by issuing our own `CacheFetch` with a no-op
|
||||
// callback — that creates the entry and queues the query, and the
|
||||
// response handler hook still fires our event when the data lands.
|
||||
// `kMaxWaitTicks` is the hard timeout that gives up and fires
|
||||
// `ITEM_DATA_LOAD_RESULT(itemID, nil)`.
|
||||
struct Pending {
|
||||
uint32_t itemID;
|
||||
int ticksWaiting;
|
||||
bool requestIssued;
|
||||
};
|
||||
|
||||
constexpr int kMaxPending = 64;
|
||||
constexpr int kEscalateTicks = 60;
|
||||
constexpr int kMaxWaitTicks = 1200;
|
||||
|
||||
static Pending g_pending[kMaxPending];
|
||||
static int g_pendingCount = 0;
|
||||
|
||||
static int FindPending(uint32_t itemID) {
|
||||
for (int i = 0; i < g_pendingCount; ++i) {
|
||||
if (g_pending[i].itemID == itemID)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void RemovePendingAt(int idx) {
|
||||
--g_pendingCount;
|
||||
if (idx != g_pendingCount)
|
||||
g_pending[idx] = g_pending[g_pendingCount];
|
||||
}
|
||||
|
||||
// No-op callback for the escalation path — we just need a non-null
|
||||
// callback pointer so the engine's `_GetRecord` queues the query.
|
||||
// The response handler hook will detect the fill and fire the event;
|
||||
// this callback doesn't need to do anything.
|
||||
static void __stdcall ItemLoadCallback_NoOp(void * /*userData*/, int /*success*/) {}
|
||||
|
||||
static void RequestAndMaybeNotify(uint32_t itemID) {
|
||||
// Already loaded — fire success synchronously.
|
||||
if (CacheFetch(itemID, nullptr) != nullptr) {
|
||||
FireItemDataLoadResult(static_cast<int>(itemID), true);
|
||||
return;
|
||||
}
|
||||
// Already tracking — second request just dedups.
|
||||
if (FindPending(itemID) >= 0)
|
||||
return;
|
||||
if (g_pendingCount >= kMaxPending)
|
||||
return;
|
||||
g_pending[g_pendingCount].itemID = itemID;
|
||||
g_pending[g_pendingCount].ticksWaiting = 0;
|
||||
g_pending[g_pendingCount].requestIssued = false;
|
||||
++g_pendingCount;
|
||||
}
|
||||
|
||||
// Response handler hook. After the engine fills cache entries from
|
||||
// `SMSG_ITEM_QUERY_SINGLE_RESPONSE` and walks the per-entry callback
|
||||
// lists, we sweep `g_pending` for anything now-loaded and fire the
|
||||
// event. This is the engine-aligned alternative to per-tick polling:
|
||||
// we observe fills exactly when they happen, with zero added latency
|
||||
// and no risk of racing the engine's own callback walk.
|
||||
//
|
||||
// `__thiscall` is mimicked as `__fastcall(ecx, edx_unused, ...)` per
|
||||
// the convention used elsewhere in this codebase (see
|
||||
// `KeyringDescChange_h`, `FrameRegisterEvent_h`).
|
||||
using ItemStatsCacheResponse_t = void(__thiscall *)(void *cache, void *packetReader, int flag);
|
||||
static ItemStatsCacheResponse_t ItemStatsCacheResponse_o = nullptr;
|
||||
|
||||
static void __fastcall ItemStatsCacheResponse_h(void *cache, void * /*edx_unused*/,
|
||||
void *packetReader, int flag) {
|
||||
using Trampoline_t = void(__fastcall *)(void *cache, void *edx, void *packetReader, int flag);
|
||||
reinterpret_cast<Trampoline_t>(ItemStatsCacheResponse_o)(cache, nullptr, packetReader, flag);
|
||||
// Sweep pending — the engine just finished filling any matching
|
||||
// entries. Peek (no callback) returns non-null iff the entry's
|
||||
// loaded flag is set.
|
||||
for (int i = 0; i < g_pendingCount;) {
|
||||
const uint32_t itemID = g_pending[i].itemID;
|
||||
if (CacheFetch(itemID, nullptr) != nullptr) {
|
||||
RemovePendingAt(i);
|
||||
FireItemDataLoadResult(static_cast<int>(itemID), true);
|
||||
continue;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookCacheResponse{
|
||||
Offsets::FUN_ITEMSTATS_CACHE_RESPONSE,
|
||||
reinterpret_cast<void *>(&ItemStatsCacheResponse_h),
|
||||
reinterpret_cast<void **>(&ItemStatsCacheResponse_o)};
|
||||
|
||||
// Escalation + timeout. The hook covers the happy path (engine fills
|
||||
// cache → we fire). This tick is only responsible for:
|
||||
// 1. Issuing a query for items the engine doesn't naturally prefetch
|
||||
// (e.g. quest rewards) — wait `kEscalateTicks` so we don't race
|
||||
// the engine's startup-time state machine, then call CacheFetch
|
||||
// with a no-op callback. The engine sends the SMSG, the response
|
||||
// handler hook fires the event when the response lands.
|
||||
// 2. Giving up after `kMaxWaitTicks` for items that never resolve
|
||||
// (bad itemID, server doesn't respond, etc.) — fire failure.
|
||||
static void OnWorldTick() {
|
||||
for (int i = 0; i < g_pendingCount;) {
|
||||
if (!g_pending[i].requestIssued && g_pending[i].ticksWaiting >= kEscalateTicks) {
|
||||
CacheFetch(g_pending[i].itemID, &ItemLoadCallback_NoOp);
|
||||
g_pending[i].requestIssued = true;
|
||||
}
|
||||
if (g_pending[i].ticksWaiting >= kMaxWaitTicks) {
|
||||
const uint32_t itemID = g_pending[i].itemID;
|
||||
RemovePendingAt(i);
|
||||
FireItemDataLoadResult(static_cast<int>(itemID), false);
|
||||
continue;
|
||||
}
|
||||
++g_pending[i].ticksWaiting;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
// Public C++ API — see Item/Data.h. Implicit (transparent) warmup
|
||||
// path used by the `Script_GetItemInfo` hook and `SetItemByID`. Fires
|
||||
// the cache request only if the item isn't already cached; uses the
|
||||
|
||||
+5
-49
@@ -166,49 +166,6 @@ int __fastcall Script_C_Item_IsEquippedItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Walks bags 0..4 looking for an item matching `arg`. On hit, sets
|
||||
// `*outBag` / `*outSlot` / `*outItem` and returns true.
|
||||
//
|
||||
// itemID match is direct; name match peeks the item-cache record's
|
||||
// `m_name[0]` and compares case-insensitively. Stomps the Lua stack
|
||||
// per the helpers it calls.
|
||||
bool FindItemInBags(void *L, const Item::Arg::Resolved &arg, int *outBag, int *outSlot,
|
||||
const uint8_t **outItem) {
|
||||
for (int bag = 0; bag <= 4; ++bag) {
|
||||
const int slotCount = Item::Location::GetBagSlotCount(bag);
|
||||
for (int slot = 1; slot <= slotCount; ++slot) {
|
||||
const uint8_t *item = Item::Location::ResolveBag(L, bag, slot);
|
||||
if (item == nullptr) continue;
|
||||
|
||||
const int id = Item::ID::FromCGItem(item);
|
||||
if (id == 0) continue;
|
||||
|
||||
if (arg.itemID > 0) {
|
||||
if (id == arg.itemID) {
|
||||
*outBag = bag;
|
||||
*outSlot = slot;
|
||||
*outItem = item;
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
auto *record = PeekItemRecord(static_cast<uint32_t>(id));
|
||||
if (record == nullptr) continue;
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
if (name == nullptr) continue;
|
||||
if (_stricmp(name, arg.name) == 0) {
|
||||
*outBag = bag;
|
||||
*outSlot = slot;
|
||||
*outItem = item;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// `C_Item.EquipItemByName(itemInfo [, dstSlot])` — finds the first
|
||||
// item in the player's bags matching `itemInfo` (itemID, link, or
|
||||
// name) and equips it.
|
||||
@@ -255,14 +212,13 @@ int __fastcall Script_C_Item_EquipItemByName(void *L) {
|
||||
reinterpret_cast<int(__fastcall *)(void *)>(
|
||||
Offsets::FUN_SCRIPT_CLEAR_CURSOR)(L);
|
||||
|
||||
int bag = -1, slot = -1;
|
||||
const uint8_t *cgItem = nullptr;
|
||||
if (!FindItemInBags(L, arg, &bag, &slot, &cgItem)) {
|
||||
Item::Location::ByGUIDResult found;
|
||||
if (!Item::Location::FindByArgInBags(L, arg, &found)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (hasDstSlot) {
|
||||
Item::Swap::FromBag(cgItem, bag, slot, dstSlot);
|
||||
Item::Swap::FromBag(found.item, found.bagID, found.slotIndex, dstSlot);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -273,8 +229,8 @@ int __fastcall Script_C_Item_EquipItemByName(void *L) {
|
||||
// — `Script_AutoEquipCursorItem` is a thin wrapper that just
|
||||
// resolves the local player and calls this with flag=0.
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(bag));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(found.bagID));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(found.slotIndex));
|
||||
auto pickup = reinterpret_cast<ScriptFn_t>(Offsets::FUN_SCRIPT_PICKUP_CONTAINER_ITEM);
|
||||
pickup(L);
|
||||
|
||||
|
||||
+22
-33
@@ -14,28 +14,12 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Item::Exists {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// `C_Item.DoesItemExist(itemLocation)` — true iff the location resolves
|
||||
// to a populated inventory slot on the active player. Equipment-slot
|
||||
// locations check the character pane; bag-slot locations walk the
|
||||
@@ -51,27 +35,32 @@ static int __fastcall Script_C_Item_DoesItemExist(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Item.DoesItemExistByID(itemInfo)` — true iff the cache currently
|
||||
// has data for this itemID (i.e. `GetItemInfo` would return non-nil
|
||||
// right now). Same semantics as the modern function and the existing
|
||||
// Lua shim: cache miss → false, but we kick off the network query
|
||||
// so a subsequent call after `GET_ITEM_INFO_RECEIVED` will succeed.
|
||||
// `C_Item.DoesItemExistByID(itemInfo)` — true if `itemID` looks like a
|
||||
// valid item identifier. Modern WoW answers from the client-side item
|
||||
// database (Item-sparse.db2) and is independent of whether the network
|
||||
// cache has been warmed yet. 1.12 has no client-side DB, so we
|
||||
// optimistically accept any positive itemID; callers needing the
|
||||
// "is the cache populated?" check should use `IsItemDataCachedByID`.
|
||||
//
|
||||
// This split matters for `ItemMixin:IsItemEmpty` (FrameXML ItemUtil),
|
||||
// which uses `DoesItemExistByID` to gate `ContinueOnItemLoad` — the
|
||||
// whole point of `ContinueOnItemLoad` is to wait for an uncached item
|
||||
// to load, so the existence check must succeed for items that aren't
|
||||
// in the cache yet.
|
||||
//
|
||||
// We deliberately do NOT auto-warm the cache here. Existence is a pure
|
||||
// query about the ID, not a load request — and during early-login
|
||||
// (cold WDB, pfQuest's PLAYER_ENTERING_WORLD path) a spurious cache
|
||||
// touch can race the engine's natural inventory prefetch and leave the
|
||||
// item stuck. Callers wanting to actually load data should use
|
||||
// `RequestLoadItemDataByID`; passive readers (`GetItemInfo`,
|
||||
// `GetItemNameByID`) already warm on their own miss path.
|
||||
//
|
||||
// Accepts numeric itemID or `"item:NNN..."` string (including full
|
||||
// chat links). Returns false for any input we can't resolve to an
|
||||
// itemID; never raises.
|
||||
// chat links); never raises.
|
||||
static int __fastcall Script_C_Item_DoesItemExistByID(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
if (itemID <= 0) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
if (PeekItemRecord(static_cast<uint32_t>(itemID)) == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBoolean(L, 1);
|
||||
Game::Lua::PushBoolean(L, itemID > 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
+20
-3
@@ -93,13 +93,31 @@ static bool BuildIconPath(uint32_t displayInfoID, char *out, size_t outSize) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// `GetItemInfoInstant` is the modern "synchronous, never blocks" item
|
||||
// info call. In modern WoW the trailing 6 fields come from client-side
|
||||
// `Item-sparse.db2` and so the call always returns the full 7-tuple.
|
||||
// 1.12 has no client-side item DB — those fields live only in the
|
||||
// network-fed `DBCache_ItemStats`. We mirror the contract by:
|
||||
// - always returning the itemID (echoed from input) so callers using
|
||||
// `GetItemInfoInstant(link_or_id)` as an ID extractor never see nil
|
||||
// - returning the remaining 6 fields from the cache when present,
|
||||
// nil when not (preserving the 7-tuple shape)
|
||||
// - not warming the cache; "Instant" implies no side effects. Callers
|
||||
// that need the full info should use `GetItemInfo`, which already
|
||||
// triggers the warmup via the `Script_GetItemInfo` hook.
|
||||
static int __fastcall Script_GetItemInfoInstant(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(itemID));
|
||||
|
||||
const uint8_t *record = FetchItemRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
if (record == nullptr) {
|
||||
for (int i = 0; i < 6; ++i)
|
||||
Game::Lua::PushNil(L);
|
||||
return 7;
|
||||
}
|
||||
|
||||
const uint32_t classID =
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_ITEMSTATS_CLASS);
|
||||
@@ -114,7 +132,6 @@ static int __fastcall Script_GetItemInfoInstant(void *L) {
|
||||
if (!BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
iconPath[0] = 0;
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(itemID));
|
||||
Game::Lua::PushString(L, LookupItemClassName(classID));
|
||||
Game::Lua::PushString(L, LookupItemSubClassName(classID, subClassID));
|
||||
Game::Lua::PushString(L, LookupInvType(invType));
|
||||
|
||||
@@ -105,6 +105,47 @@ bool FitsSlot(int slot1Based, uint32_t invType) {
|
||||
return (kSlotMaskByInvType[invType] & (1u << (slot1Based - 1))) != 0;
|
||||
}
|
||||
|
||||
// ── Proficiency gate ──────────────────────────────────────────────
|
||||
// `FitsSlot` only checks invType→slot shape compatibility; without
|
||||
// also gating on the player's proficiencies this would return (e.g.)
|
||||
// mail helms for a warlock since both are `invType=HEAD`. Modern
|
||||
// WoW's equipment-set picker filters by what the player can actually
|
||||
// equip; we mirror that.
|
||||
//
|
||||
// Source of truth: the engine's per-itemClass proficiency bitmap at
|
||||
// `[VAR_PROFICIENCY_TABLE]` (17 u32 entries indexed by `m_class`).
|
||||
// SMSG_SET_PROFICIENCY's handler at `0x005E7B70` writes the bitmask
|
||||
// for each class verbatim from the wire. Bit N of `table[class]` =
|
||||
// "player can equip subclass N of this class". Same data the engine
|
||||
// itself reads for the tooltip "Mail/Leather/Plate" red-text and the
|
||||
// cursor-drop-on-paperdoll-slot check — covers both armor and weapon
|
||||
// proficiencies in one lookup.
|
||||
//
|
||||
// 4.3.4's `Script_GetInventoryItemsForSlot` does the equivalent check
|
||||
// via `FUN_00553AD0(itemClass) & (1 << subclass)`, reading from
|
||||
// `DAT_00DD4D38` (its own copy of the same packet-fed table).
|
||||
|
||||
bool PlayerHasProficiency(uint32_t itemClass, uint32_t subclass) {
|
||||
if (itemClass > 16)
|
||||
return true; // table only covers 0..16; be permissive past that
|
||||
const uint32_t mask = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PROFICIENCY_TABLE) + itemClass * 4);
|
||||
if (mask == 0)
|
||||
return true; // table not populated yet (pre-SMSG_SET_PROFICIENCY)
|
||||
// — fail open so empty-mask doesn't blank the list
|
||||
return (mask & (1u << subclass)) != 0;
|
||||
}
|
||||
|
||||
bool PlayerCanEquip(const uint8_t *record) {
|
||||
if (record == nullptr)
|
||||
return true; // missing record — be permissive, don't hide the item
|
||||
const uint32_t classCode = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CLASS);
|
||||
const uint32_t subclass = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
return PlayerHasProficiency(classCode, subclass);
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -112,11 +153,14 @@ const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
uint32_t ReadInventoryType(const uint8_t *cgItem) {
|
||||
const uint8_t *ItemRecordForCGItem(const uint8_t *cgItem) {
|
||||
const int itemID = Item::ID::FromCGItem(cgItem);
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
auto *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
return nullptr;
|
||||
return PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
}
|
||||
|
||||
uint32_t ReadInventoryType(const uint8_t *record) {
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint32_t *>(
|
||||
@@ -132,7 +176,10 @@ void CollectIfFits(int slot, const uint8_t *cgItem, int location,
|
||||
std::vector<Hit> &out) {
|
||||
if (cgItem == nullptr)
|
||||
return;
|
||||
if (!FitsSlot(slot, ReadInventoryType(cgItem)))
|
||||
const uint8_t *record = ItemRecordForCGItem(cgItem);
|
||||
if (!FitsSlot(slot, ReadInventoryType(record)))
|
||||
return;
|
||||
if (!PlayerCanEquip(record))
|
||||
return;
|
||||
out.push_back({location, cgItem});
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "Arg.h"
|
||||
#include "ID.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace Item::Location {
|
||||
|
||||
namespace {
|
||||
@@ -183,6 +186,44 @@ bool FindByItemID(void *L, int itemID, ByGUIDResult *out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FindByArgInBags(void *L, const Item::Arg::Resolved &arg, ByGUIDResult *out) {
|
||||
if (arg.itemID <= 0 && arg.name == nullptr)
|
||||
return false;
|
||||
|
||||
for (int bagID = 0; bagID <= 4; ++bagID) {
|
||||
const int slotCount = GetBagSlotCount(bagID);
|
||||
for (int slotIndex = 1; slotIndex <= slotCount; ++slotIndex) {
|
||||
auto *item = ResolveBag(L, bagID, slotIndex);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
|
||||
const int id = Item::ID::FromCGItem(item);
|
||||
if (id == 0)
|
||||
continue;
|
||||
|
||||
if (arg.itemID > 0) {
|
||||
if (id != arg.itemID)
|
||||
continue;
|
||||
} else {
|
||||
auto *record = PeekItemRecord(static_cast<uint32_t>(id));
|
||||
if (record == nullptr)
|
||||
continue;
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
if (name == nullptr || _stricmp(name, arg.name) != 0)
|
||||
continue;
|
||||
}
|
||||
|
||||
out->equipmentSlotIndex = 0;
|
||||
out->bagID = bagID;
|
||||
out->slotIndex = slotIndex;
|
||||
out->item = item;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FindByGUID(void *L, uint64_t guid, ByGUIDResult *out) {
|
||||
if (guid == 0)
|
||||
return false;
|
||||
|
||||
@@ -96,6 +96,26 @@ bool FindByGUID(void *L, uint64_t guid, ByGUIDResult *out);
|
||||
// for link-decoration purposes. Stomps the Lua stack.
|
||||
bool FindByItemID(void *L, int itemID, ByGUIDResult *out);
|
||||
|
||||
} // namespace Item::Location
|
||||
|
||||
// Forward-declare for `FindByArgInBags` below.
|
||||
namespace Item::Arg { struct Resolved; }
|
||||
|
||||
namespace Item::Location {
|
||||
|
||||
// Walks the player's bags (0..4 only — no equipment) looking for the
|
||||
// first item matching `arg`. `arg.itemID > 0` ⇒ direct ID compare;
|
||||
// otherwise `arg.name` is matched case-insensitively against the
|
||||
// cached `ItemStats_C.m_name[0]` of each candidate. On hit, populates
|
||||
// `out->bagID` / `out->slotIndex` / `out->item` and sets
|
||||
// `equipmentSlotIndex = 0`. Stomps the Lua stack.
|
||||
//
|
||||
// Used by the by-name action APIs (`C_Item.EquipItemByName`,
|
||||
// `C_Item.UseItemByName`, `C_Item.UseAtCursor`) — none of them want
|
||||
// equipment in the candidate pool (the engine doesn't equip-from-
|
||||
// equipped or use-from-equipped; the item has to be in bags first).
|
||||
bool FindByArgInBags(void *L, const Item::Arg::Resolved &arg, ByGUIDResult *out);
|
||||
|
||||
// Parses the `"0xHHHHHHHHLLLLLLLL"` GUID string format
|
||||
// `C_Item.GetItemGUID` returns. Strict: requires exactly `0x` prefix
|
||||
// + 16 case-insensitive hex digits. Returns false (and leaves `*out`
|
||||
|
||||
+62
-45
@@ -11,18 +11,31 @@
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Item.GetItemUniqueness(itemLocation)` — uniqueness cap. Reads
|
||||
// `ItemStats_C.m_maxCount` from the item's cached record; that's the
|
||||
// only uniqueness signal vanilla's client carries. `1` = unique
|
||||
// ("Held In Off-hand" trinkets, special quest items, etc.); `0` =
|
||||
// unlimited; higher values cap the inventory count.
|
||||
// `C_Item.GetItemUniqueness(itemLocation)` and the `*ByID` variant
|
||||
// — uniqueness cap. Both read `ItemStats_C.m_maxCount` from the
|
||||
// item's cached record; that's the only uniqueness signal vanilla
|
||||
// carries. `1` = unique-equipped (one at a time); higher values
|
||||
// cap the inventory count; `0` = unlimited.
|
||||
//
|
||||
// Modern WoW returns a 3-tuple `(maxEquipped, category, equippedCount)`
|
||||
// keyed off the post-TBC category system (e.g. "Unique-Equipped:
|
||||
// Brewfest Mug"). Vanilla has no category system, so we surface
|
||||
// `m_maxCount` as the first return and leave the category nil and
|
||||
// equipped count `0` — matching modern's "field present but inapplicable"
|
||||
// convention.
|
||||
// Modern WoW (Classic Era 1.15.x) gives the two functions **different
|
||||
// signatures**, verified against `_classic_` BlizzardInterfaceCode's
|
||||
// `ItemDocumentation.lua`:
|
||||
//
|
||||
// GetItemUniqueness(itemLocation) →
|
||||
// limitCategory (number, non-nilable),
|
||||
// limitMax (number, non-nilable)
|
||||
//
|
||||
// GetItemUniquenessByID(itemInfo) →
|
||||
// isUnique (bool, non-nilable),
|
||||
// limitCategoryName (string, nilable),
|
||||
// limitCategoryCount (number, nilable),
|
||||
// limitCategoryID (number, nilable)
|
||||
//
|
||||
// Vanilla has no `LimitCategory.dbc` system (TBC+ addition for
|
||||
// "Unique-Equipped: Brewfest Mug"-style cross-item category caps),
|
||||
// so the category fields are always nil/0. `isUnique` collapses to
|
||||
// `m_maxCount > 0`; `limitMax` and `limitCategoryCount` both surface
|
||||
// the raw `m_maxCount`.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -50,54 +63,58 @@ const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Pushes the 3-tuple `(maxAllowed, nil, 0)` derived from the
|
||||
// ItemStats_C record, or nil + early-return on cache miss.
|
||||
int PushFromItemID(void *L, uint32_t itemID) {
|
||||
// Returns the item's `m_maxCount` or -1 if the record isn't cached.
|
||||
// `-1` is a "warm-and-bail" sentinel; caller pushes nothing on -1
|
||||
// so the Lua side sees the modern "MayReturnNothing" behaviour.
|
||||
int ReadMaxCount(uint32_t itemID) {
|
||||
const uint8_t *record = PeekItemRecord(itemID);
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(itemID);
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
return -1;
|
||||
}
|
||||
const int maxCount = *reinterpret_cast<const int *>(
|
||||
return *reinterpret_cast<const int *>(
|
||||
record + Offsets::OFF_ITEMSTATS_MAX_COUNT);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(maxCount));
|
||||
Game::Lua::PushNil(L); // category (vanilla has none)
|
||||
Game::Lua::PushNumber(L, 0.0); // equippedCount (not tracked)
|
||||
return 3;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// `GetItemUniqueness(itemLocation)` shape: `(limitCategory, limitMax)`.
|
||||
static int __fastcall Script_GetItemUniqueness(void *L) {
|
||||
if (!Item::Location::IsLocationArg(L, 1)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (!Item::Location::IsLocationArg(L, 1))
|
||||
return 0;
|
||||
auto *item = Item::Location::Resolve(L, 1);
|
||||
if (item == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (item == nullptr)
|
||||
return 0;
|
||||
const int itemID = Item::ID::FromCGItem(item);
|
||||
if (itemID <= 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
return PushFromItemID(L, static_cast<uint32_t>(itemID));
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const int maxCount = ReadMaxCount(static_cast<uint32_t>(itemID));
|
||||
if (maxCount < 0)
|
||||
return 0; // cache miss — modern API documents "MayReturnNothing"
|
||||
Game::Lua::PushNumber(L, 0.0); // limitCategory (vanilla has no LimitCategory.dbc)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(maxCount));
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `GetItemUniquenessByID(itemID)` shape:
|
||||
// `(isUnique, limitCategoryName, limitCategoryCount, limitCategoryID)`.
|
||||
static int __fastcall Script_GetItemUniquenessByID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0;
|
||||
const int itemID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (itemID <= 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
return PushFromItemID(L, static_cast<uint32_t>(itemID));
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const int maxCount = ReadMaxCount(static_cast<uint32_t>(itemID));
|
||||
if (maxCount < 0)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, maxCount > 0);
|
||||
Game::Lua::PushNil(L); // limitCategoryName (no categories in vanilla)
|
||||
if (maxCount > 0)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(maxCount));
|
||||
else
|
||||
Game::Lua::PushNil(L); // limitCategoryCount — nil when not limited
|
||||
Game::Lua::PushNil(L); // limitCategoryID
|
||||
return 4;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
|
||||
+82
-54
@@ -11,9 +11,12 @@
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Item.UseItemByName(itemInfo)` — finds the first item in the
|
||||
// player's bags matching `itemInfo` (itemID, link, or name) and uses
|
||||
// it via the engine's `CGItem::UseItem` primitive.
|
||||
// `C_Item.UseItemByName(itemInfo [, unit])` — finds the first item in
|
||||
// the player's bags matching `itemInfo` (itemID, link, or name) and
|
||||
// uses it via the engine's `CGItem::UseItem` primitive. Optional
|
||||
// `unit` is a unit token (`"player"`, `"target"`, `"focus"`, …)
|
||||
// passed to spell-cast items (scrolls, traps, on-use targeted
|
||||
// effects) as the cast target.
|
||||
//
|
||||
// Same structure 3.3.5's `Script_UseItemByName` uses: locate the item
|
||||
// directly, then hand it to the engine's by-item-pointer use call.
|
||||
@@ -25,91 +28,116 @@
|
||||
// food / potion / hearthstone / scroll / quiver all route through
|
||||
// the same entry.
|
||||
//
|
||||
// Target arg: we pass nullptr / a zero GUID. The engine substitutes
|
||||
// the player itself for self-use items (hearthstone, food, potion);
|
||||
// for target-required items (scrolls, traps) the engine reads
|
||||
// cursor-target state at call time, which a Lua-side caller can
|
||||
// pre-set with `TargetUnit("...")`.
|
||||
// Target arg: `FUN_ITEM_USE`'s `param_1` is read as a `uint64_t *`
|
||||
// pointing at the target's GUID. The engine writes it through to the
|
||||
// "use item" event (`DAT_00c4c1d0`/`DAT_00c4c1d4` + event 0x122) for
|
||||
// spell-cast items, and into the cast-targeting helper at
|
||||
// `FUN_006e5a90`. For self-use items (hearthstone, food, potion) the
|
||||
// engine overwrites with the item's own GUID before dispatch, so a
|
||||
// stray target is harmless. We default to a zero GUID when the caller
|
||||
// omits `unit` — matches existing call sites.
|
||||
//
|
||||
// Returns nothing. Silently no-ops on bad input, item not found in
|
||||
// bags, item locked, on cooldown, etc.
|
||||
// bags, item locked, on cooldown, etc. An unrecognized `unit` string
|
||||
// is treated as "no target" rather than raising, matching arg-1's
|
||||
// "silently no-ops on bad input" contract.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace Item::Use {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
|
||||
using UseItem_t = unsigned(__thiscall *)(const void *item, const uint64_t *targetGuid,
|
||||
int flag);
|
||||
|
||||
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);
|
||||
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
|
||||
|
||||
bool HasPrefix(const char *s, const char *prefix) {
|
||||
return std::strncmp(s, prefix, std::strlen(prefix)) == 0;
|
||||
}
|
||||
|
||||
// Returns the matching CGItem* in `*outItem` on hit; mirrors the
|
||||
// `Item::Equipment::FindItemInBags` walk but skips bag/slot output
|
||||
// since `FUN_ITEM_USE` operates directly on the CGItem pointer.
|
||||
bool FindItemInBags(void *L, const Item::Arg::Resolved &arg, const uint8_t **outItem) {
|
||||
for (int bag = 0; bag <= 4; ++bag) {
|
||||
const int slotCount = Item::Location::GetBagSlotCount(bag);
|
||||
for (int slot = 1; slot <= slotCount; ++slot) {
|
||||
const uint8_t *item = Item::Location::ResolveBag(L, bag, slot);
|
||||
if (item == nullptr) continue;
|
||||
// Walk past any "target" suffixes (`targettarget`, `focustargettarget`,
|
||||
// …); returns a pointer at the first non-"target" character.
|
||||
const char *SkipTargetSuffixes(const char *s) {
|
||||
while (HasPrefix(s, "target"))
|
||||
s += 6;
|
||||
return s;
|
||||
}
|
||||
|
||||
const int id = Item::ID::FromCGItem(item);
|
||||
if (id == 0) continue;
|
||||
|
||||
if (arg.itemID > 0) {
|
||||
if (id == arg.itemID) {
|
||||
*outItem = item;
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
auto *record = PeekItemRecord(static_cast<uint32_t>(id));
|
||||
if (record == nullptr) continue;
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
if (name == nullptr) continue;
|
||||
if (_stricmp(name, arg.name) == 0) {
|
||||
*outItem = item;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Predicate mirror of the engine's token grammar inside
|
||||
// `FUN_TOKEN_TO_GUID` (`0x00515970`) plus the families our own
|
||||
// `Unit::TokenExtensions` hook adds (`focus`, `nameplateN`). The
|
||||
// engine raises a Lua error on unknown input, so we gate before
|
||||
// dispatching to keep our "silently no-ops on bad input" contract.
|
||||
//
|
||||
// Every base token may be followed by zero or more `target` suffixes
|
||||
// (`playertarget`, `party1targettarget`, …) which the engine's
|
||||
// resolver walks via `LAB_005159d3..0x00515A2C`.
|
||||
bool MatchBase(const char *s, const char *base, bool requireDigits) {
|
||||
if (!HasPrefix(s, base))
|
||||
return false;
|
||||
const char *rest = s + std::strlen(base);
|
||||
if (requireDigits) {
|
||||
const char *p = rest;
|
||||
while (*p >= '0' && *p <= '9')
|
||||
++p;
|
||||
if (p == rest)
|
||||
return false;
|
||||
rest = p;
|
||||
}
|
||||
return *SkipTargetSuffixes(rest) == '\0';
|
||||
}
|
||||
|
||||
bool LooksLikeToken(const char *s) {
|
||||
if (s == nullptr || *s == '\0')
|
||||
return false;
|
||||
// Order matters for shared roots: longer prefix first
|
||||
// (`partypet`/`raidpet` before `party`/`raid`).
|
||||
if (MatchBase(s, "partypet", true)) return true;
|
||||
if (MatchBase(s, "raidpet", true)) return true;
|
||||
if (MatchBase(s, "party", true)) return true;
|
||||
if (MatchBase(s, "raid", true)) return true;
|
||||
if (MatchBase(s, "nameplate", true)) return true;
|
||||
if (MatchBase(s, "mouseover", false)) return true;
|
||||
if (MatchBase(s, "player", false)) return true;
|
||||
if (MatchBase(s, "target", false)) return true;
|
||||
if (MatchBase(s, "focus", false)) return true;
|
||||
if (MatchBase(s, "npc", false)) return true;
|
||||
if (MatchBase(s, "pet", false)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t ResolveUnitGuid(void *L, int idx) {
|
||||
if (!Game::Lua::IsString(L, idx))
|
||||
return 0;
|
||||
const char *token = Game::Lua::ToString(L, idx);
|
||||
if (!LooksLikeToken(token))
|
||||
return 0;
|
||||
auto fn = reinterpret_cast<TokenToGUID_t>(Offsets::FUN_TOKEN_TO_GUID);
|
||||
return fn(token);
|
||||
}
|
||||
|
||||
int __fastcall Script_C_Item_UseItemByName(void *L) {
|
||||
const auto arg = Item::Arg::Resolve(L, 1);
|
||||
if (arg.itemID <= 0 && arg.name == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint8_t *cgItem = nullptr;
|
||||
if (!FindItemInBags(L, arg, &cgItem)) {
|
||||
Item::Location::ByGUIDResult found;
|
||||
if (!Item::Location::FindByArgInBags(L, arg, &found)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint64_t zeroTarget = 0;
|
||||
const uint64_t targetGuid = ResolveUnitGuid(L, 2);
|
||||
auto useItem = reinterpret_cast<UseItem_t>(Offsets::FUN_ITEM_USE);
|
||||
useItem(cgItem, &zeroTarget, 0);
|
||||
useItem(found.item, &targetGuid, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Item.UseAtCursor(itemInfo)` — use the named item at the player's
|
||||
// current cursor world position. For items whose on-use spell is
|
||||
// ground-target (Iron Grenade, demolition charges, etc.), bypasses
|
||||
// the manual click on the AoE reticle that the engine would
|
||||
// otherwise require.
|
||||
//
|
||||
// Mirrors `C_Item.UseItemByName`'s bag-walk + `FUN_ITEM_USE` dispatch
|
||||
// — the engine's item-use path eventually hands the on-use spell to
|
||||
// the same cast dispatcher that ground-target spells go through, so
|
||||
// placement-mode activation works identically. After firing the
|
||||
// item, `Spell::AtCursor::Resolve()` commits the placement at cursor.
|
||||
//
|
||||
// Returns `true` when the cursor-placement leg landed; `false` for
|
||||
// items that aren't ground-target (the item still fires, just with
|
||||
// no implicit target), unparseable input, item-not-in-bags, etc.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Location.h"
|
||||
#include "spell/AtCursor.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Item::UseAtCursor {
|
||||
|
||||
namespace {
|
||||
|
||||
using UseItem_t = unsigned(__thiscall *)(const void *item, const uint64_t *targetGuid,
|
||||
int flag);
|
||||
|
||||
int __fastcall Script_C_Item_UseAtCursor(void *L) {
|
||||
const auto arg = Item::Arg::Resolve(L, 1);
|
||||
if (arg.itemID <= 0 && arg.name == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Item::Location::ByGUIDResult found;
|
||||
if (!Item::Location::FindByArgInBags(L, arg, &found)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint64_t zeroTarget = 0;
|
||||
auto useItem = reinterpret_cast<UseItem_t>(Offsets::FUN_ITEM_USE);
|
||||
useItem(found.item, &zeroTarget, 0);
|
||||
|
||||
const bool placed = Spell::AtCursor::Resolve();
|
||||
Game::Lua::PushBool(L, placed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Item", "UseAtCursor",
|
||||
&Script_C_Item_UseAtCursor);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Item::UseAtCursor
|
||||
+4
-7
@@ -31,12 +31,10 @@ static const Event::Custom::AutoReserve _reserveQuestDataLoadResult{kQuestDataLo
|
||||
// `ret 8` cleans both args. We encode the questID into `userData` at
|
||||
// request time so the callback knows which quest completed.
|
||||
static void __stdcall QuestLoadCallback(void *userData, int success) {
|
||||
const int slot = Event::Custom::Lookup(kQuestDataLoadResult);
|
||||
if (slot < 0)
|
||||
return;
|
||||
const auto questID =
|
||||
static_cast<int>(reinterpret_cast<uintptr_t>(userData));
|
||||
Event::Custom::Fire(slot, "%d%d", questID, static_cast<int>(success != 0));
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kQuestDataLoadResult),
|
||||
questID, success != 0);
|
||||
}
|
||||
|
||||
static void RequestAndMaybeNotify(uint32_t questID) {
|
||||
@@ -47,9 +45,8 @@ static void RequestAndMaybeNotify(uint32_t questID) {
|
||||
// invoking our callback. Fire the event ourselves so addons get the
|
||||
// notification regardless of cache state, matching modern semantics.
|
||||
if (wasCached) {
|
||||
const int slot = Event::Custom::Lookup(kQuestDataLoadResult);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%d%d", static_cast<int>(questID), 1);
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kQuestDataLoadResult),
|
||||
static_cast<int>(questID), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Suppress vanilla 1.12.1's "Your game interface files are corrupt"
|
||||
// fatal error so the engine accepts the in-memory file modifications
|
||||
// ClassicAPI makes via the file-read hook:
|
||||
//
|
||||
// - `src/bindings/Inject.cpp` splices into FrameXML's `Bindings.xml`
|
||||
// - `src/addons/Embedded.cpp` surfaces the embedded `!!!ClassicAPI`
|
||||
// addon
|
||||
//
|
||||
// Both diverge from what's on disk, so the cumulative hashes the
|
||||
// engine computes against `Interface\FrameXML.sig` (global) and each
|
||||
// addon entry's stored signature (per-addon) no longer match. The
|
||||
// engine calls `FUN_00401560(10)` and tears down the process.
|
||||
//
|
||||
// We hook `FUN_00401560` (the fatal-error dispatcher) and drop calls
|
||||
// with code `10`. Other codes pass through to the original handler
|
||||
// unmolested — we don't want to mask legitimate fatal conditions
|
||||
// (login failures, connection drops, etc., which use other codes).
|
||||
//
|
||||
// Modded clients (Octo + TLS-bootloader, SuperWoW-style) usually
|
||||
// neutralize the integrity checks at startup; the engine never
|
||||
// reaches `FUN_00401560(10)` there and our hook is a harmless no-op.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Security::FrameXMLBypass {
|
||||
|
||||
namespace {
|
||||
|
||||
using FatalError_t = void(__fastcall *)(unsigned code);
|
||||
FatalError_t FatalError_o = nullptr;
|
||||
|
||||
void __fastcall FatalError_h(unsigned code) {
|
||||
if (code == 10)
|
||||
return;
|
||||
FatalError_o(code);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookreg{
|
||||
Offsets::FUN_FATAL_ERROR,
|
||||
reinterpret_cast<void *>(&FatalError_h),
|
||||
reinterpret_cast<void **>(&FatalError_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Security::FrameXMLBypass
|
||||
@@ -0,0 +1,199 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Spell.CastAtCursor(spellID)` — cast a ground-target spell at
|
||||
// the player's current cursor world position, bypassing the manual
|
||||
// click on the AoE reticle.
|
||||
//
|
||||
// Implementation chains the engine's existing primitives:
|
||||
//
|
||||
// 1. Initiate the cast the same way `Script_CastSpellByName` does:
|
||||
// resolve `spellID` (via our `Spell::CastByID` numeric-name hook)
|
||||
// to a spellbook slot through `FUN_RESOLVE_SPELL_NAME_TO_SLOT`,
|
||||
// then dispatch through `FUN_SPELL_CAST_DISPATCH`. For a ground-
|
||||
// target spell, the engine enters placement mode at this point
|
||||
// (sets `VAR_SPELL_PLACEMENT_STATE`); for a normal cast, it
|
||||
// fires immediately.
|
||||
//
|
||||
// 2. `Spell::AtCursor::Resolve()` then refreshes the cursor's world
|
||||
// raycast via `FUN_REFRESH_CURSOR_RAYCAST` and commits the
|
||||
// placement at the resulting world coords via
|
||||
// `FUN_COMMIT_PLACEMENT_COORDS`. Self-cancelling when the spell
|
||||
// isn't ground-target (unit-target placement → cancel) or when
|
||||
// the cursor isn't on terrain (over UI / off-screen → cancel).
|
||||
//
|
||||
// Returns `true` from `Resolve()` when we successfully placed at
|
||||
// cursor; `false` otherwise (no placement was needed, or cursor was
|
||||
// invalid). The cast itself proceeds either way — `Resolve()`'s
|
||||
// return tells the caller whether the cursor-placement leg landed.
|
||||
|
||||
#include "AtCursor.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "spell/MacroPrimarySpell.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace Spell::AtCursor {
|
||||
|
||||
namespace {
|
||||
|
||||
using RefreshCursorRaycast_t = void(__fastcall *)(void *clickInfo);
|
||||
using CommitPlacementCoords_t = unsigned(__fastcall *)(const float *xyz);
|
||||
using StopPlacement_t = void(__fastcall *)();
|
||||
|
||||
bool InWorld() {
|
||||
// Player resolves only when in-world. Same gate other modules
|
||||
// (e.g. `Item::Location::ResolveBag`) use to avoid touching
|
||||
// engine state during loading screens or glue.
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *);
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return fn("player") != nullptr;
|
||||
}
|
||||
|
||||
void CancelPlacement() {
|
||||
auto fn = reinterpret_cast<StopPlacement_t>(Offsets::FUN_STOP_PLACEMENT);
|
||||
fn();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Resolve() {
|
||||
const uint32_t state = *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_SPELL_PLACEMENT_STATE);
|
||||
if (state == 0)
|
||||
return false; // no placement active — nothing to commit
|
||||
|
||||
if ((state & Offsets::SPELL_PLACEMENT_GROUND_MASK) == 0) {
|
||||
// Spell wants a unit / item / corpse / etc. — not ground.
|
||||
// Cancel so the cursor recovers; caller learns via the false
|
||||
// return that no cursor-placement happened.
|
||||
CancelPlacement();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InWorld())
|
||||
return false;
|
||||
|
||||
void *clickInfo = *reinterpret_cast<void *const *>(
|
||||
Offsets::VAR_WORLDFRAME_CLICK_INFO);
|
||||
if (clickInfo == nullptr)
|
||||
return false;
|
||||
|
||||
// Re-raycast at the current cursor screen position. Writes the
|
||||
// new click type to `[clickInfo + OFF_CLICK_INFO_TYPE]` and the
|
||||
// hit world coords to `[clickInfo + OFF_CLICK_INFO_COORDS]`.
|
||||
auto refresh = reinterpret_cast<RefreshCursorRaycast_t>(
|
||||
Offsets::FUN_REFRESH_CURSOR_RAYCAST);
|
||||
refresh(clickInfo);
|
||||
|
||||
const uint32_t hitType = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<const uint8_t *>(clickInfo) + Offsets::OFF_CLICK_INFO_TYPE);
|
||||
if (hitType != 1) {
|
||||
// Cursor wasn't on terrain (over UI, off-screen, on a unit
|
||||
// we didn't ask for). Modern `[@cursor]` would error; we
|
||||
// chose silent cancel-and-return-false.
|
||||
CancelPlacement();
|
||||
return false;
|
||||
}
|
||||
|
||||
const float *coords = reinterpret_cast<const float *>(
|
||||
static_cast<const uint8_t *>(clickInfo) + Offsets::OFF_CLICK_INFO_COORDS);
|
||||
|
||||
auto commit = reinterpret_cast<CommitPlacementCoords_t>(
|
||||
Offsets::FUN_COMMIT_PLACEMENT_COORDS);
|
||||
commit(coords);
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using NameToSlot_t = int(__fastcall *)(const char *name, void *out);
|
||||
using CastDispatch_t = void(__fastcall *)(unsigned slot, int bookType,
|
||||
unsigned targetGuidLo, float targetGuidHi);
|
||||
|
||||
int __fastcall Script_C_Spell_CastAtCursor(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (spellID <= 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Hand the spellID off as a numeric string — our `Spell::CastByID`
|
||||
// hook on `FUN_RESOLVE_SPELL_NAME_TO_SLOT` translates pure-numeric
|
||||
// input into the locale-resolved spell name internally, so the
|
||||
// engine treats it just like `CastSpellByName("Blizzard")`.
|
||||
char numBuf[16];
|
||||
std::snprintf(numBuf, sizeof(numBuf), "%d", spellID);
|
||||
|
||||
auto nameToSlot = reinterpret_cast<NameToSlot_t>(
|
||||
Offsets::FUN_RESOLVE_SPELL_NAME_TO_SLOT);
|
||||
int bookType = 0;
|
||||
const int slot = nameToSlot(numBuf, &bookType);
|
||||
if (slot < 0) {
|
||||
// Spell not in the player's spellbook — can't cast it from
|
||||
// here either way. Match `CastSpellByName`'s silent failure.
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto dispatch = reinterpret_cast<CastDispatch_t>(
|
||||
Offsets::FUN_SPELL_CAST_DISPATCH);
|
||||
dispatch(static_cast<unsigned>(slot), bookType,
|
||||
0, 0.0f); // no implicit target unit; engine handles placement
|
||||
|
||||
const bool placed = Resolve();
|
||||
Game::Lua::PushBool(L, placed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "CastAtCursor",
|
||||
&Script_C_Spell_CastAtCursor);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// Macro-parser pattern: tag macros that call
|
||||
// `C_Spell.CastAtCursor(<spellID>)` with the spellID, so action-bar
|
||||
// UIs highlight the slot the same way they do for `/cast SpellName`.
|
||||
// The Lua call only accepts a numeric spellID, so the extractor just
|
||||
// reads a numeric literal — no name resolution needed.
|
||||
int ExtractCastAtCursorArg(const char *p, const char *end) {
|
||||
while (p < end && (*p == ' ' || *p == '\t'))
|
||||
++p;
|
||||
int value = 0;
|
||||
const char *digitStart = p;
|
||||
while (p < end && *p >= '0' && *p <= '9') {
|
||||
if (value > 100000000) // sanity cap before overflow
|
||||
return 0;
|
||||
value = value * 10 + (*p - '0');
|
||||
++p;
|
||||
}
|
||||
return (p > digitStart && value > 0) ? value : 0;
|
||||
}
|
||||
|
||||
const Spell::MacroPrimarySpell::PatternAutoRegister _patreg{
|
||||
"C_Spell.CastAtCursor(", &ExtractCastAtCursorArg};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Spell::AtCursor
|
||||
@@ -0,0 +1,33 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Spell::AtCursor {
|
||||
|
||||
// Resolves the engine's in-flight ground-target placement at the
|
||||
// player's current cursor world position. Intended to be called
|
||||
// immediately after initiating a ground-target cast (spell or item)
|
||||
// to skip the manual click. Decision tree:
|
||||
//
|
||||
// 1. No placement active → return false (nothing to do)
|
||||
// 2. Placement is unit-target, not ground → cancel placement, return false
|
||||
// 3. Cursor not pointed at terrain → cancel placement, return false
|
||||
// 4. Cursor on terrain → commit at those world coords, return true
|
||||
//
|
||||
// Shared by `C_Spell.CastAtCursor` and `C_Item.UseAtCursor`. Safe to
|
||||
// call from any in-world context — internally gates on a non-NULL
|
||||
// click-info struct pointer.
|
||||
bool Resolve();
|
||||
|
||||
} // namespace Spell::AtCursor
|
||||
+27
-130
@@ -42,6 +42,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "spell/Arg.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "spell/MacroPrimarySpell.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -208,141 +209,37 @@ void RegisterLuaFunctions() {
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Macro parser hook
|
||||
// Macro parser pattern
|
||||
//
|
||||
// Without this hook, a macro whose only cast statement is
|
||||
// `CastSpellNoToggle("Shoot")` doesn't get tagged in the spell-state
|
||||
// cache with the resolved spellID, because the engine's parser
|
||||
// (`FUN_004EFE00`) only knows to look for `/cast` slash-aliases and
|
||||
// `CastSpellByName("...")`. `IsAutoRepeatAction(macroSlot)` then has
|
||||
// nothing to match against the active auto-repeat spell, so the slot
|
||||
// fails to highlight in action-bar UIs (pfUI etc.).
|
||||
//
|
||||
// We hook the parser, let the original run, and if it didn't recognize
|
||||
// any pattern (cache field stays 0), scan the body ourselves for our
|
||||
// function name and use the engine's own name→spellID resolver to
|
||||
// fill in the field.
|
||||
// Tags macros containing `CastSpellNoToggle("Name")` with the
|
||||
// resolved spellID, so `IsAutoRepeatAction(macroSlot)` reports
|
||||
// correctly and action-bar UIs (pfUI etc.) highlight the slot for
|
||||
// auto-repeat spells. `Spell::MacroPrimarySpell` owns the parser
|
||||
// hook and the line walk; we just declare our `(prefix, extractor)`.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
using MacroParse_t = void(__fastcall *)(int macroEntry);
|
||||
MacroParse_t MacroParse_o = nullptr;
|
||||
|
||||
|
||||
// Scan a macro body for `CastSpellNoToggle("<name>")` and return the
|
||||
// resolved spellID, or 0 if no recognized invocation is present.
|
||||
// Mirrors the engine parser's "first hit wins, line-at-a-time" walk
|
||||
// order so behavior is consistent with how `/cast` and
|
||||
// `CastSpellByName` are matched.
|
||||
int ScanMacroBodyForOurPattern(const char *body) {
|
||||
static constexpr char kPattern[] = "CastSpellNoToggle(";
|
||||
static constexpr size_t kPatternLen = sizeof(kPattern) - 1;
|
||||
|
||||
const char *line = body;
|
||||
while (*line != '\0') {
|
||||
const char *eol = line;
|
||||
while (*eol != '\0' && *eol != '\n')
|
||||
++eol;
|
||||
|
||||
const char *p = line;
|
||||
const char *end = eol;
|
||||
while (p + kPatternLen <= end) {
|
||||
if (std::memcmp(p, kPattern, kPatternLen) == 0) {
|
||||
p += kPatternLen;
|
||||
while (p < end && (*p == ' ' || *p == '\t'))
|
||||
++p;
|
||||
if (p < end && *p == '"') {
|
||||
++p;
|
||||
const char *q = p;
|
||||
while (q < end && *q != '"')
|
||||
++q;
|
||||
if (q < end) {
|
||||
const size_t n = static_cast<size_t>(q - p);
|
||||
if (n > 0 && n < 128) {
|
||||
char name[128];
|
||||
std::memcpy(name, p, n);
|
||||
name[n] = '\0';
|
||||
const int spellID = Spell::Arg::NameToSpellID(name);
|
||||
if (spellID > 0)
|
||||
return spellID;
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // matched our prefix on this line; don't double-match
|
||||
}
|
||||
++p;
|
||||
}
|
||||
|
||||
line = (*eol == '\n') ? eol + 1 : eol;
|
||||
}
|
||||
return 0;
|
||||
int ExtractCastSpellNoToggleArg(const char *p, const char *end) {
|
||||
while (p < end && (*p == ' ' || *p == '\t'))
|
||||
++p;
|
||||
if (p >= end || *p != '"')
|
||||
return 0;
|
||||
++p;
|
||||
const char *q = p;
|
||||
while (q < end && *q != '"')
|
||||
++q;
|
||||
if (q >= end)
|
||||
return 0;
|
||||
const size_t n = static_cast<size_t>(q - p);
|
||||
if (n == 0 || n >= 128)
|
||||
return 0;
|
||||
char name[128];
|
||||
std::memcpy(name, p, n);
|
||||
name[n] = '\0';
|
||||
return Spell::Arg::NameToSpellID(name);
|
||||
}
|
||||
|
||||
// The vanilla parser's per-line stack buffer is 256 bytes. When a
|
||||
// macro line exceeds that, the parser's local at `[EBP-4]` (the saved
|
||||
// entry pointer) and the saved return address get clobbered with body
|
||||
// bytes — observed in a crash with `entry@[EBP-4]` reading as ASCII
|
||||
// `"onTi"` (fragment of `"expirationTime"` from a long `/run` line).
|
||||
// The bound-check on the line-read function reads correctly in
|
||||
// Ghidra, so the overflow path is somewhere we couldn't isolate from
|
||||
// static analysis — we settle for a conservative pre-check that
|
||||
// skips the original entirely when any line is long enough to risk
|
||||
// the bug.
|
||||
//
|
||||
// 240 (not 256) leaves a safety margin: the parser writes a NUL
|
||||
// terminator and may walk one or two bytes past in its loop logic,
|
||||
// so we keep clear of the boundary. Macros that hit this branch
|
||||
// can't have a `/cast` line anyway (`/cast SpellName(Rank N)` is
|
||||
// well under 100 bytes), so losing the engine's `/cast` /
|
||||
// `CastSpellByName` tagging is no loss. We still run our own
|
||||
// `CastSpellNoToggle` pattern scan since it bound-checks its own
|
||||
// extraction.
|
||||
bool BodyHasLongLine(const char *body) {
|
||||
if (body == nullptr)
|
||||
return false;
|
||||
constexpr int kMaxSafeLineBytes = 240;
|
||||
int lineLen = 0;
|
||||
for (; *body != '\0'; ++body) {
|
||||
if (*body == '\n') {
|
||||
lineLen = 0;
|
||||
} else if (++lineLen > kMaxSafeLineBytes) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void __fastcall MacroParse_h(int macroEntry) {
|
||||
const char *body = reinterpret_cast<const char *>(
|
||||
macroEntry + Offsets::OFF_MACRO_BODY);
|
||||
int *cacheField = reinterpret_cast<int *>(
|
||||
macroEntry + Offsets::OFF_MACRO_PRIMARY_SPELL);
|
||||
|
||||
if (BodyHasLongLine(body)) {
|
||||
// Skip the engine parser entirely — it overflows on lines
|
||||
// longer than its 256-byte buffer. Mark the cache as "no
|
||||
// recognized cast" (0); our own scan below may still tag it
|
||||
// if it's a `CastSpellNoToggle("...")` macro.
|
||||
*cacheField = 0;
|
||||
const int spellID = ScanMacroBodyForOurPattern(body);
|
||||
if (spellID > 0)
|
||||
*cacheField = spellID;
|
||||
return;
|
||||
}
|
||||
|
||||
MacroParse_o(macroEntry);
|
||||
|
||||
if (*cacheField != 0)
|
||||
return;
|
||||
|
||||
const int spellID = ScanMacroBodyForOurPattern(body);
|
||||
if (spellID > 0)
|
||||
*cacheField = spellID;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookreg{
|
||||
Offsets::FUN_MACRO_PARSE_PRIMARY_SPELL,
|
||||
reinterpret_cast<void *>(&MacroParse_h),
|
||||
reinterpret_cast<void **>(&MacroParse_o)};
|
||||
const Spell::MacroPrimarySpell::PatternAutoRegister _patreg{
|
||||
"CastSpellNoToggle(", &ExtractCastSpellNoToggleArg};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Owns the hook on `FUN_MACRO_PARSE_PRIMARY_SPELL` and dispatches to
|
||||
// feature-module-registered `(prefix, extractor)` pairs. The engine's
|
||||
// parser only recognizes `/cast SpellName` and `CastSpellByName(...)`
|
||||
// — addons that define new cast wrappers (`CastSpellNoToggle`,
|
||||
// `C_Spell.CastAtCursor`, etc.) need their macro slots tagged with
|
||||
// the resolved spellID too, otherwise action-bar UIs lose
|
||||
// auto-repeat highlight and cooldown overlay.
|
||||
//
|
||||
// Each feature module owns an extractor that knows how to read the
|
||||
// arg shape for its specific function (quoted string, numeric
|
||||
// literal, etc.). Patterns auto-register at static-init via
|
||||
// `PatternAutoRegister`. The hook walks each macro line ONCE,
|
||||
// checking every registered prefix at every position; on a prefix
|
||||
// match it calls the extractor to resolve a spellID.
|
||||
//
|
||||
// Long-line overflow guard: the vanilla parser has a 256-byte
|
||||
// per-line stack buffer that overflows on `/run` lines longer than
|
||||
// that. We pre-check and skip the original entirely when any line
|
||||
// exceeds the safe threshold — the original's tagging is lost in
|
||||
// that case (no `/cast` on a 240+ byte line anyway) but our
|
||||
// patterns still run since they bound-check their own extraction.
|
||||
|
||||
#include "MacroPrimarySpell.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Spell::MacroPrimarySpell {
|
||||
|
||||
namespace {
|
||||
|
||||
PatternAutoRegister *g_patternHead = nullptr;
|
||||
|
||||
using MacroParse_t = void(__fastcall *)(int macroEntry);
|
||||
MacroParse_t MacroParse_o = nullptr;
|
||||
|
||||
// Vanilla's parser uses a 256-byte stack buffer per line; bytes past
|
||||
// the buffer clobber the saved entry pointer at `[EBP-4]` and the
|
||||
// return address. 240 leaves a safety margin.
|
||||
constexpr int kMaxSafeLineBytes = 240;
|
||||
|
||||
bool BodyHasLongLine(const char *body) {
|
||||
if (body == nullptr)
|
||||
return false;
|
||||
int lineLen = 0;
|
||||
for (; *body != '\0'; ++body) {
|
||||
if (*body == '\n') {
|
||||
lineLen = 0;
|
||||
} else if (++lineLen > kMaxSafeLineBytes) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Walks each line once. At every byte position, checks every
|
||||
// registered prefix; on first match, invokes the extractor. First
|
||||
// extractor that returns a positive spellID wins for the whole macro
|
||||
// — matches the engine parser's "first hit, line-at-a-time" walk.
|
||||
int ScanBody(const char *body) {
|
||||
if (body == nullptr)
|
||||
return 0;
|
||||
const char *line = body;
|
||||
while (*line != '\0') {
|
||||
const char *eol = line;
|
||||
while (*eol != '\0' && *eol != '\n')
|
||||
++eol;
|
||||
|
||||
for (const char *p = line; p < eol; ++p) {
|
||||
for (auto *node = g_patternHead; node != nullptr; node = node->next) {
|
||||
if (p + node->prefixLen > eol)
|
||||
continue;
|
||||
if (std::memcmp(p, node->prefix, node->prefixLen) != 0)
|
||||
continue;
|
||||
const int spellID = node->extract(p + node->prefixLen, eol);
|
||||
if (spellID > 0)
|
||||
return spellID;
|
||||
// Prefix matched but extraction failed (malformed
|
||||
// args). Don't keep checking other prefixes at this
|
||||
// position — they couldn't possibly also match here.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
line = (*eol == '\n') ? eol + 1 : eol;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void __fastcall MacroParse_h(int macroEntry) {
|
||||
const char *body = reinterpret_cast<const char *>(
|
||||
macroEntry + Offsets::OFF_MACRO_BODY);
|
||||
int *cacheField = reinterpret_cast<int *>(
|
||||
macroEntry + Offsets::OFF_MACRO_PRIMARY_SPELL);
|
||||
|
||||
if (BodyHasLongLine(body)) {
|
||||
*cacheField = 0;
|
||||
const int spellID = ScanBody(body);
|
||||
if (spellID > 0)
|
||||
*cacheField = spellID;
|
||||
return;
|
||||
}
|
||||
|
||||
MacroParse_o(macroEntry);
|
||||
|
||||
if (*cacheField != 0)
|
||||
return;
|
||||
|
||||
const int spellID = ScanBody(body);
|
||||
if (spellID > 0)
|
||||
*cacheField = spellID;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookreg{
|
||||
Offsets::FUN_MACRO_PARSE_PRIMARY_SPELL,
|
||||
reinterpret_cast<void *>(&MacroParse_h),
|
||||
reinterpret_cast<void **>(&MacroParse_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
PatternAutoRegister::PatternAutoRegister(const char *p, ArgExtractor e)
|
||||
: prefix(p),
|
||||
prefixLen(static_cast<unsigned>(std::strlen(p))),
|
||||
extract(e),
|
||||
next(g_patternHead) {
|
||||
g_patternHead = this;
|
||||
}
|
||||
|
||||
} // namespace Spell::MacroPrimarySpell
|
||||
@@ -0,0 +1,44 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Spell::MacroPrimarySpell {
|
||||
|
||||
// Parses the argument(s) immediately after a known function-call
|
||||
// prefix. `p` points to the first byte past the closing `(` of the
|
||||
// prefix (so for `CastSpellNoToggle("Shoot")`, `p` would be at the
|
||||
// `"`). `end` is end-of-line. Returns the resolved spellID, or 0 if
|
||||
// extraction failed (malformed args, unknown spell, etc.).
|
||||
using ArgExtractor = int(*)(const char *p, const char *end);
|
||||
|
||||
// File-scope static auto-registers a `(prefix, extractor)` pair. Each
|
||||
// feature module that wants its Lua wrapper to tag macros declares:
|
||||
//
|
||||
// static const Spell::MacroPrimarySpell::PatternAutoRegister _r{
|
||||
// "CastSpellNoToggle(", &MyExtractor};
|
||||
//
|
||||
// The macro-parser hook walks every line once, checks each registered
|
||||
// prefix at each position, and on match invokes the extractor to
|
||||
// resolve a spellID. The resolved ID is written to the macro's
|
||||
// primary-spell cache field — drives auto-repeat highlight, cooldown
|
||||
// overlay, etc. in action-bar UIs (pfUI).
|
||||
struct PatternAutoRegister {
|
||||
PatternAutoRegister(const char *prefix, ArgExtractor extract);
|
||||
const char *prefix;
|
||||
unsigned prefixLen;
|
||||
ArgExtractor extract;
|
||||
PatternAutoRegister *next;
|
||||
};
|
||||
|
||||
} // namespace Spell::MacroPrimarySpell
|
||||
Reference in New Issue
Block a user