mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 07:00:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6245d72694 | ||
|
|
bd14228ad7 | ||
|
|
5b15172894 | ||
|
|
b5470e8d3d | ||
|
|
c601967fdb | ||
|
|
5c063ade73 | ||
|
|
2bd33288c9 | ||
|
|
bf7e476c10 | ||
|
|
8f3ed96d51 | ||
|
|
e1ffaab6b4 |
@@ -15,8 +15,6 @@
|
||||
-- - 1.12 has no concept of an item GUID and no public item-lock API,
|
||||
-- so those return nil / no-op.
|
||||
|
||||
C_Item.GetItemInfo = GetItemInfo
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Item = {}
|
||||
|
||||
@@ -41,11 +41,11 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.PickupItem`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.PickupItem`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
|
||||
| [Lua](docs/API.md#lua) | `coroutine.create`, `coroutine.resume`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `math.fmod`, `select`, `string.gmatch`, `string.match`, `table.wipe` |
|
||||
| [Lua](docs/API.md#lua) | `coroutine.create`, `coroutine.resume`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `math.fmod`, `math.huge`, `math.modf`, `select`, `string.gmatch`, `string.match`, `string.reverse`, `strjoin`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.wipe` |
|
||||
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapOverlays`, `C_Map.GetMapWorldSize` |
|
||||
@@ -55,7 +55,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
|
||||
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GetClass`, `C_PlayerInfo.GetName`, `C_PlayerInfo.GetRace`, `C_PlayerInfo.GetSex`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `C_PlayerInfo.IsConnected` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Screen](docs/API.md#screen) | `GetPhysicalScreenSize` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
|
||||
+235
-3
@@ -95,6 +95,7 @@ build instructions.
|
||||
|
||||
- [Events](#events)
|
||||
- [`C_EventUtils.IsEventValid(eventName)`](#c_eventutilsiseventvalideventname)
|
||||
- [`PLAYER_ENTERING_WORLD` payload (`isInitialLogin`, `isReloadingUi`)](#player_entering_world-payload-isinitiallogin-isreloadingui)
|
||||
- [`BAG_UPDATE_DELAYED` event](#bag_update_delayed-event)
|
||||
- [`PLAYER_EQUIPMENT_CHANGED` event](#player_equipment_changed-event)
|
||||
- [`UPDATE_INVENTORY_DURABILITY` event](#update_inventory_durability-event)
|
||||
@@ -214,6 +215,7 @@ build instructions.
|
||||
- [`C_Item.GetItemFamily(item)`](#c_itemgetitemfamilyitem)
|
||||
- [`C_Item.GetItemGUID(itemLocation)`](#c_itemgetitemguiditemlocation)
|
||||
- [`C_Item.GetItemID(itemLocation)`](#c_itemgetitemiditemlocation)
|
||||
- [`C_Item.GetItemInfo(itemInfo)`](#c_itemgetiteminfoiteminfo)
|
||||
- [`C_Item.GetItemInfoInstant(item)`](#c_itemgetiteminfoinstantitem)
|
||||
- [`C_Item.GetItemInventorySlotInfo(inventorySlot)`](#c_itemgetiteminventoryslotinfoinventoryslot)
|
||||
- [`C_Item.GetItemInventorySlotKey(inventorySlot)`](#c_itemgetiteminventoryslotkeyinventoryslot)
|
||||
@@ -281,7 +283,14 @@ build instructions.
|
||||
- [`select(index, ...)`](#selectindex-)
|
||||
- [`table.wipe(t)`](#tablewipet)
|
||||
- [`string.match` / `string.gmatch`](#stringmatch--stringgmatch)
|
||||
- [`strsplit(sep, str [, pieces])`](#strsplitsep-str--pieces)
|
||||
- [`strjoin(delimiter, ...)`](#strjoindelimiter-)
|
||||
- [`strtrim(str [, chars])`](#strtrimstr--chars)
|
||||
- [`strreplace(str, find, replace)`](#strreplacestr-find-replace)
|
||||
- [`string.reverse(s)` / `strrev(s)`](#stringreverses--strrevs)
|
||||
- [`math.fmod(x, y)`](#mathfmodx-y)
|
||||
- [`math.modf(x)`](#mathmodfx)
|
||||
- [`math.huge`](#mathhuge)
|
||||
- [`coroutine.create(fn)`](#coroutinecreatefn)
|
||||
- [`coroutine.resume(co, ...)`](#coroutineresumeco-)
|
||||
- [`coroutine.yield(...)`](#coroutineyield)
|
||||
@@ -348,6 +357,8 @@ build instructions.
|
||||
- [`C_PlayerInfo.GetName / GetClass / GetRace / GetSex / IsConnected(playerLocation)`](#c_playerinfogetname--getclass--getrace--getsex--isconnectedplayerlocation)
|
||||
- [Quest](#quest)
|
||||
- [`C_QuestLog.GetQuestIDForLogIndex(index)`](#c_questlogGetQuestIDForLogIndexindex)
|
||||
- [`C_QuestLog.GetLogIndexForQuestID(questID)`](#c_questloggetlogindexforquestidquestid)
|
||||
- [`C_QuestLog.GetHeaderIndexForQuest(questID)`](#c_questloggetheaderindexforquestquestid)
|
||||
- [`C_QuestLog.RequestLoadQuestByID(questID)`](#c_questlogrequestloadquestbyidquestid)
|
||||
- [`C_QuestLog.IsOnQuest(questID)`](#c_questlogisonquestquestid)
|
||||
- [`C_QuestLog.IsUnitOnQuest(unit, questID)`](#c_questlogisunitonquestunit-questid)
|
||||
@@ -2210,6 +2221,41 @@ SuperWoWhook patches the binary's event names with its own. This matches
|
||||
what `frame:RegisterEvent` will actually accept, which is what addon code
|
||||
needs to know.
|
||||
|
||||
### `PLAYER_ENTERING_WORLD` payload (`isInitialLogin`, `isReloadingUi`)
|
||||
|
||||
Backports the 8.0.1 payload onto vanilla's (natively arg-less)
|
||||
`PLAYER_ENTERING_WORLD`:
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
f:SetScript("OnEvent", function()
|
||||
local isInitialLogin, isReloadingUi = arg1, arg2
|
||||
if isInitialLogin then -- fresh character login (from char-select)
|
||||
elseif isReloadingUi then -- /reload
|
||||
else -- zone / instance transition
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
- `isInitialLogin` — `true` on a fresh login (including logging out to
|
||||
char-select and back in), `nil` otherwise.
|
||||
- `isReloadingUi` — `true` when the fire is caused by a `/reload`, `nil`
|
||||
otherwise.
|
||||
- Both `nil` on an instance/zone transition — matching retail.
|
||||
|
||||
Delivered as real event args (`arg1`/`arg2`), so retail-style handlers work
|
||||
unchanged. Values are `true`→`1` / false→`nil` (the engine dispatcher has no
|
||||
boolean), so `if isInitialLogin then …` reads naturally. Existing handlers
|
||||
that ignore the args are unaffected.
|
||||
|
||||
PEW is a no-arg event, so the engine broadcasts it through the
|
||||
`FUN_FIRE_EVENT_NO_ARGS` dispatcher; we intercept that (via the shared
|
||||
`Event::SignalHook`) and re-broadcast through the variadic dispatcher with
|
||||
the payload. The flags are derived from whether the in-game Lua state was
|
||||
just (re)built (login/`reload`) and whether we just came through the
|
||||
glue/login screen — no `PLAYER_ENTERING_WORLD`-specific engine hook.
|
||||
|
||||
### `BAG_UPDATE_DELAYED` event
|
||||
|
||||
Fires (with no payload) once per frame in which any `BAG_UPDATE`
|
||||
@@ -4984,6 +5030,50 @@ if id then
|
||||
end
|
||||
```
|
||||
|
||||
### `C_Item.GetItemInfo(itemInfo)`
|
||||
|
||||
The full modern `GetItemInfo` tuple, sourced from the client-side item cache
|
||||
plus the class/subclass/inventory-type DBC name lookups. The namespaced
|
||||
counterpart to the stock global `GetItemInfo` (which returns only vanilla's
|
||||
short tuple); this returns the wide modern set backported addons expect.
|
||||
|
||||
Accepts a numeric `itemID`, an `"item:NNN"` string, or a full chat link.
|
||||
|
||||
Returns 18 values:
|
||||
|
||||
| # | Return | Notes |
|
||||
|---|--------|-------|
|
||||
| 1 | `itemName` | |
|
||||
| 2 | `itemLink` | basic `item:ID` hyperlink (colored, bracketed name) |
|
||||
| 3 | `itemQuality` | 0–5 |
|
||||
| 4 | `itemLevel` | |
|
||||
| 5 | `itemMinLevel` | required level |
|
||||
| 6 | `itemType` | localized class name (e.g. "Weapon") |
|
||||
| 7 | `itemSubType` | localized subclass name (e.g. "Sword") |
|
||||
| 8 | `itemStackCount` | max stack size |
|
||||
| 9 | `itemEquipLoc` | `INVTYPE_*` token, `""` for non-equippable |
|
||||
| 10 | `itemTexture` | icon **path** (1.12 has no fileID system) |
|
||||
| 11 | `sellPrice` | vendor sell price in copper |
|
||||
| 12 | `classID` | |
|
||||
| 13 | `subclassID` | |
|
||||
| 14 | `bindType` | 0 none / 1 BoP / 2 BoE / 3 BoU / 4 quest |
|
||||
| 15 | `expansionID` | always `0` (classic) |
|
||||
| 16 | `setID` | item-set ID, `nil` if none |
|
||||
| 17 | `isCraftingReagent` | always `false` (no such flag in 1.12 data) |
|
||||
| 18 | `itemDescription` | item flavor/description text (`""` if none) |
|
||||
|
||||
**Asynchronous on a cache miss:** if the item isn't cached yet it returns
|
||||
nothing (nil) and warms the cache; the value lands on a retry after
|
||||
`GET_ITEM_INFO_RECEIVED` fires — same contract as the stock `GetItemInfo`
|
||||
and modern clients. Fields 4–18 are the ones the vanilla global never
|
||||
returned.
|
||||
|
||||
```lua
|
||||
local name, link, quality, ilvl, minLevel, itype, isub, stack, equipLoc,
|
||||
tex, sell, classID, subID, bind, expac, setID, reagent, desc
|
||||
= C_Item.GetItemInfo(6948) -- Hearthstone
|
||||
```
|
||||
|
||||
### `C_Item.GetItemInfoInstant(item)`
|
||||
|
||||
Modern-style accessor for the always-available subset of item info — the
|
||||
@@ -6792,6 +6882,83 @@ for word in string.gmatch("a,bb,ccc", "[^,]+") do print(word) end -- a / bb / cc
|
||||
> a hot VM-core function. This is the long-standing vanilla 1.12 constraint —
|
||||
> always write `string.match(s, p)`, never `s:match(p)`.
|
||||
|
||||
### `strsplit(sep, str [, pieces])`
|
||||
|
||||
The WoW global (backported from 3.3.5), splits `str` on **any** character in
|
||||
`sep` and returns the pieces as multiple values.
|
||||
|
||||
- `sep` — a set of delimiter *characters* (not a pattern); each character is
|
||||
a delimiter. `","` splits on commas; `" -"` splits on spaces and dashes.
|
||||
- `pieces` (optional) — caps the number of results. After `pieces - 1`
|
||||
splits the rest of the string (delimiters and all) is returned as the final
|
||||
piece. `0` or omitted = unlimited. `1` returns the whole string unsplit.
|
||||
|
||||
Consecutive/trailing delimiters produce empty pieces, matching retail.
|
||||
|
||||
```lua
|
||||
strsplit(",", "a,b,c") -- "a", "b", "c"
|
||||
strsplit(",", "a,b,c,d", 2) -- "a", "b,c,d" (capped at 2)
|
||||
strsplit(" -", "a b-c") -- "a", "b", "c" (space OR dash)
|
||||
strsplit(",", "a,,b") -- "a", "", "b" (empty middle piece)
|
||||
local zone, x, y = strsplit(":", "Durotar:52:38")
|
||||
```
|
||||
|
||||
Errors `strsplit(): Stack overflow` if a string splits into more pieces than
|
||||
the Lua stack can grow to hold (`lua_checkstack` guards every push) — the
|
||||
same guard and message 3.3.5 uses.
|
||||
|
||||
### `strjoin(delimiter, ...)`
|
||||
|
||||
The inverse of `strsplit`: concatenate the vararg pieces with `delimiter`
|
||||
between each.
|
||||
|
||||
```lua
|
||||
strjoin(",", "a", "b", "c") -- "a,b,c"
|
||||
strjoin("", "a", "b", "c") -- "abc"
|
||||
strjoin(",") -- "" (no pieces)
|
||||
```
|
||||
|
||||
A `nil` / non-string-coercible piece is treated as an empty string (so the
|
||||
delimiters still line up) rather than erroring.
|
||||
|
||||
### `strtrim(str [, chars])`
|
||||
|
||||
Remove any of the characters in `chars` from both ends of `str`. `chars` is a
|
||||
literal **set of characters** (not a Lua pattern); it defaults to whitespace
|
||||
(space, tab, `\n`, `\v`, `\f`, `\r`).
|
||||
|
||||
```lua
|
||||
strtrim(" hello ") -- "hello"
|
||||
strtrim("xxhelloxx", "x") -- "hello"
|
||||
strtrim("[a]", "[]") -- "a"
|
||||
```
|
||||
|
||||
### `strreplace(str, find, replace)`
|
||||
|
||||
Replace every occurrence of the literal substring `find` with `replace`,
|
||||
returning `(result, count)`. This is a **plain-text** replace — no pattern
|
||||
magic-character escaping, unlike `string.gsub`. An empty `find` returns `str`
|
||||
unchanged with count `0`.
|
||||
|
||||
```lua
|
||||
strreplace("a.b.c", ".", "-") -- "a-b-c", 2 (no pattern escaping)
|
||||
strreplace("hello world", "o", "0") -- "hell0 w0rld", 2
|
||||
```
|
||||
|
||||
> Not a stock WoW global — a ClassicAPI convenience. For pattern-based
|
||||
> replacement use `string.gsub`.
|
||||
|
||||
### `string.reverse(s)` / `strrev(s)`
|
||||
|
||||
Returns `s` with its bytes reversed. Lua 5.1 added `string.reverse`; 5.0's
|
||||
string library lacks it. Byte-wise (not UTF-8 aware), matching stock Lua.
|
||||
Registered both on the `string` table and as the `strrev` global.
|
||||
|
||||
```lua
|
||||
string.reverse("abc") -- "cba"
|
||||
strrev("hello") -- "olleh"
|
||||
```
|
||||
|
||||
### `math.fmod(x, y)`
|
||||
|
||||
The floating-point remainder of `x / y` (the quotient truncated toward zero),
|
||||
@@ -6806,6 +6973,28 @@ math.fmod(-7, 3) -- -1 (result takes the sign of the dividend)
|
||||
math.fmod(5.5, 2) -- 1.5
|
||||
```
|
||||
|
||||
### `math.modf(x)`
|
||||
|
||||
Returns `(integral, fractional)` — the integral part truncated toward zero
|
||||
and the fractional part carrying `x`'s sign. Lua 5.1 function, genuinely
|
||||
missing from 1.12's math library (not just renamed like `fmod`).
|
||||
|
||||
```lua
|
||||
math.modf(3.7) -- 3, 0.7
|
||||
math.modf(-3.7) -- -3, -0.7
|
||||
math.modf(5) -- 5, 0
|
||||
```
|
||||
|
||||
### `math.huge`
|
||||
|
||||
Positive infinity, as the numeric constant `math.huge` (Lua 5.1). Handy as a
|
||||
"larger than anything" sentinel in min/max scans.
|
||||
|
||||
```lua
|
||||
local best = math.huge
|
||||
for _, v in ipairs(costs) do best = math.min(best, v) end
|
||||
```
|
||||
|
||||
### `coroutine.*`
|
||||
|
||||
Restores Lua 5.0's stripped `coroutine.*` library. The C-level
|
||||
@@ -8368,6 +8557,37 @@ for i = 1, GetNumQuestLogEntries() do
|
||||
end
|
||||
```
|
||||
|
||||
### `C_QuestLog.GetLogIndexForQuestID(questID)`
|
||||
|
||||
The inverse of `GetQuestIDForLogIndex`: returns the 1-based quest log index
|
||||
of `questID`, or `nil` if the quest isn't in the log. The index spans the
|
||||
full entry list (headers included), so it can be passed straight to
|
||||
`GetQuestLogTitle(index)` / `GetQuestLogLeaderBoardID(...)` and the other
|
||||
index-based quest-log accessors.
|
||||
|
||||
```lua
|
||||
local index = C_QuestLog.GetLogIndexForQuestID(questID)
|
||||
if index then
|
||||
local title = GetQuestLogTitle(index)
|
||||
-- ...
|
||||
end
|
||||
```
|
||||
|
||||
### `C_QuestLog.GetHeaderIndexForQuest(questID)`
|
||||
|
||||
Returns the 1-based log index of the collapsible **header** (zone /
|
||||
`"Dungeon"` / class-sort category, …) that `questID` sits under, or `nil` if
|
||||
the quest isn't in the log. The quest log is laid out header-then-its-quests,
|
||||
so this locates the quest and walks back to the nearest preceding header row.
|
||||
Pass the result to `GetQuestLogTitle(headerIndex)` for the category name.
|
||||
|
||||
```lua
|
||||
local headerIndex = C_QuestLog.GetHeaderIndexForQuest(questID)
|
||||
if headerIndex then
|
||||
local zone = GetQuestLogTitle(headerIndex) -- e.g. "Westfall"
|
||||
end
|
||||
```
|
||||
|
||||
### `C_QuestLog.RequestLoadQuestByID(questID)`
|
||||
|
||||
Asks the engine to fetch the static data for `questID` (title, description,
|
||||
@@ -11389,13 +11609,25 @@ end
|
||||
|
||||
| Return | Meaning |
|
||||
|--------|---------|
|
||||
| `inRange` | `true` if the unit's world position is within 40 yards of the player's. `false` if out of range OR if `checkedRange` is `false`. |
|
||||
| `inRange` | `true` if the unit is within a 40-yard heal's reach of the player. `false` if out of range OR if `checkedRange` is `false`. |
|
||||
| `checkedRange` | `true` when a position-based range check was performed. `false` when the unit's position isn't available — either the token didn't resolve (empty `partyN` slot, no target, raid member in a different zone, etc.) or `unit == "player"` (see below). |
|
||||
|
||||
Reads the world position via the `CGObject::GetPosition` vtable
|
||||
virtual (slot 5, offset `+0x14`) — same path
|
||||
`CheckInteractDistance` uses. Squared-distance compare against
|
||||
`40 * 40 = 1600`.
|
||||
`CheckInteractDistance` uses.
|
||||
|
||||
**Reach-aware.** The threshold is not a flat 40 yards on the raw
|
||||
center-to-center distance — it's `40 + playerReach + targetReach`,
|
||||
mirroring the engine's own spell-range formula (`FUN_006e3480`), where
|
||||
each unit's reach is the bounding-radius float at `[m_objectFields +
|
||||
0x1F0]` (the same size factor the interact-distance and loot-range
|
||||
checks add). A 40-yard heal lands on a target whose *center* is up to
|
||||
`40 + reach` yards away, so a bare center-distance cap of 40 wrongly
|
||||
reports out-of-range for a target the client can actually heal (on a
|
||||
large target the reach gap is 2–3 yards). If a unit's descriptor isn't
|
||||
populated yet the reach term is 0 and the check degrades to a plain
|
||||
40-yard cap. Note this is the one difference from `UnitDistanceSquared`,
|
||||
which stays raw center-to-center (matching retail).
|
||||
|
||||
> **`UnitInRange("player")` returns `(false, false)`** by design,
|
||||
> matching modern WoW's behavior. The function is meant for healing-
|
||||
|
||||
+2
-1
@@ -57,7 +57,8 @@ namespace Lua {
|
||||
F(Resume, lua_resume, LUA_RESUME) \
|
||||
F(Yield, lua_yield, LUA_YIELD) \
|
||||
F(ArgError, luaL_argerror, LUAL_ARG_ERROR) \
|
||||
F(SetN, luaL_setn, LUAL_SETN)
|
||||
F(SetN, luaL_setn, LUAL_SETN) \
|
||||
F(CheckStack, lua_checkstack, LUA_CHECK_STACK)
|
||||
|
||||
#define CLASSICAPI_BIND_LUA(Name, Typedef, Offset) \
|
||||
const Typedef##_t Name = reinterpret_cast<Typedef##_t>(Offsets::Offset);
|
||||
|
||||
@@ -100,6 +100,7 @@ using lua_resume_t = int(__fastcall *)(void *L, int nargs);
|
||||
using lua_yield_t = int(__fastcall *)(void *L, int nresults);
|
||||
using luaL_argerror_t = void(__fastcall *)(void *L, int narg, const char *msg);
|
||||
using luaL_setn_t = void(__fastcall *)(void *L, int t, int n);
|
||||
using lua_checkstack_t = int(__fastcall *)(void *L, int size);
|
||||
|
||||
extern const lua_isnumber_t IsNumber;
|
||||
extern const lua_isstring_t IsString;
|
||||
@@ -142,6 +143,7 @@ extern const lua_resume_t Resume;
|
||||
extern const lua_yield_t Yield;
|
||||
extern const luaL_argerror_t ArgError;
|
||||
extern const luaL_setn_t SetN;
|
||||
extern const lua_checkstack_t CheckStack;
|
||||
|
||||
// Returns the global `lua_State *` (read on demand from the engine's global).
|
||||
// Callable outside a Lua callback, e.g. during LoadScriptFunctions setup.
|
||||
|
||||
@@ -3796,6 +3796,14 @@ enum Offsets {
|
||||
// `rawset(nil)` clear leaves it stale and subsequent `table.insert`
|
||||
// appends past the wiped slots.
|
||||
LUAL_SETN = 0x6F4EA0,
|
||||
// `lua_checkstack(L, n)` — `int __fastcall(L /*ecx*/, n /*edx*/)`.
|
||||
// Ensures room for `n` more stack values, growing if needed; returns 0
|
||||
// (without growing) when `(top-base)/16 + n` would exceed LUA_MAXCSTACK
|
||||
// (0x800 = 2048), 1 otherwise. Verified by the `cmp reg, 0x800` overflow
|
||||
// test at 0x006F2F43. Needed by any C function that pushes an unbounded
|
||||
// number of results (e.g. `strsplit`) — pushing past the ~20-slot
|
||||
// C-call guarantee without growing corrupts the stack.
|
||||
LUA_CHECK_STACK = 0x006F2F30,
|
||||
// `str_find` — the Lua 5.0 string-library `string.find` C function
|
||||
// (`int __fastcall(void *L)`), entry in the strlib luaL_reg table at
|
||||
// `0x00822dd0`. Returns `1` (nil pushed, no match), `2` (start, end),
|
||||
|
||||
+40
-5
@@ -14,16 +14,21 @@
|
||||
// Lua 5.1 math-library additions that 1.12's Lua 5.0 is missing:
|
||||
//
|
||||
// - `math.fmod(x, y)` — floating-point remainder of `x / y`.
|
||||
// - `math.modf(x)` — split into integral + fractional parts.
|
||||
// - `math.huge` — positive infinity constant.
|
||||
//
|
||||
// This is a pure rename: Lua 5.0 ships the exact same C function under the
|
||||
// name `math.mod`; 5.1 renamed the Lua-facing entry to `math.fmod`. We
|
||||
// register `math.fmod` as a direct alias of the engine's `math.mod` C
|
||||
// function (`FUN_LUA_MATH_FMOD`) — no wrapper. `math.mod` stays available too
|
||||
// (we don't touch it), so code written for either name works.
|
||||
// `math.fmod` is a pure rename: Lua 5.0 ships the exact same C function under
|
||||
// the name `math.mod`; 5.1 renamed the Lua-facing entry to `math.fmod`. We
|
||||
// register it as a direct alias of the engine's `math.mod` C function
|
||||
// (`FUN_LUA_MATH_FMOD`) — no wrapper. `math.mod` stays available too, so code
|
||||
// written for either name works. `modf` and `huge` are genuinely new in 5.1
|
||||
// (not in 1.12's mathlib table), so we implement/define them here.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace BaseLib::MathLib {
|
||||
|
||||
namespace {
|
||||
@@ -33,8 +38,38 @@ namespace {
|
||||
const auto Script_math_fmod =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_MATH_FMOD);
|
||||
|
||||
// `math.modf(x)` — returns `(integral, fractional)`, the integral part
|
||||
// truncated toward zero and the fractional part carrying `x`'s sign
|
||||
// (`modf(3.7) → 3, 0.7`; `modf(-3.7) → -3, -0.7`). Matches C `modf` / Lua 5.1.
|
||||
int __fastcall Script_math_modf(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: math.modf(x)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
double integral = 0.0;
|
||||
const double fractional = std::modf(Game::Lua::ToNumber(L, 1), &integral);
|
||||
Game::Lua::PushNumber(L, integral);
|
||||
Game::Lua::PushNumber(L, fractional);
|
||||
return 2;
|
||||
}
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("math", "fmod", Script_math_fmod);
|
||||
Game::Lua::RegisterTableFunction("math", "modf", &Script_math_modf);
|
||||
|
||||
// `math.huge` — a numeric constant, not a function. Fetch the (now
|
||||
// guaranteed-present) `math` table and set the field directly.
|
||||
void *L = Game::Lua::State();
|
||||
if (L != nullptr) {
|
||||
Game::Lua::PushString(L, "math");
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX); // [math]
|
||||
if (Game::Lua::Type(L, -1) == 5 /* LUA_TTABLE */) {
|
||||
Game::Lua::SetFieldNumber(L, "huge", HUGE_VAL); // math.huge = inf; [math]
|
||||
Game::Lua::SetTop(L, -2); // pop [math]
|
||||
} else {
|
||||
Game::Lua::SetTop(L, -2); // pop whatever GetTable returned
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Both states are Lua 5.0 and equally missing this; RegisterTableFunction
|
||||
|
||||
+188
-4
@@ -11,16 +11,22 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Lua 5.1 string-library additions that 1.12's Lua 5.0 is missing:
|
||||
// String helpers that 1.12's Lua 5.0 is missing:
|
||||
//
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction.
|
||||
// - `string.gmatch(s, pattern)` — match iterator.
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction (5.1).
|
||||
// - `string.gmatch(s, pattern)` — match iterator (5.1).
|
||||
// - `strsplit(sep, str [, pieces])` — WoW global; split on any char.
|
||||
// - `strjoin(delimiter, ...)` — WoW global; join varargs.
|
||||
// - `strtrim(str [, chars])` — WoW global; trim a char set.
|
||||
// - `strreplace(str, find, replace)` — plain-text replace (see below).
|
||||
// - `string.reverse(s)` / `strrev(s)` — 5.1 string function, missing in 5.0.
|
||||
//
|
||||
// Both are the same underlying pattern engine as functions 5.0 already ships:
|
||||
// The two pattern `string.*` ones reuse machinery 5.0 already ships:
|
||||
// - `match` is `find` returning captures / the whole match instead of the
|
||||
// start/end indices — so we call the engine's `str_find` and transform.
|
||||
// - `gmatch` is exactly 5.0's `string.gfind` (renamed in 5.1); we register
|
||||
// it as a direct alias of the engine's `gfind` C function.
|
||||
// `strsplit` is a hand-rolled port of 3.3.5's `strsplit` (see below).
|
||||
//
|
||||
// NOTE: only the `string.foo(s, ...)` call form works — NOT the
|
||||
// `("x"):foo(...)` method sugar. WoW's Lua VM strips type-metatables for
|
||||
@@ -33,6 +39,8 @@
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace BaseLib::StringLib {
|
||||
|
||||
@@ -83,9 +91,185 @@ int __fastcall Script_string_match(void *L) {
|
||||
const auto Script_string_gmatch =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_GFIND);
|
||||
|
||||
// `strsplit(sep, str [, pieces])` — the WoW global (also aliased as
|
||||
// `string.split`), split `str` on ANY character in `sep` and return the
|
||||
// pieces as multiple values. `pieces > 0` caps the result count, with the
|
||||
// unsplit remainder as the final piece; `0` / omitted = unlimited. Ported
|
||||
// from 3.3.5's `strsplit` (FUN_00816a60) with one deliberate change: we do
|
||||
// NOT `lua_settop(L, 0)` up front. Popping the args would leave the source
|
||||
// string unreferenced, so a GC step triggered by a `pushlstring` could free
|
||||
// the very buffer we're still scanning. Keeping the args on the stack keeps
|
||||
// the source GC-rooted; Lua returns the top N values (our pieces) regardless
|
||||
// of the args sitting below them. Each push is guarded by `lua_checkstack`
|
||||
// (this pushes an unbounded number of results), erroring like 3.3.5 on
|
||||
// genuine stack exhaustion.
|
||||
int __fastcall Script_strsplit(void *L) {
|
||||
const char *sep = Game::Lua::ToString(L, 1);
|
||||
const char *str = Game::Lua::ToString(L, 2);
|
||||
if (sep == nullptr || str == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strsplit(\"separators\", str [, pieces])");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const int pieces =
|
||||
Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0;
|
||||
|
||||
const char *segStart = str;
|
||||
int count = 0;
|
||||
// Only scan for separators while unlimited (pieces == 0) or the cap
|
||||
// hasn't been reached (pieces > 1). pieces == 1 (or <= 0 but non-zero)
|
||||
// yields the whole string as a single piece — matches 3.3.5.
|
||||
if (pieces == 0 || pieces > 1) {
|
||||
for (const char *p = str; *p != '\0'; ++p) {
|
||||
bool isSep = false;
|
||||
for (const char *s = sep; *s != '\0'; ++s) {
|
||||
if (*p == *s) {
|
||||
isSep = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isSep)
|
||||
continue;
|
||||
++count;
|
||||
if (Game::Lua::CheckStack(L, count) == 0) {
|
||||
Game::Lua::Error(L, "strsplit(): Stack overflow");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::PushLString(L, segStart,
|
||||
static_cast<unsigned int>(p - segStart));
|
||||
segStart = p + 1;
|
||||
if (count == pieces - 1)
|
||||
break; // cap hit; remainder becomes the final piece
|
||||
}
|
||||
}
|
||||
if (Game::Lua::CheckStack(L, count + 1) == 0) {
|
||||
Game::Lua::Error(L, "strsplit(): Stack overflow");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::PushString(L, segStart); // final piece (remainder to end)
|
||||
return count + 1;
|
||||
}
|
||||
|
||||
// `strjoin(delimiter, ...)` — the inverse of `strsplit`: concatenate the
|
||||
// vararg pieces with `delimiter` between each. Standard WoW global (FrameXML
|
||||
// defines it in Lua; there's no C original to mirror). A nil / non-coercible
|
||||
// piece is treated as an empty string rather than erroring, so the delimiter
|
||||
// placement stays predictable. `strjoin(",")` with no pieces returns "".
|
||||
int __fastcall Script_strjoin(void *L) {
|
||||
const char *delim = Game::Lua::ToString(L, 1);
|
||||
if (delim == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strjoin(delimiter, ...)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const unsigned delimLen = Game::Lua::StrLen(L, 1);
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
|
||||
std::string out; // no Error() past this point — a longjmp would leak it
|
||||
for (int i = 2; i <= top; ++i) {
|
||||
if (i > 2)
|
||||
out.append(delim, delimLen);
|
||||
const char *piece = Game::Lua::ToString(L, i);
|
||||
if (piece != nullptr)
|
||||
out.append(piece, Game::Lua::StrLen(L, i));
|
||||
}
|
||||
Game::Lua::PushLString(L, out.data(), static_cast<unsigned int>(out.size()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `strtrim(str [, chars])` — remove any of the characters in `chars` from
|
||||
// both ends of `str` (default: whitespace). `chars` is a literal set of
|
||||
// characters, not a pattern. Standard WoW global. Two-ended scan — no
|
||||
// allocation, embedded NULs preserved (bounds come from the Lua length).
|
||||
int __fastcall Script_strtrim(void *L) {
|
||||
const char *str = Game::Lua::ToString(L, 1);
|
||||
if (str == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strtrim(str [, chars])");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const unsigned len = Game::Lua::StrLen(L, 1);
|
||||
const char *chars = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2)
|
||||
: " \t\n\v\f\r";
|
||||
|
||||
bool trim[256] = {false};
|
||||
for (const char *c = chars; *c != '\0'; ++c)
|
||||
trim[static_cast<unsigned char>(*c)] = true;
|
||||
|
||||
unsigned start = 0, end = len;
|
||||
while (start < end && trim[static_cast<unsigned char>(str[start])])
|
||||
++start;
|
||||
while (end > start && trim[static_cast<unsigned char>(str[end - 1])])
|
||||
--end;
|
||||
Game::Lua::PushLString(L, str + start, end - start);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `strreplace(str, find, replace)` — replace every occurrence of the literal
|
||||
// substring `find` with `replace`; returns `(result, count)`. NOT a stock WoW
|
||||
// global and NOT pattern-based (that's `gsub`) — a plain-text replace that
|
||||
// needs no magic-character escaping. Empty `find` returns `str` unchanged
|
||||
// (count 0) rather than looping forever.
|
||||
int __fastcall Script_strreplace(void *L) {
|
||||
const char *str = Game::Lua::ToString(L, 1);
|
||||
const char *find = Game::Lua::ToString(L, 2);
|
||||
const char *repl = Game::Lua::ToString(L, 3);
|
||||
if (str == nullptr || find == nullptr || repl == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strreplace(str, find, replace)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const unsigned strLen = Game::Lua::StrLen(L, 1);
|
||||
const unsigned findLen = Game::Lua::StrLen(L, 2);
|
||||
const unsigned replLen = Game::Lua::StrLen(L, 3);
|
||||
|
||||
if (findLen == 0) { // nothing to match — return input unchanged
|
||||
Game::Lua::PushLString(L, str, strLen);
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::string out; // no Error() past this point
|
||||
int count = 0;
|
||||
unsigned i = 0;
|
||||
while (i < strLen) {
|
||||
if (i + findLen <= strLen && std::memcmp(str + i, find, findLen) == 0) {
|
||||
out.append(repl, replLen);
|
||||
i += findLen;
|
||||
++count;
|
||||
} else {
|
||||
out.push_back(str[i]);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
Game::Lua::PushLString(L, out.data(), static_cast<unsigned int>(out.size()));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(count));
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `string.reverse(s)` / `strrev(s)` — return `s` with its bytes reversed.
|
||||
// Lua 5.1 added `string.reverse`; 5.0's strlib lacks it. Byte-wise (not
|
||||
// UTF-8 aware), matching stock Lua; embedded NULs preserved (bounds from the
|
||||
// Lua length).
|
||||
int __fastcall Script_string_reverse(void *L) {
|
||||
const char *s = Game::Lua::ToString(L, 1);
|
||||
if (s == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: string.reverse(s)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const unsigned len = Game::Lua::StrLen(L, 1);
|
||||
std::string out(len, '\0'); // no Error() past this point
|
||||
for (unsigned i = 0; i < len; ++i)
|
||||
out[i] = s[len - 1 - i];
|
||||
Game::Lua::PushLString(L, out.data(), len);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "match", &Script_string_match);
|
||||
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch);
|
||||
Game::Lua::RegisterTableFunction("string", "reverse", &Script_string_reverse);
|
||||
Game::Lua::RegisterGlobalFunction("strsplit", &Script_strsplit);
|
||||
Game::Lua::RegisterGlobalFunction("strjoin", &Script_strjoin);
|
||||
Game::Lua::RegisterGlobalFunction("strtrim", &Script_strtrim);
|
||||
Game::Lua::RegisterGlobalFunction("strreplace", &Script_strreplace);
|
||||
Game::Lua::RegisterGlobalFunction("strrev", &Script_string_reverse);
|
||||
}
|
||||
|
||||
// Both states are Lua 5.0 and equally missing these; RegisterTableFunction
|
||||
|
||||
@@ -107,11 +107,19 @@ static int AuxResume(void *L, void *co, int nargs) {
|
||||
: "cannot resume non-suspended coroutine");
|
||||
return -1;
|
||||
}
|
||||
if (Game::Lua::CheckStack(co, nargs) == 0) {
|
||||
Game::Lua::PushString(L, "too many arguments to resume coroutine");
|
||||
return -1;
|
||||
}
|
||||
Game::Lua::XMove(L, co, nargs);
|
||||
const int s = Game::Lua::Resume(co, nargs);
|
||||
const bool yielded = LuaCI(co) > LuaBaseCI(co);
|
||||
if (s == LUA_OK || yielded) {
|
||||
const int nres = Game::Lua::GetTop(co);
|
||||
if (Game::Lua::CheckStack(L, nres) == 0) {
|
||||
Game::Lua::PushString(L, "too many results to resume coroutine");
|
||||
return -1;
|
||||
}
|
||||
Game::Lua::XMove(co, L, nres);
|
||||
return nres;
|
||||
}
|
||||
|
||||
+11
-1
@@ -28,7 +28,17 @@ namespace {
|
||||
// any engine code runs. `RetryClaims` (triggered by the
|
||||
// `Frame::RegisterEvent` hook) walks this list and claims a NULL slot
|
||||
// for each unclaimed entry.
|
||||
constexpr int MAX_RESERVED = 32;
|
||||
//
|
||||
// MUST stay comfortably above the total number of `AutoReserve` instances
|
||||
// across the codebase (file-scope + the lazily-registered TTS ones) — the
|
||||
// constructor SILENTLY drops any reservation past the cap, and which ones
|
||||
// overflow depends on unspecified cross-TU static-init order, so exceeding
|
||||
// it makes an arbitrary subset of custom events quietly stop firing (their
|
||||
// `Lookup` returns -1 and `Fire` no-ops). We hit exactly that at 32: ~40
|
||||
// reservations meant a shifting handful (e.g. ITEM_DATA_LOAD_RESULT /
|
||||
// GET_ITEM_INFO_RECEIVED) never claimed a slot. Grep `AutoReserve` before
|
||||
// bumping the reservation count near this ceiling.
|
||||
constexpr int MAX_RESERVED = 64;
|
||||
struct ReservedName {
|
||||
const char *name;
|
||||
int slot; // -1 until claimed
|
||||
|
||||
+3
-2
@@ -37,8 +37,9 @@ namespace Event::Custom {
|
||||
// DLL operate on the table independently of the others.
|
||||
//
|
||||
// The name pointer must outlive the engine (a string literal does).
|
||||
// Same name reserved twice is deduped; reserving more than 32 names
|
||||
// total silently drops the overflow.
|
||||
// Same name reserved twice is deduped; reserving more than `MAX_RESERVED`
|
||||
// names total (see Custom.cpp) silently drops the overflow — keep that cap
|
||||
// above the codebase-wide `AutoReserve` count.
|
||||
struct AutoReserve {
|
||||
explicit AutoReserve(const char *name);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `PLAYER_ENTERING_WORLD(isInitialLogin, isReloadingUi)` — the 8.0.1 payload,
|
||||
// backported. Vanilla fires PEW with no args; backported addons expect the
|
||||
// two booleans to distinguish a fresh login from a `/reload` from an
|
||||
// instance/zone transition.
|
||||
//
|
||||
// Delivery: PEW is a no-arg event, so the engine broadcasts it through
|
||||
// `FUN_FIRE_EVENT_NO_ARGS` (the `__fastcall(int)` dispatcher), NOT the
|
||||
// variadic `FUN_FIRE_EVENT`. We intercept that dispatcher (via the shared
|
||||
// `Event::SignalHook`) and, for the PEW event id, suppress the no-arg fire
|
||||
// and instead re-broadcast through the variadic `FUN_FIRE_EVENT` with a
|
||||
// two-boolean payload. Both fire paths read the same frame chain off the
|
||||
// event's table slot, so the frames registered for PEW receive the args
|
||||
// exactly once. Booleans use the `%d`/`%s`+nullptr trick (`1` / `nil`), so
|
||||
// `if isInitialLogin then …` reads naturally.
|
||||
//
|
||||
// Flags, from signals we already produce — no PEW-specific engine hook:
|
||||
// - isInitialLogin: our GlueModuleAutoRegister runs on every glue-screen
|
||||
// boot (initial launch + each logout return), so a set latch means "the
|
||||
// next enter-world is a fresh login". The first PEW consumes it.
|
||||
// - isReloadingUi: our ModuleAutoRegister runs whenever the in-game Lua
|
||||
// state is (re)built — login OR `/reload`, but NOT a zone transition. So
|
||||
// "script re-init happened and it wasn't the fresh login" == a reload.
|
||||
// - Instance/zone transition rebuilds neither, so both latches are clear →
|
||||
// both false, matching retail.
|
||||
|
||||
#include "Game.h"
|
||||
#include "event/Custom.h"
|
||||
#include "event/SignalHook.h"
|
||||
|
||||
namespace Event::EnteringWorld {
|
||||
|
||||
namespace {
|
||||
|
||||
// Set on glue-screen boot; a pending latch means the next enter-world is a
|
||||
// fresh character login (from the login/char-select screen).
|
||||
bool g_atGlue = false;
|
||||
// Set when the in-game Lua state is (re)built (login or /reload).
|
||||
bool g_scriptReloaded = false;
|
||||
// Cached PLAYER_ENTERING_WORLD event-table slot; -1 = unresolved. Reset per
|
||||
// in-game init (the table is rebuilt, though engine slots are stable) and
|
||||
// re-resolved lazily at fire time.
|
||||
int g_pewId = -1;
|
||||
|
||||
int PewId() {
|
||||
if (g_pewId < 0)
|
||||
g_pewId = Event::Custom::LookupByName("PLAYER_ENTERING_WORLD");
|
||||
return g_pewId;
|
||||
}
|
||||
|
||||
// Shared-hook interceptor. For PEW, re-broadcast with the payload and
|
||||
// suppress the engine's no-arg fire; everything else passes through.
|
||||
bool Intercept(int eventID) {
|
||||
const int pew = PewId();
|
||||
if (pew < 0 || eventID != pew)
|
||||
return false;
|
||||
|
||||
const bool isInitialLogin = g_atGlue;
|
||||
g_atGlue = false;
|
||||
const bool isReloadingUi = !isInitialLogin && g_scriptReloaded;
|
||||
g_scriptReloaded = false;
|
||||
|
||||
// Re-fire through the variadic dispatcher with the two booleans. true → 1
|
||||
// (%d), false → nil (%s + nullptr, which the dispatcher's pushstring
|
||||
// tail-jumps to pushnil).
|
||||
auto *const kNil = static_cast<const char *>(nullptr);
|
||||
if (isInitialLogin && isReloadingUi)
|
||||
Event::Custom::Fire(pew, "%d%d", 1, 1);
|
||||
else if (isInitialLogin)
|
||||
Event::Custom::Fire(pew, "%d%s", 1, kNil);
|
||||
else if (isReloadingUi)
|
||||
Event::Custom::Fire(pew, "%s%d", kNil, 1);
|
||||
else
|
||||
Event::Custom::Fire(pew, "%s%s", kNil, kNil);
|
||||
return true; // handled — suppress the original no-arg PEW fire
|
||||
}
|
||||
|
||||
const Event::SignalHook::AutoSubscribe _intercept{&Intercept};
|
||||
|
||||
// In-game Lua (re)init: login or /reload rebuilt the script state (never a
|
||||
// zone transition). Also re-arm the PEW slot lookup — the event table was
|
||||
// rebuilt.
|
||||
void OnInGameInit() {
|
||||
g_scriptReloaded = true;
|
||||
g_pewId = -1;
|
||||
}
|
||||
|
||||
// Glue-screen boot: we're at the login/char-select screen, so the next
|
||||
// enter-world will be a fresh login.
|
||||
void OnGlueInit() { g_atGlue = true; }
|
||||
|
||||
const Game::ModuleAutoRegister _inGame{&OnInGameInit};
|
||||
const Game::GlueModuleAutoRegister _glue{&OnGlueInit};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Event::EnteringWorld
|
||||
@@ -0,0 +1,49 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "SignalHook.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Event::SignalHook {
|
||||
|
||||
namespace {
|
||||
|
||||
// Chained at static-init time by each `AutoSubscribe` constructor, before
|
||||
// `DllMain`. Zero-initialized (namespace static) ahead of any dynamic init,
|
||||
// so the head is valid when subscribers link in.
|
||||
AutoSubscribe *g_head = nullptr;
|
||||
|
||||
// `FUN_FIRE_EVENT_NO_ARGS` is `__fastcall(int eventID)`; we mirror loot's
|
||||
// long-standing shape with a dummy EDX so the trampoline call matches the
|
||||
// register layout, and forward `0` for it (unused by the callee).
|
||||
using FireNoArgs_t = void(__fastcall *)(int eventID, int edx);
|
||||
FireNoArgs_t g_orig = nullptr;
|
||||
|
||||
void __fastcall Hook(int eventID, int /*edx*/) {
|
||||
for (AutoSubscribe *s = g_head; s != nullptr; s = s->next)
|
||||
if (s->fn(eventID))
|
||||
return; // a subscriber handled it — suppress the original fire
|
||||
g_orig(eventID, 0);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hook{
|
||||
Offsets::FUN_FIRE_EVENT_NO_ARGS, reinterpret_cast<void *>(&Hook),
|
||||
reinterpret_cast<void **>(&g_orig)};
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoSubscribe::AutoSubscribe(Interceptor f) : fn(f), next(g_head) { g_head = this; }
|
||||
|
||||
} // namespace Event::SignalHook
|
||||
@@ -0,0 +1,47 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// Shared interceptor for the engine's no-arg event dispatcher
|
||||
// `FUN_FIRE_EVENT_NO_ARGS` (`0x00703E50`) — the `__fastcall(int eventID)`
|
||||
// path every no-arg event broadcast goes through (PLAYER_ENTERING_WORLD,
|
||||
// LOOT_OPENED/CLOSED, zone changes, …), as opposed to the variadic
|
||||
// `FUN_FIRE_EVENT` used by events that carry a printf-style payload.
|
||||
//
|
||||
// MinHook allows only ONE hook per target address, and more than one feature
|
||||
// needs to intercept this dispatcher (loot silent-scan suppression, the
|
||||
// PLAYER_ENTERING_WORLD payload). So — exactly like `Tick::WorldTick` for the
|
||||
// per-frame tick — this module owns the single hook and fans out to
|
||||
// subscribers. Declare a file-scope `static const
|
||||
// Event::SignalHook::AutoSubscribe` in your module.
|
||||
//
|
||||
// An interceptor returns `true` when it has fully handled `eventID` and the
|
||||
// engine's original no-arg fire must be SUPPRESSED — either because it
|
||||
// swallowed the event (loot) or re-fired it a different way (the PEW payload
|
||||
// re-broadcasts through the variadic `FUN_FIRE_EVENT` with args). Returning
|
||||
// `false` lets the event fall through to the next subscriber and ultimately
|
||||
// the original. The FIRST subscriber to return `true` wins and the original
|
||||
// is skipped, so subscribers must claim disjoint event ids.
|
||||
|
||||
namespace Event::SignalHook {
|
||||
|
||||
using Interceptor = bool (*)(int eventID);
|
||||
|
||||
struct AutoSubscribe {
|
||||
explicit AutoSubscribe(Interceptor fn);
|
||||
Interceptor fn;
|
||||
AutoSubscribe *next;
|
||||
};
|
||||
|
||||
} // namespace Event::SignalHook
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "item/Arg.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -245,7 +246,89 @@ static int __fastcall Script_C_Item_GetItemFamily(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Item.GetItemInfo(itemInfo)` — the full modern info tuple, sourced
|
||||
// entirely from the cached `ItemStats_C` record (+ the class/subclass/invtype
|
||||
// DBC name lookups this file already does for `GetItemInfoInstant`). Accepts
|
||||
// a numeric itemID, an `"item:NNN"` string, or a full chat link — same parser
|
||||
// as `GetItemInfoInstant`.
|
||||
//
|
||||
// Returns (18 values):
|
||||
// itemName, itemLink, itemQuality, itemLevel, itemMinLevel, itemType,
|
||||
// itemSubType, itemStackCount, itemEquipLoc, itemTexture, sellPrice,
|
||||
// classID, subclassID, bindType, expansionID, setID, isCraftingReagent,
|
||||
// itemDescription
|
||||
//
|
||||
// `expansionID` is always 0 (vanilla) and `isCraftingReagent` always false
|
||||
// (no such flag in 1.12's item data); `setID` is nil when the item has no
|
||||
// set. On a cache miss it warms the cache and returns nothing (nil) —
|
||||
// the value lands on a retry after `GET_ITEM_INFO_RECEIVED`,
|
||||
// matching the modern async contract.
|
||||
static int __fastcall Script_C_Item_GetItemInfo(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *record = FetchItemRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0; // nil this call; GET_ITEM_INFO_RECEIVED fires when ready
|
||||
}
|
||||
|
||||
auto u32 = [record](int off) {
|
||||
return *reinterpret_cast<const uint32_t *>(record + off);
|
||||
};
|
||||
const uint32_t quality = u32(Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
const uint32_t itemLevel = u32(Offsets::OFF_ITEMSTATS_ITEM_LEVEL);
|
||||
const uint32_t reqLevel = u32(Offsets::OFF_ITEMSTATS_REQUIRED_LEVEL);
|
||||
const uint32_t classID = u32(Offsets::OFF_ITEMSTATS_CLASS);
|
||||
const uint32_t subClassID = u32(Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
const uint32_t stackCount = u32(Offsets::OFF_ITEMSTATS_STACKABLE);
|
||||
const uint32_t invType = u32(Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
const uint32_t displayInfoID = u32(Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
const uint32_t sellPrice = u32(Offsets::OFF_ITEMSTATS_SELL_PRICE);
|
||||
const uint32_t bindType = u32(Offsets::OFF_ITEMSTATS_BONDING);
|
||||
const uint32_t setID = u32(Offsets::OFF_ITEMSTATS_ITEM_SET);
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
const char *description = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DESCRIPTION);
|
||||
|
||||
char iconPath[260];
|
||||
if (!BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
iconPath[0] = 0;
|
||||
char link[256];
|
||||
const bool haveLink = Item::Link::BasicFromItemID(static_cast<uint32_t>(itemID),
|
||||
link, sizeof(link));
|
||||
|
||||
Game::Lua::PushString(L, PushedOrEmpty(name)); // 1 itemName
|
||||
if (haveLink)
|
||||
Game::Lua::PushString(L, link); // 2 itemLink
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(quality)); // 3 itemQuality
|
||||
Game::Lua::PushNumber(L, static_cast<double>(itemLevel)); // 4 itemLevel
|
||||
Game::Lua::PushNumber(L, static_cast<double>(reqLevel)); // 5 itemMinLevel
|
||||
Game::Lua::PushString(L, LookupItemClassName(classID)); // 6 itemType
|
||||
Game::Lua::PushString(L, LookupItemSubClassName(classID, subClassID)); // 7 itemSubType
|
||||
Game::Lua::PushNumber(L, static_cast<double>(stackCount)); // 8 itemStackCount
|
||||
Game::Lua::PushString(L, LookupInvType(invType)); // 9 itemEquipLoc
|
||||
Game::Lua::PushString(L, iconPath); // 10 itemTexture
|
||||
Game::Lua::PushNumber(L, static_cast<double>(sellPrice)); // 11 sellPrice
|
||||
Game::Lua::PushNumber(L, static_cast<double>(classID)); // 12 classID
|
||||
Game::Lua::PushNumber(L, static_cast<double>(subClassID)); // 13 subclassID
|
||||
Game::Lua::PushNumber(L, static_cast<double>(bindType)); // 14 bindType
|
||||
Game::Lua::PushNumber(L, 0.0); // 15 expansionID (classic)
|
||||
if (setID != 0)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(setID)); // 16 setID
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushBool(L, false); // 17 isCraftingReagent
|
||||
Game::Lua::PushString(L, PushedOrEmpty(description)); // 18 itemDescription
|
||||
return 18;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemInfo", &Script_C_Item_GetItemInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemInfoInstant", &Script_GetItemInfoInstant);
|
||||
Game::Lua::RegisterGlobalFunction("GetItemIcon", &Script_GetItemIcon);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemIcon", &Script_C_Item_GetItemIcon);
|
||||
|
||||
+10
-14
@@ -27,6 +27,7 @@
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../event/Custom.h"
|
||||
#include "../event/SignalHook.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "../tick/WorldTick.h"
|
||||
|
||||
@@ -115,9 +116,6 @@ using LootSlotLinkBuilder_t = const char *(__fastcall *)(uint32_t userFacingSlot
|
||||
using LootController_t = void(__fastcall *)(int lootStructPtr,
|
||||
int coin,
|
||||
int unk);
|
||||
using FireEventNoArgs_t = void(__fastcall *)(int eventID,
|
||||
int /*edx unused*/);
|
||||
|
||||
struct C3Vector {
|
||||
float x, y, z;
|
||||
};
|
||||
@@ -256,7 +254,6 @@ void StartLoot(uint64_t guid) {
|
||||
// === Hooks ===
|
||||
|
||||
LootController_t s_lootController_o = nullptr;
|
||||
FireEventNoArgs_t s_fireEventNoArgs_o = nullptr;
|
||||
|
||||
// `LOOT_OPENED` / `LOOT_CLOSED` get suppressed only while a scan is
|
||||
// actively interleaving open/close cycles, signaled by
|
||||
@@ -265,12 +262,14 @@ FireEventNoArgs_t s_fireEventNoArgs_o = nullptr;
|
||||
// `SendCloseLoot` that follows, then dropped before
|
||||
// `TryStartNext`-initiated CMSG_LOOTs go out. Every other engine
|
||||
// event fire — and normal play's loot events — pass through.
|
||||
void __fastcall FireEventNoArgs_h(int eventID, int /*unusedEdx*/) {
|
||||
if (s_scan.suppressEvents &&
|
||||
(eventID == EVENT_LOOT_OPENED || eventID == EVENT_LOOT_CLOSED)) {
|
||||
return;
|
||||
}
|
||||
s_fireEventNoArgs_o(eventID, 0);
|
||||
//
|
||||
// Runs as an interceptor on the shared no-arg event hook
|
||||
// (`Event::SignalHook`) rather than owning the hook directly, so it can
|
||||
// coexist with other features that intercept the same dispatcher (e.g. the
|
||||
// PLAYER_ENTERING_WORLD payload). Return true to swallow.
|
||||
bool SuppressLootEvents(int eventID) {
|
||||
return s_scan.suppressEvents &&
|
||||
(eventID == EVENT_LOOT_OPENED || eventID == EVENT_LOOT_CLOSED);
|
||||
}
|
||||
|
||||
// Intercepts the engine's loot-window state controller. Two paths:
|
||||
@@ -336,10 +335,7 @@ const Game::HookAutoRegister _hookController{
|
||||
reinterpret_cast<void *>(&LootController_h),
|
||||
reinterpret_cast<void **>(&s_lootController_o)};
|
||||
|
||||
const Game::HookAutoRegister _hookFireEvent{
|
||||
Offsets::FUN_FIRE_EVENT_NO_ARGS,
|
||||
reinterpret_cast<void *>(&FireEventNoArgs_h),
|
||||
reinterpret_cast<void **>(&s_fireEventNoArgs_o)};
|
||||
const Event::SignalHook::AutoSubscribe _lootIntercept{&SuppressLootEvents};
|
||||
|
||||
// === Tick (timeout only) ===
|
||||
|
||||
|
||||
@@ -76,6 +76,87 @@ static int __fastcall Script_GetQuestIDForLogIndex(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_QuestLog.GetLogIndexForQuestID(questID)` — the inverse of
|
||||
// `GetQuestIDForLogIndex`: the 1-based quest-log index of `questID`, or nil
|
||||
// if the quest isn't in the log. The index spans the full entry array
|
||||
// (headers included), matching `GetQuestIDForLogIndex` /
|
||||
// `GetQuestLogTitle`. Walks `VAR_QUEST_LOG_ENTRIES`, skipping header rows via
|
||||
// the `+8` header-pointer gate, and returns the first matching entry's index.
|
||||
static int __fastcall Script_GetLogIndexForQuestID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetLogIndexForQuestID(questID)");
|
||||
return 0;
|
||||
}
|
||||
const int target = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (target <= 0)
|
||||
return 0; // nil — no such quest
|
||||
|
||||
const int total = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_QUEST_LOG_ENTRY_COUNT));
|
||||
auto *base = reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_QUEST_LOG_ENTRIES));
|
||||
for (int i = 0; i < total; ++i) {
|
||||
auto *entry = base + i * Offsets::OFF_QUEST_LOG_ENTRY_STRIDE;
|
||||
if (*reinterpret_cast<const void *const *>(
|
||||
entry + Offsets::OFF_QUEST_LOG_ENTRY_HEADER_PTR) != nullptr)
|
||||
continue; // header row
|
||||
const int questID = *reinterpret_cast<const int *>(
|
||||
entry + Offsets::OFF_QUEST_LOG_ENTRY_QUEST_ID);
|
||||
if (questID == target) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1)); // 1-based
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0; // nil — quest not in the log
|
||||
}
|
||||
|
||||
// `C_QuestLog.GetHeaderIndexForQuest(questID)` — the 1-based log index of the
|
||||
// collapsible category header (zone / "Dungeon" / class sort, …) the quest
|
||||
// sits under, or nil if the quest isn't in the log or has no header above it.
|
||||
// The log is laid out header-then-its-quests, so we locate the quest's entry
|
||||
// and walk backwards to the nearest preceding header row (`+8` non-NULL).
|
||||
static int __fastcall Script_GetHeaderIndexForQuest(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetHeaderIndexForQuest(questID)");
|
||||
return 0;
|
||||
}
|
||||
const int target = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (target <= 0)
|
||||
return 0; // nil — no such quest
|
||||
|
||||
const int total = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_QUEST_LOG_ENTRY_COUNT));
|
||||
auto *base = reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_QUEST_LOG_ENTRIES));
|
||||
|
||||
// Locate the quest's entry.
|
||||
int questIndex = -1;
|
||||
for (int i = 0; i < total; ++i) {
|
||||
auto *entry = base + i * Offsets::OFF_QUEST_LOG_ENTRY_STRIDE;
|
||||
if (*reinterpret_cast<const void *const *>(
|
||||
entry + Offsets::OFF_QUEST_LOG_ENTRY_HEADER_PTR) != nullptr)
|
||||
continue; // header row
|
||||
if (*reinterpret_cast<const int *>(
|
||||
entry + Offsets::OFF_QUEST_LOG_ENTRY_QUEST_ID) == target) {
|
||||
questIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (questIndex < 0)
|
||||
return 0; // quest not in the log
|
||||
|
||||
// Walk back to the nearest preceding header.
|
||||
for (int i = questIndex - 1; i >= 0; --i) {
|
||||
auto *entry = base + i * Offsets::OFF_QUEST_LOG_ENTRY_STRIDE;
|
||||
if (*reinterpret_cast<const void *const *>(
|
||||
entry + Offsets::OFF_QUEST_LOG_ENTRY_HEADER_PTR) != nullptr) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1)); // 1-based
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0; // nil — quest sits above any header (shouldn't normally happen)
|
||||
}
|
||||
|
||||
// `C_QuestLog.IsOnQuest(questID)` — true iff `questID` is currently
|
||||
// in the player's quest log (incomplete OR ready-to-turn-in; the log
|
||||
// holds both). Walks `VAR_QUEST_LOG_ENTRIES` and matches against each
|
||||
@@ -148,6 +229,10 @@ static int __fastcall Script_IsUnitOnQuest(void *L) {
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_QuestLog", "GetQuestIDForLogIndex",
|
||||
&Script_GetQuestIDForLogIndex);
|
||||
Game::Lua::RegisterTableFunction("C_QuestLog", "GetLogIndexForQuestID",
|
||||
&Script_GetLogIndexForQuestID);
|
||||
Game::Lua::RegisterTableFunction("C_QuestLog", "GetHeaderIndexForQuest",
|
||||
&Script_GetHeaderIndexForQuest);
|
||||
Game::Lua::RegisterTableFunction("C_QuestLog", "IsOnQuest",
|
||||
&Script_IsOnQuest);
|
||||
Game::Lua::RegisterTableFunction("C_QuestLog", "IsUnitOnQuest",
|
||||
|
||||
+69
-16
@@ -13,20 +13,33 @@
|
||||
// `GetPosition` virtual (vtable slot 5) `CheckInteractDistance` uses:
|
||||
//
|
||||
// `UnitInRange(unit) → (inRange, checkedRange)` — 40-yard healing-
|
||||
// range check (fixed threshold on the squared distance).
|
||||
// range check. Reach-aware: matches the engine's own spell-range
|
||||
// formula (see below), so a 40-yard heal registers in-range against
|
||||
// a target whose *center* is slightly past 40 yards but whose body
|
||||
// is within reach.
|
||||
// `UnitDistanceSquared(unit) → (distanceSquared, checkedPosition)` —
|
||||
// the raw squared world distance player→unit. Squared because
|
||||
// nearly every consumer compares against a threshold (`distSq <=
|
||||
// range*range`) or ranks by nearest, neither of which needs the
|
||||
// sqrt; that's why retail exposes only the squared form (added
|
||||
// 5.0.4). Center-to-center — NOT reach-aware (that's UnitXP_SP3's
|
||||
// niche); equivalent to computing it from SuperWoW `UnitPosition`,
|
||||
// but self-contained (reads the position in C, no sibling-DLL
|
||||
// dependency, no clash with SuperWoW's own `UnitPosition`).
|
||||
// 5.0.4). Center-to-center — this one is NOT reach-adjusted (retail's
|
||||
// `UnitDistanceSquared` is also raw center distance); the reach
|
||||
// correction lives only in `UnitInRange`.
|
||||
//
|
||||
// Reach-awareness: the engine's spell-range helper `FUN_006e3480` compares
|
||||
// center distance against `baseRange + reachCaster + reachTarget`, where
|
||||
// each unit's reach is the bounding-radius float at `[m_objectFields +
|
||||
// 0x1F0]` (the same size factor the interact-distance and loot-range
|
||||
// checks add). So the effective in-range distance for a 40-yard heal is
|
||||
// `40 + playerReach + targetReach`. On big targets that's easily 2–3
|
||||
// yards past the raw 40, which is why a pure center-to-center 40-yard cap
|
||||
// reported out-of-range for a target the client could actually heal.
|
||||
|
||||
#include "Game.h"
|
||||
#include "unit/Position.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Unit::Range {
|
||||
@@ -34,18 +47,44 @@ namespace Unit::Range {
|
||||
namespace {
|
||||
|
||||
constexpr float kRangeYards = 40.0f;
|
||||
constexpr float kRangeYardsSq = kRangeYards * kRangeYards;
|
||||
|
||||
// Squared world distance from the player to `token`'s unit. Returns true
|
||||
// and writes *outSq on success; false when either position is unavailable
|
||||
// (unresolved / absent token, or an object with no known position yet —
|
||||
// e.g. a party member outside the client's sync range). This false is
|
||||
// exactly the `checkedPosition = false` case retail flags.
|
||||
// Reads a unit's bounding radius (the engine's per-unit range "size
|
||||
// factor") from `[m_objectFields + 0x1F0]`. Returns 0 for a null object
|
||||
// or a unit whose descriptor isn't populated yet — a safe fallback that
|
||||
// degrades to the plain base-range check. Same field the loot/interact
|
||||
// range checks (loot/Nearby.cpp) read.
|
||||
float BoundingRadius(void *obj) {
|
||||
if (obj == nullptr)
|
||||
return 0.0f;
|
||||
auto *fields = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(obj) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (fields == nullptr)
|
||||
return 0.0f;
|
||||
return *reinterpret_cast<const float *>(
|
||||
fields + Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
// Resolves `token` once and reads both its world position and bounding
|
||||
// radius. Returns false when the position is unavailable (unresolved /
|
||||
// absent token, or an object with no known position yet — e.g. a party
|
||||
// member outside the client's sync range). This false is exactly the
|
||||
// `checkedPosition = false` case retail flags.
|
||||
bool ReadPosAndReach(const char *token, float pos[3], float *reach) {
|
||||
void *obj = Unit::Position::ResolveToken(token);
|
||||
if (!Unit::Position::Read(obj, pos))
|
||||
return false;
|
||||
*reach = BoundingRadius(obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Squared world distance from the player to `token`'s unit (raw,
|
||||
// center-to-center; reach is applied by the caller when needed).
|
||||
bool PlayerToUnitDistSq(const char *token, float *outSq) {
|
||||
float unitPos[3] = {};
|
||||
float playerPos[3] = {};
|
||||
if (!Unit::Position::ReadToken(token, unitPos) ||
|
||||
!Unit::Position::ReadToken("player", playerPos))
|
||||
float reach = 0.0f;
|
||||
if (!ReadPosAndReach(token, unitPos, &reach) ||
|
||||
!ReadPosAndReach("player", playerPos, &reach))
|
||||
return false;
|
||||
const float dx = unitPos[0] - playerPos[0];
|
||||
const float dy = unitPos[1] - playerPos[1];
|
||||
@@ -72,14 +111,28 @@ int __fastcall Script_UnitInRange(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
float distSq = 0.0f;
|
||||
if (!PlayerToUnitDistSq(token, &distSq)) {
|
||||
float unitPos[3] = {};
|
||||
float playerPos[3] = {};
|
||||
float unitReach = 0.0f;
|
||||
float playerReach = 0.0f;
|
||||
if (!ReadPosAndReach(token, unitPos, &unitReach) ||
|
||||
!ReadPosAndReach("player", playerPos, &playerReach)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
}
|
||||
|
||||
Game::Lua::PushBool(L, distSq <= kRangeYardsSq);
|
||||
const float dx = unitPos[0] - playerPos[0];
|
||||
const float dy = unitPos[1] - playerPos[1];
|
||||
const float dz = unitPos[2] - playerPos[2];
|
||||
const float distSq = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
// Effective range = base + both units' reach, mirroring the engine's
|
||||
// spell-range formula (FUN_006e3480). Without the reach terms a target
|
||||
// whose center sits just past 40 yards but whose body is within reach
|
||||
// reports out-of-range even though a 40-yard heal would land.
|
||||
const float effRange = kRangeYards + playerReach + unitReach;
|
||||
Game::Lua::PushBool(L, distSq <= effRange * effRange);
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 2;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user