mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 04:47:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ed0edc947 | ||
|
|
1645e6b43c | ||
|
|
efcdc54a50 | ||
|
|
6a24271cd1 | ||
|
|
a7ce7555f0 | ||
|
|
04790cb6c1 | ||
|
|
e7ecae56da | ||
|
|
65c6b0b271 | ||
|
|
5f94fb98bd | ||
|
|
426f6b6ec9 |
@@ -51,7 +51,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
|
||||
| Namespace | Calls |
|
||||
|-----------|-------|
|
||||
| [Action](docs/API.md#action) | `GetActionInfo`, `IsItemAction`, `HasBonusActionBar`, `C_ActionBar.EnableActionRangeCheck` |
|
||||
| [Action](docs/API.md#action) | `GetActionInfo`, `IsItemAction`, `IsStackableAction`, `HasBonusActionBar`, `C_ActionBar.EnableActionRangeCheck`, `C_ActionBar.GetSpell`, `C_ActionBar.GetItemActionOnEquipSpellID`, `C_ActionBar.IsStackableAction`, `C_ActionBar.IsMacroActionWithShowTooltip`, `C_ActionBar.FindSpellActionButtons`, `C_ActionBar.HasSpellActionButtons`, `C_ActionBar.IsOnBarOrSpecialBar` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnInterfaceVersion`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded`, `C_AddOns.LoadAddOn` |
|
||||
| [APIDocumentation](docs/API.md#apidocumentation) | `/classicapi` in-game browser, `C_APIDocumentation.GetSystem`, `C_APIDocumentation.GetSystems` |
|
||||
| [Animation](docs/API.md#animation) | `region:CreateAnimationGroup`, `region:GetAnimationGroups`, `region:StopAnimating`, `SetMaxAnimFramerate`, `GetMaxAnimFramerate`, `AnimationGroup` and `Animation` / `Alpha` / `Translation` / `Scale` / `Rotation` / `Path` / `ControlPoint` methods |
|
||||
@@ -211,8 +211,8 @@ for what each command does, and
|
||||
|
||||
Each setting is saved in `WTF\Config.wtf`. Read and write them with `GetCVar`
|
||||
/ `SetCVar` or `/console`. Click a group name for the full rules. To change
|
||||
these settings and the player cache in the game, open the game menu and
|
||||
click AddOn Options, then ClassicAPI.
|
||||
these settings, the player cache and the ClassicAPI key bindings in the game,
|
||||
open the game menu and click AddOn Options, then ClassicAPI.
|
||||
|
||||
| Group | CVar | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
|
||||
+306
-72
@@ -15,6 +15,14 @@ build instructions.
|
||||
- [`HasBonusActionBar()`](#hasbonusactionbar)
|
||||
- [`C_ActionBar.ShouldShowKeyring()`](#c_actionbarshouldshowkeyring)
|
||||
- [`C_ActionBar.EnableActionRangeCheck(slot, enable)`](#c_actionbarenableactionrangecheckslot-enable)
|
||||
- [`C_ActionBar.GetSpell(actionID)`](#c_actionbargetspellactionid)
|
||||
- [`C_ActionBar.GetItemActionOnEquipSpellID(actionID)`](#c_actionbargetitemactiononequipspellidactionid)
|
||||
- [`IsStackableAction(slot)` / `C_ActionBar.IsStackableAction(actionID)`](#isstackableactionslot--c_actionbarisstackableactionactionid)
|
||||
- [`C_ActionBar.IsMacroActionWithShowTooltip(actionID)`](#c_actionbarismacroactionwithshowtooltipactionid)
|
||||
- [`C_ActionBar.FindSpellActionButtons(spell [, setToSearch])`](#c_actionbarfindspellactionbuttonsspell--settosearch)
|
||||
- [`C_ActionBar.HasSpellActionButtons(spell [, setToSearch])`](#c_actionbarhasspellactionbuttonsspell--settosearch)
|
||||
- [`C_ActionBar.IsOnBarOrSpecialBar(spell [, setToSearch])`](#c_actionbarisonbarorspecialbarspell--settosearch)
|
||||
- [`Enum.ActionBarSet`](#enumactionbarset)
|
||||
|
||||
- [AddOns](#addons)
|
||||
- [`C_AddOns.GetAddOnName(indexOrName)`](#c_addonsgetaddonnameindexorname)
|
||||
@@ -665,21 +673,26 @@ build instructions.
|
||||
- [`getfenv` / `setfenv` environment protection](#getfenv--setfenv-environment-protection)
|
||||
- [`select(index, ...)`](#selectindex-)
|
||||
- [`unpack(list [, i [, j]])`](#unpacklist--i--j)
|
||||
- [`newproxy([proxy])`](#newproxyproxy)
|
||||
- [`scrub(...)`](#scrub)
|
||||
- [`difftime(time2 [, time1])`](#difftimetime2--time1)
|
||||
- [`xpcall(f, msgh [, arg1, ...])`](#xpcallf-msgh--arg1-)
|
||||
- [`GetCallstackHeight()` / `GetErrorCallstackHeight()` / `SetErrorCallstackHeight([height])`](#getcallstackheight--geterrorcallstackheight--seterrorcallstackheightheight)
|
||||
- [`collectgarbage(opt [, arg])`](#collectgarbageopt--arg)
|
||||
- [`table.wipe(t)` / `wipe(t)`](#tablewipet--wipet)
|
||||
- [`table.count(tbl)`](#tablecounttbl)
|
||||
- [`table.maxn(t)`](#tablemaxnt)
|
||||
- [`table.removemulti(t [, pos [, count]])`](#tableremovemultit--pos--count)
|
||||
- [Stale table lengths (5.1 healing)](#stale-table-lengths-51-healing)
|
||||
- [`Mixin(object, ...)` / `CreateFromMixins(...)`](#mixinobject---createfrommixins)
|
||||
- [`string.match` / `string.gmatch`](#stringmatch--stringgmatch)
|
||||
- [`string.gsub` table replacement](#stringgsub-table-replacement)
|
||||
- [`string.gsub` replacements](#stringgsub-replacements)
|
||||
- [`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)
|
||||
- [`string.byte(s [, i [, j]])` / `strbyte`](#stringbytes--i--j--strbyte)
|
||||
- [`strlenutf8(s)`](#strlenutf8s)
|
||||
- [`strcmputf8i(string1, string2)`](#strcmputf8istring1-string2)
|
||||
- [`strsplittable(sep, str [, pieces])`](#strsplittablesep-str--pieces)
|
||||
@@ -687,6 +700,7 @@ build instructions.
|
||||
- [`math.fmod(x, y)`](#mathfmodx-y)
|
||||
- [`math.modf(x)`](#mathmodfx)
|
||||
- [`math.huge`](#mathhuge)
|
||||
- [`math.cosh(x)` / `math.sinh(x)` / `math.tanh(x)`](#mathcoshx--mathsinhx--mathtanhx)
|
||||
- [`coroutine.create(fn)`](#coroutinecreatefn)
|
||||
- [`coroutine.resume(co, ...)`](#coroutineresumeco-)
|
||||
- [`coroutine.yield(...)`](#coroutineyield)
|
||||
@@ -1341,6 +1355,135 @@ end)
|
||||
C_ActionBar.EnableActionRangeCheck(1, true)
|
||||
```
|
||||
|
||||
### `C_ActionBar.GetSpell(actionID)`
|
||||
|
||||
Returns the ID of the spell that a 1-based action-bar slot casts. The
|
||||
answer depends on what the slot holds:
|
||||
|
||||
- A spell gives its own spell ID.
|
||||
- A macro gives the spell that its button shows. The button uses this
|
||||
spell for its cooldown, usable state and range.
|
||||
- An item gives the spell that the item casts when you use it.
|
||||
|
||||
If the slot is empty, has no spell, or is not in the range 1 to 120, the
|
||||
function returns `0`. If the client does not have the data of an item yet,
|
||||
the function also returns `0` for that item.
|
||||
|
||||
```lua
|
||||
local actionType, id = GetActionInfo(1)
|
||||
if actionType == "item" then
|
||||
id = C_ActionBar.GetSpell(1)
|
||||
end
|
||||
```
|
||||
|
||||
### `C_ActionBar.GetItemActionOnEquipSpellID(actionID)`
|
||||
|
||||
Returns the ID of the spell that the item of a 1-based action-bar slot
|
||||
gives while you wear it. The item is the item in the slot, or the item that
|
||||
a macro in the slot shows.
|
||||
|
||||
If the slot has no item, or the item has no such spell, the function
|
||||
returns nil. If the client does not have the data of the item yet, the
|
||||
function also returns nil.
|
||||
|
||||
```lua
|
||||
local spellID = C_ActionBar.GetItemActionOnEquipSpellID(1)
|
||||
if spellID then
|
||||
print("Equip effect", GetSpellInfo(spellID))
|
||||
end
|
||||
```
|
||||
|
||||
### `IsStackableAction(slot)` / `C_ActionBar.IsStackableAction(actionID)`
|
||||
|
||||
Tells if the item of a 1-based action-bar slot stacks. The item is the item
|
||||
in the slot, or the item that a macro in the slot shows. A potion stacks.
|
||||
A sword does not stack.
|
||||
|
||||
`IsStackableAction` returns 1 or nil. `C_ActionBar.IsStackableAction`
|
||||
returns `true` or `false`. A slot without an item gives nil or `false`.
|
||||
|
||||
```lua
|
||||
if C_ActionBar.IsStackableAction(1) then
|
||||
print("Count", GetActionCount(1))
|
||||
end
|
||||
```
|
||||
|
||||
### `C_ActionBar.IsMacroActionWithShowTooltip(actionID)`
|
||||
|
||||
Returns `true` if a 1-based action-bar slot holds a macro with a
|
||||
`#showtooltip` line. A macro with only `#show` gives `false`. Any other
|
||||
slot gives `false`.
|
||||
|
||||
```lua
|
||||
if C_ActionBar.IsMacroActionWithShowTooltip(1) then
|
||||
print("The button shows the tooltip of the macro spell or item")
|
||||
end
|
||||
```
|
||||
|
||||
### `C_ActionBar.FindSpellActionButtons(spell [, setToSearch])`
|
||||
|
||||
Returns a table of the 1-based action-bar slots that hold a spell. If no
|
||||
slot holds the spell, the function returns nothing.
|
||||
|
||||
`spell` is a spell ID, a spell link, or the name of a spell in your
|
||||
spellbook. A name gives the highest rank that you know.
|
||||
|
||||
A slot counts only when it holds the spell itself. A macro or an item that
|
||||
casts the spell does not count. Each rank is a different spell. Thus, a
|
||||
slot with Fireball rank 11 does not count for Fireball rank 10.
|
||||
|
||||
`setToSearch` is a value of [`Enum.ActionBarSet`](#enumactionbarset). The default is
|
||||
`Enum.ActionBarSet.All`. The client has only the `Mkb` set of slots. Thus,
|
||||
`Enum.ActionBarSet.Gamepad` finds no slot.
|
||||
|
||||
```lua
|
||||
local slots = C_ActionBar.FindSpellActionButtons(133)
|
||||
if slots then
|
||||
for _, slot in ipairs(slots) do
|
||||
print("Fireball on slot", slot)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `C_ActionBar.HasSpellActionButtons(spell [, setToSearch])`
|
||||
|
||||
Returns `true` if one or more action-bar slots hold a spell. The function
|
||||
uses the same rules as
|
||||
[`FindSpellActionButtons`](#c_actionbarfindspellactionbuttonsspell--settosearch).
|
||||
|
||||
```lua
|
||||
print(C_ActionBar.HasSpellActionButtons("Fireball"))
|
||||
```
|
||||
|
||||
### `C_ActionBar.IsOnBarOrSpecialBar(spell [, setToSearch])`
|
||||
|
||||
Returns `true` if a spell is on an action-bar slot, on the shapeshift bar,
|
||||
or on the pet bar. The action-bar slots use the same rules as
|
||||
[`FindSpellActionButtons`](#c_actionbarfindspellactionbuttonsspell--settosearch).
|
||||
`setToSearch` has an effect only on the action-bar slots.
|
||||
|
||||
The shapeshift bar holds stances, forms, auras and stealth. The pet bar
|
||||
counts only while you have a pet bar.
|
||||
|
||||
```lua
|
||||
-- Bear Form is on the shapeshift bar.
|
||||
print(C_ActionBar.IsOnBarOrSpecialBar(5487))
|
||||
```
|
||||
|
||||
### `Enum.ActionBarSet`
|
||||
|
||||
The sets of action-bar slots, for `setToSearch`.
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| `None` | 0 |
|
||||
| `Mkb` | 1 |
|
||||
| `Gamepad` | 2 |
|
||||
| `All` | 3 |
|
||||
|
||||
All 120 action-bar slots are in the `Mkb` set. No slot is in the `Gamepad`
|
||||
set.
|
||||
|
||||
## AddOns
|
||||
|
||||
`C_AddOns.*` getters that splat the
|
||||
@@ -3495,57 +3638,41 @@ for a wand without re-walking every other matching item.
|
||||
|
||||
### `C_Container.GetContainerNumFreeSlots(bagID)`
|
||||
|
||||
Returns the number of empty slots in the given bag, plus the bag's
|
||||
`BagFamily` bitfield (the type of items the bag is restricted to).
|
||||
Returns the number of empty slots in a bag, and the bag family of that
|
||||
bag. The bag family is a bitmask that shows which items the bag accepts.
|
||||
|
||||
```
|
||||
numberOfFreeSlots, bagType = C_Container.GetContainerNumFreeSlots(bagID)
|
||||
numFreeSlots, bagFamily = C_Container.GetContainerNumFreeSlots(bagID)
|
||||
```
|
||||
|
||||
- `bagID = 0` — the player's main backpack. Always reports
|
||||
`(freeCount, 0)` — the backpack is unfamilied (general-purpose).
|
||||
- `bagID = 1..4` — the player's equipped bag slots. Slot count and
|
||||
bagType come from the bag item's cached `ItemStats_C` record —
|
||||
`m_containerSlots` and `m_bagFamily` respectively. If no bag is
|
||||
equipped (or the item somehow isn't cached): `(0, 0)`.
|
||||
- Other `bagID` values (bank, keyring, out-of-range): `(0, 0)`. Always
|
||||
returns two values, never nil.
|
||||
- `bagID = 0`: the backpack. `bagFamily` is always `0`, because the
|
||||
backpack accepts all items.
|
||||
- `bagID = 1..4`: the equipped bags.
|
||||
- `bagID = -1`: the main bank. `bagFamily` is always `0`.
|
||||
- `bagID = 5..10`: the bank bags.
|
||||
|
||||
The main bank and the bank bags are available only while the bank window
|
||||
is open. If the bag is not there, the function returns only `0`, and
|
||||
`bagFamily` is `nil`. This includes the keyring, an empty bag slot, and
|
||||
the bank while the bank window is closed.
|
||||
|
||||
```lua
|
||||
local free, bagType = C_Container.GetContainerNumFreeSlots(0)
|
||||
-- free = number of empty slots in backpack, bagType = 0
|
||||
|
||||
for bag = 0, 4 do
|
||||
local free = C_Container.GetContainerNumFreeSlots(bag)
|
||||
-- ...
|
||||
local free, bagFamily = C_Container.GetContainerNumFreeSlots(1)
|
||||
if bagFamily == 0 then
|
||||
-- a general-purpose bag
|
||||
end
|
||||
```
|
||||
|
||||
> **`bagType` is derived for specialty bags.** Bags leave the raw
|
||||
> BagFamily field empty in Turtle's data (Soul Bag, Herb Bag, quiver,
|
||||
> ammo pouch, … all read 0). We fall back to deriving the family from the
|
||||
> bag's class + subclass, so a Soul Bag reports `0x4` here, a quiver
|
||||
> `0x1`, an ammo pouch `0x2` — the same values
|
||||
> [`C_Item.GetItemFamily`](#c_itemgetitemfamilyitem) reports for both
|
||||
> the bag and the items it holds. This also means
|
||||
> `CalculateTotalNumberOfFreeBagSlots` now correctly excludes specialty
|
||||
> bags from the general-purpose free-slot total (it previously counted
|
||||
> them, since the raw field was 0). Note the Turtle custom families
|
||||
> (Meat/Fish/Leather/Mining, `0x200`–`0x1000`) collide with the standard
|
||||
> bit meanings — see the `GetItemFamily` family table for the caveat.
|
||||
> **Specialty bags report their family.** A Soul Bag reports `0x4`, a
|
||||
> quiver reports `0x1`, and an ammo pouch reports `0x2`. These are the same
|
||||
> values that [`C_Item.GetItemFamily`](#c_itemgetitemfamilyitem) gives for
|
||||
> the bag and for the items that go in it. The Turtle bags (Meat, Fish,
|
||||
> Leather, Mining) use `0x200` to `0x1000`. These bits have a different
|
||||
> meaning in other clients. See the `GetItemFamily` family table.
|
||||
|
||||
> **The bitmask encoding.** `bagType` is the bit
|
||||
> position (`1 << (familyID - 1)`), not the raw stored familyID,
|
||||
> so callers can bitwise-AND with itemFamily values from
|
||||
> [`C_Item.GetItemFamily`](#c_itemgetitemfamilyitem) directly. We
|
||||
> convert internally — see that function's notes for the
|
||||
> encoding-shift story.
|
||||
|
||||
Implementation walks the bag using
|
||||
[`Item::Location::ResolveBag`](#c_containergetcontaineritemidbagindex-slotindex)
|
||||
internally and counts slots that resolve to a null `CGItem *` (i.e.,
|
||||
empty). Cross-checked in-game against a manual
|
||||
`C_Container.GetContainerItemID` walk; counts match.
|
||||
> **The encoding.** `bagFamily` is a bitmask (`1 << (familyID - 1)`), so
|
||||
> you can use `bit.band` to compare it with a value from
|
||||
> [`C_Item.GetItemFamily`](#c_itemgetitemfamilyitem).
|
||||
|
||||
### `C_Container.GetContainerFreeSlots(bagID)`
|
||||
|
||||
@@ -3556,10 +3683,11 @@ ascending order.
|
||||
freeSlots = C_Container.GetContainerFreeSlots(bagID)
|
||||
```
|
||||
|
||||
- `bagID = 0` — the player's main backpack.
|
||||
- `bagID = 1..4` — the player's equipped bag slots. If no bag is
|
||||
equipped there, the call returns nothing.
|
||||
- Other `bagID` values (bank, keyring, out of range) return nothing.
|
||||
- `bagID` takes the same values as
|
||||
[`C_Container.GetContainerNumFreeSlots`](#c_containergetcontainernumfreeslotsbagid).
|
||||
The main bank and the bank bags are available only while the bank
|
||||
window is open.
|
||||
- If the bag is not there, the call returns nothing.
|
||||
|
||||
A full bag returns an empty table. A bag that is not there returns
|
||||
nothing at all, so an empty table and a missing bag stay easy to tell
|
||||
@@ -3650,8 +3778,8 @@ An empty slot, and a slot in a bag you do not have, both report
|
||||
### `C_Container.CalculateTotalNumberOfFreeBagSlots()`
|
||||
|
||||
Returns the total number of free slots across the player's
|
||||
**general-purpose** bags — backpack plus any equipped bags 0..4 whose
|
||||
`BagFamily` is 0. Specialty bags (quivers, soul bags, herb bags, …) are
|
||||
**general-purpose** bags: the backpack, plus each equipped bag (1..4)
|
||||
whose bag family is 0. Specialty bags (quivers, soul bags, herb bags, …) are
|
||||
excluded, matching FrameXML's implementation:
|
||||
|
||||
```lua
|
||||
@@ -3665,12 +3793,9 @@ end
|
||||
|
||||
Takes no arguments.
|
||||
|
||||
> **Same sparse-`BagFamily` caveat** as
|
||||
> [`GetContainerNumFreeSlots`](#c_containergetcontainernumfreeslotsbagid):
|
||||
> because the server data leaves most bags' family field at 0,
|
||||
> specialty bags that would normally be excluded are counted here. The result
|
||||
> stays consistent with the per-bag `GetContainerNumFreeSlots` values it
|
||||
> sums.
|
||||
> The result is the sum of the per-bag
|
||||
> [`GetContainerNumFreeSlots`](#c_containergetcontainernumfreeslotsbagid)
|
||||
> values for the bags whose bag family is 0.
|
||||
|
||||
### `C_Container.IsContainerItemOpenable(containerIndex, slotIndex)`
|
||||
|
||||
@@ -12856,7 +12981,7 @@ Returned table:
|
||||
| `maxStackSize` | number | `m_stackable`. |
|
||||
| `maxCount` | number | `m_maxCount` — 0=unlimited, 1=unique, otherwise per-character cap. |
|
||||
| `containerSlots` | number | `m_containerSlots` — bag slot count, 0 for non-bags. |
|
||||
| `bagFamily` | number | `m_bagFamily` converted to a bitmask (`1 << (id-1)`). |
|
||||
| `bagFamily` | number | The bag family bitmask. The same value as [`C_Item.GetItemFamily`](#c_itemgetitemfamilyitem), bags included. |
|
||||
| `buyPrice` | number | Vendor buy price in copper. |
|
||||
| `sellPrice` | number | Vendor sell price in copper. |
|
||||
| `itemLevel` | number | Base ilvl from `ItemSparse`. |
|
||||
@@ -15912,6 +16037,44 @@ unpack({10, 20, 30}, 2, 2) -- 20
|
||||
length, so a vararg `arg` table's `n` field is honored and embedded nils
|
||||
keep their slots. Errors on a non-table first argument, exactly like 5.1.
|
||||
|
||||
### `newproxy([proxy])`
|
||||
|
||||
Returns a new empty userdata. The argument sets its metatable:
|
||||
|
||||
- `false` or no argument: the userdata has no metatable.
|
||||
- `true`: the userdata gets a new empty metatable. Use `getmetatable` to get
|
||||
this metatable and add metamethods to it.
|
||||
- A userdata from `newproxy(true)`: the new userdata shares the metatable of
|
||||
that userdata.
|
||||
|
||||
Any other argument causes the error `boolean or proxy expected`.
|
||||
|
||||
```lua
|
||||
local proxy = newproxy(true)
|
||||
getmetatable(proxy).__gc = function() print("collected") end
|
||||
local twin = newproxy(proxy) -- same metatable as proxy
|
||||
```
|
||||
|
||||
### `scrub(...)`
|
||||
|
||||
Returns its arguments. Each argument that is not a boolean, a number or a
|
||||
string becomes `nil`.
|
||||
|
||||
```lua
|
||||
scrub(1, "a", true, {}, print) -- 1, "a", true, nil, nil
|
||||
```
|
||||
|
||||
### `difftime(time2 [, time1])`
|
||||
|
||||
Returns the number of seconds from `time1` to `time2`. `time1` is 0 by
|
||||
default. Both values are times in seconds, as `time()` returns.
|
||||
|
||||
```lua
|
||||
local start = time()
|
||||
-- ...
|
||||
difftime(time(), start) -- the seconds since start
|
||||
```
|
||||
|
||||
### `xpcall(f, msgh [, arg1, ...])`
|
||||
|
||||
Calls `f` under the message handler `msgh`, passing the trailing arguments
|
||||
@@ -16047,6 +16210,21 @@ table.maxn({ a = 1 }) -- 0
|
||||
|
||||
Errors on non-table input.
|
||||
|
||||
### `table.removemulti(t [, pos [, count]])`
|
||||
|
||||
Removes `count` elements from `t`, starting at position `pos`, and returns
|
||||
the removed elements in order. The elements after them move down to fill the
|
||||
gap. `pos` is the last position by default, and `count` is 1 by default.
|
||||
|
||||
If `t` is empty, the function returns no values. If the range is not inside
|
||||
the table, the function causes the error `parameters out of bounds`.
|
||||
|
||||
```lua
|
||||
local t = { "a", "b", "c", "d", "e" }
|
||||
table.removemulti(t, 2, 3) -- "b", "c", "d"
|
||||
-- t is now { "a", "e" }
|
||||
```
|
||||
|
||||
### Stale table lengths (5.1 healing)
|
||||
|
||||
Lua 5.0 stores a table's `table.insert` / `table.getn` length **out of
|
||||
@@ -16133,29 +16311,51 @@ for word in string.gmatch("a,bb,ccc", "[^,]+") do print(word) end -- a / bb / cc
|
||||
Both call forms work: `string.match(s, p)` and `s:match(p)`. See
|
||||
[String methods](#string-methods-supper-sformat). `string.match` is also the
|
||||
global `strmatch`, as on 3.x clients, alongside 1.12's own `strfind`, `strsub`
|
||||
and the other globals for the 5.0 string functions.
|
||||
and the other globals for the 5.0 string functions. `string.gmatch` is also the
|
||||
global `gmatch`.
|
||||
|
||||
### `string.gsub` table replacement
|
||||
### `string.gsub` replacements
|
||||
|
||||
Lua 5.1 allows a **table** as `gsub`'s replacement: every match looks up
|
||||
the first capture (the whole match when the pattern declares no
|
||||
captures) as a key and substitutes the value. A `nil` or `false` value
|
||||
keeps the original match. The stock 5.0 `gsub` accepts only a string or
|
||||
function replacement and errors on a table
|
||||
(`string or function expected`).
|
||||
`string.gsub(s, pattern, repl [, n])` replaces each match of `pattern` in
|
||||
`s`. The replacement `repl` can be a string, a number, a table, or a
|
||||
function.
|
||||
|
||||
- **String or number**: the text replaces the match. In this text, `%0` is
|
||||
the whole match and `%1` to `%9` are the captures. If the pattern has no
|
||||
captures, `%1` is also the whole match. `%%` is one `%` character.
|
||||
- **Table**: the first capture is the key. If the pattern has no captures,
|
||||
the whole match is the key. The value at that key replaces the match.
|
||||
- **Function**: the function gets the captures as arguments. If the pattern
|
||||
has no captures, it gets the whole match. Its first return value replaces
|
||||
the match.
|
||||
|
||||
If a table value is `nil` or `false`, the original match stays in the
|
||||
string. Any other table value that is not a string or a number causes the
|
||||
error `invalid replacement value (a <type>)`.
|
||||
|
||||
If a function returns a value that is not a string or a number, the match is
|
||||
removed. This includes `nil` and `false`. To keep a match, return the match
|
||||
itself.
|
||||
|
||||
If `repl` is not one of the four types, the call fails with the error
|
||||
`string/function/table expected`.
|
||||
|
||||
```lua
|
||||
("hello world"):gsub("%w+", "<%0>")
|
||||
-- "<hello> <world>", 2
|
||||
|
||||
("$name the $class"):gsub("%$(%w+)", { name = "Bob", class = "Druid" })
|
||||
-- "Bob the Druid", 2
|
||||
|
||||
("$name and $unknown"):gsub("%$(%w+)", { name = "Bob" })
|
||||
-- "Bob and $unknown", 2 (a missing key keeps the match, as in 5.1)
|
||||
-- "Bob and $unknown", 2
|
||||
|
||||
("a b c"):gsub("%a", function(c) if c == "b" then return "B" end end)
|
||||
-- " B ", 3 (the function returns nil for "a" and "c")
|
||||
```
|
||||
|
||||
Both `string.gsub` and the bare `gsub` global have the upgrade; string
|
||||
and function replacements behave exactly as before. One residual: when
|
||||
the pattern itself contains a `%1`..`%9` back-reference, a missing table
|
||||
key removes the match instead of keeping it.
|
||||
`string.gsub`, the global `gsub`, and the method form `s:gsub(...)` are the
|
||||
same function.
|
||||
|
||||
### `strsplit(sep, str [, pieces])`
|
||||
|
||||
@@ -16241,6 +16441,25 @@ string.reverse("abc") -- "cba"
|
||||
strrev("hello") -- "olleh"
|
||||
```
|
||||
|
||||
### `string.byte(s [, i [, j]])` / `strbyte`
|
||||
|
||||
Returns the numeric codes of the bytes of `s` from position `i` to position
|
||||
`j`, as separate values. `i` is 1 by default, and `j` is `i` by default. A
|
||||
negative position counts back from the end of the string: -1 is the last
|
||||
byte. Positions outside the string are clamped to it. If the range is empty,
|
||||
the function returns no values.
|
||||
|
||||
The range can have at most about 2,000 bytes. A longer range causes the
|
||||
error `stack overflow (string slice too long)`.
|
||||
|
||||
```lua
|
||||
string.byte("ABC") -- 65
|
||||
string.byte("ABC", 2) -- 66
|
||||
string.byte("ABC", 1, -1) -- 65, 66, 67
|
||||
strbyte("ABC", -2, -1) -- 66, 67
|
||||
string.byte("ABC", 4) -- (no values)
|
||||
```
|
||||
|
||||
### `strlenutf8(s)`
|
||||
|
||||
The number of characters in a UTF-8 string. `strlen` counts bytes, so the
|
||||
@@ -16326,6 +16545,16 @@ local best = math.huge
|
||||
for _, v in ipairs(costs) do best = math.min(best, v) end
|
||||
```
|
||||
|
||||
### `math.cosh(x)` / `math.sinh(x)` / `math.tanh(x)`
|
||||
|
||||
The hyperbolic cosine, sine and tangent of `x`.
|
||||
|
||||
```lua
|
||||
math.cosh(0) -- 1
|
||||
math.sinh(0) -- 0
|
||||
math.tanh(1) -- 0.76159415595576
|
||||
```
|
||||
|
||||
### `coroutine.*`
|
||||
|
||||
The Lua 5.1 `coroutine` library: `create`, `resume`, `running`,
|
||||
@@ -23109,7 +23338,10 @@ returns a boolean.
|
||||
|
||||
- `unit` and `exactMatch` are the same as for `InteractUnit`. Use
|
||||
`"anyinteract"` for the object that the Interact Key uses.
|
||||
- `looseTargeting` has no effect.
|
||||
- `looseTargeting` is `true` if you do not give it. When it is `true` and
|
||||
you have a target, the interaction does not change your target. When it
|
||||
is `false`, or when you have no target, a unit that you interact with
|
||||
becomes your target.
|
||||
|
||||
Returns `true` if the interaction ran, or `false` if nothing matched.
|
||||
|
||||
@@ -23143,9 +23375,11 @@ If more than one object qualifies, it picks the nearest one.
|
||||
|
||||
- `"softinteract"` is the object that soft interact picked. It is empty
|
||||
when nothing qualifies or when soft interact is off.
|
||||
- `"anyinteract"` is your target when you can interact with it. This is
|
||||
true also when the target is behind you or too far away. If you cannot
|
||||
interact with your target, `"anyinteract"` is the same as
|
||||
- `"anyinteract"` is your target when you can interact with it now. This
|
||||
is true also when the target is behind you. When your target is too far
|
||||
away, `"anyinteract"` is the same as `"softinteract"`. If
|
||||
`"softinteract"` is empty, `"anyinteract"` is your target. When you
|
||||
cannot interact with your target, `"anyinteract"` is the same as
|
||||
`"softinteract"`.
|
||||
- `"anyleftinteract"` is the same as `"anyinteract"`.
|
||||
|
||||
|
||||
@@ -73,12 +73,14 @@ namespace Lua {
|
||||
F(GetTable, lua_gettable, LUA_GET_TABLE) \
|
||||
F(RawGet, lua_rawget, LUA_RAW_GET) \
|
||||
F(RawGetI, lua_rawgeti, LUA_RAWGETI) \
|
||||
F(RawSetI, lua_rawseti, LUA_RAW_SET_I) \
|
||||
F(SetTable, lua_settable, LUA_SET_TABLE) \
|
||||
F(RawSet, lua_rawset, LUA_RAW_SET) \
|
||||
F(GetMetatable,lua_getmetatable,LUA_GET_METATABLE)\
|
||||
F(SetMetatable,lua_setmetatable,LUA_SET_METATABLE)\
|
||||
F(Insert, lua_insert, LUA_INSERT) \
|
||||
F(Remove, lua_remove, LUA_REMOVE) \
|
||||
F(Replace, lua_replace, LUA_REPLACE) \
|
||||
F(GetTop, lua_gettop, LUA_GET_TOP) \
|
||||
F(SetTop, lua_settop, LUA_SET_TOP) \
|
||||
F(Call, lua_call, LUA_CALL) \
|
||||
@@ -98,6 +100,9 @@ namespace Lua {
|
||||
F(Resume, lua_resume, LUA_RESUME) \
|
||||
F(Yield, lua_yield, LUA_YIELD) \
|
||||
F(ArgError, luaL_argerror, LUAL_ARG_ERROR) \
|
||||
F(CheckLString,luaL_checklstring,LUAL_CHECK_LSTRING)\
|
||||
F(CheckNumber, luaL_checknumber,LUAL_CHECK_NUMBER)\
|
||||
F(OptNumber, luaL_optnumber, LUAL_OPT_NUMBER) \
|
||||
F(SetN, luaL_setn, LUAL_SETN) \
|
||||
F(Ref, luaL_ref, LUA_REF_REF) \
|
||||
F(Unref, luaL_unref, LUA_REF_UNREF) \
|
||||
|
||||
+14
@@ -270,6 +270,8 @@ using lua_rawget_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pushes t[n] for the table at idx, raw (no metamethods). With idx =
|
||||
// REGISTRY_INDEX it pushes a value stored under an integer ref.
|
||||
using lua_rawgeti_t = void(__fastcall *)(void *L, int idx, int n);
|
||||
// Pops the top value into t[n] for the table at idx, raw.
|
||||
using lua_rawseti_t = void(__fastcall *)(void *L, int idx, int n);
|
||||
using lua_settable_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_rawset_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pushes the metatable of the value at idx and returns 1, or pushes
|
||||
@@ -280,6 +282,8 @@ using lua_getmetatable_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_setmetatable_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_insert_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_remove_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pops the top value into slot idx.
|
||||
using lua_replace_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_gettop_t = int(__fastcall *)(void *L);
|
||||
using lua_settop_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_call_t = void(__fastcall *)(void *L, int nargs, int nresults);
|
||||
@@ -313,6 +317,11 @@ using lua_newthread_t = void *(__fastcall *)(void *L);
|
||||
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);
|
||||
// The auxlib argument readers: each raises the engine's own "bad argument"
|
||||
// error. CheckLString's `len` may be null.
|
||||
using luaL_checklstring_t = const char *(__fastcall *)(void *L, int narg, unsigned int *len);
|
||||
using luaL_checknumber_t = double(__fastcall *)(void *L, int narg);
|
||||
using luaL_optnumber_t = double(__fastcall *)(void *L, int narg, double def);
|
||||
using luaL_setn_t = void(__fastcall *)(void *L, int t, int n);
|
||||
// Pops the top value into the table at `t` under a fresh integer key and
|
||||
// returns the key (LUA_REFNIL, -1, for a nil). With REGISTRY_INDEX it anchors
|
||||
@@ -358,12 +367,14 @@ extern const lua_newtable_t NewTable;
|
||||
extern const lua_gettable_t GetTable;
|
||||
extern const lua_rawget_t RawGet;
|
||||
extern const lua_rawgeti_t RawGetI;
|
||||
extern const lua_rawseti_t RawSetI;
|
||||
extern const lua_settable_t SetTable;
|
||||
extern const lua_rawset_t RawSet;
|
||||
extern const lua_getmetatable_t GetMetatable;
|
||||
extern const lua_setmetatable_t SetMetatable;
|
||||
extern const lua_insert_t Insert;
|
||||
extern const lua_remove_t Remove;
|
||||
extern const lua_replace_t Replace;
|
||||
extern const lua_gettop_t GetTop;
|
||||
extern const lua_settop_t SetTop;
|
||||
extern const lua_call_t Call;
|
||||
@@ -383,6 +394,9 @@ extern const lua_newthread_t NewThread;
|
||||
extern const lua_resume_t Resume;
|
||||
extern const lua_yield_t Yield;
|
||||
extern const luaL_argerror_t ArgError;
|
||||
extern const luaL_checklstring_t CheckLString;
|
||||
extern const luaL_checknumber_t CheckNumber;
|
||||
extern const luaL_optnumber_t OptNumber;
|
||||
extern const luaL_setn_t SetN;
|
||||
extern const luaL_ref_t Ref;
|
||||
extern const luaL_unref_t Unref;
|
||||
|
||||
+68
-7
@@ -1920,6 +1920,10 @@ enum Offsets {
|
||||
// an aura-334 gate and skips the target for creatures with type flag
|
||||
// 0x20000000; 1.12 has neither. nampower detours it (its offsets.hpp).
|
||||
FUN_ON_SPRITE_RIGHT_CLICK = 0x00492820,
|
||||
// The object right-click virtual FUN_ON_SPRITE_RIGHT_CLICK calls after the
|
||||
// select: `void __thiscall(CGObject_C *, int modifier)`, the modifier
|
||||
// being FUN_GAMEUI_IS_KEY_DOWN(0) (`call [edi+0x60]` at 0x00492881).
|
||||
OFF_OBJECT_VTABLE_ON_RIGHT_CLICK = 0x60,
|
||||
// Current target-selection GUID (lo/hi), written by
|
||||
// `FUN_SET_SELECTION_BY_GUID` — 4.3.4's CGGameUI::m_lockedTarget. The tab
|
||||
// cycle compares it with its cursor entry; TargetDirection starts from it.
|
||||
@@ -5614,6 +5618,7 @@ enum Offsets {
|
||||
OFF_ITEMSTATS_SPELL_CATEGORY_CD = 0x180, // u32[5] — category cooldown ms
|
||||
ITEMSTATS_SPELL_SLOT_COUNT = 5,
|
||||
ITEM_SPELLTRIGGER_ON_USE = 0,
|
||||
ITEM_SPELLTRIGGER_ON_EQUIP = 1,
|
||||
|
||||
// `ItemStats_C.m_maxCount` — max number of this item the player can
|
||||
// hold in inventory at once. `0` = unlimited, `1` = unique, higher
|
||||
@@ -7135,6 +7140,13 @@ enum Offsets {
|
||||
// first slot test (`0x0062244F`: slot <= 0x12 and flags & 1). 4.3.4's
|
||||
// `Script_IsEquippedItemType` searches with exactly this flag.
|
||||
INVMGR_SEARCH_EQUIPMENT = 0x01,
|
||||
// Free-slot count — `uint32 __thiscall(invMgr)`, plain `ret`. On a
|
||||
// container's own inventory (`+0x10` byte clear) it counts every slot
|
||||
// 0..count-1 whose GUID is zero: 3.3.5's `CGContainer_C::GetFreeSlots`.
|
||||
// On the player's list it counts the backpack (23..38) plus, recursing,
|
||||
// each item-class-1 bag in 19..22, and skips equipment, bank and keyring
|
||||
// (disassembled 0x00622910-0x006229CA).
|
||||
FUN_INVMGR_COUNT_FREE_SLOTS = 0x00622910,
|
||||
// The registered action-bar Lua C functions that `Action::ItemState`
|
||||
// re-registers in front of. Tail-call these with the untouched stack for
|
||||
// every slot we don't own (they raise their own usage errors).
|
||||
@@ -7470,6 +7482,10 @@ enum Offsets {
|
||||
// `and eax,0xFFFF`.
|
||||
VAR_PET_ACTION_SLOTS = 0x00B71438,
|
||||
PET_ACTION_SLOT_COUNT = 10,
|
||||
// Types 1-5 are spells, the spell ID in the low 16 bits — the `case 1..5`
|
||||
// arm of Script_GetPetActionInfo (0x004BDC50), gated on VAR_PET_GUID.
|
||||
PET_ACTION_TYPE_SPELL_FIRST = 1,
|
||||
PET_ACTION_TYPE_SPELL_LAST = 5,
|
||||
PET_ACTION_TYPE_COMMAND = 7,
|
||||
PET_ACTION_COMMAND_ATTACK = 2,
|
||||
// The open stable master's GUID as a u64, 0 while no stable window is
|
||||
@@ -7840,6 +7856,10 @@ enum Offsets {
|
||||
// Number of spells on the shapeshift bar — the dword
|
||||
// `Script_GetNumShapeshiftForms` (0x004B4590) pushes.
|
||||
VAR_SHAPESHIFT_BAR_SPELL_COUNT = 0x00B71104,
|
||||
// Pointer to the shapeshift bar's u32 spell IDs, VAR_SHAPESHIFT_BAR_SPELL_
|
||||
// COUNT of them: `mov eax,[0xB71108]; mov esi,[eax+esi*4]` in
|
||||
// Script_GetShapeshiftFormInfo (0x004B4641) after its count bound.
|
||||
VAR_SHAPESHIFT_BAR_SPELLS = 0x00B71108,
|
||||
|
||||
// Inner spell-cast dispatcher — `__fastcall(slot, bookType, targetGuidLo,
|
||||
// targetGuidHi)`. `Script_CastSpellByName` calls this after resolving
|
||||
@@ -8340,6 +8360,9 @@ enum Offsets {
|
||||
// CObjects — which is exactly why the engine pushes handler/frame
|
||||
// through it rather than a plain rawget.
|
||||
LUA_RAWGETI = 0x6F3BC0,
|
||||
// `lua_rawseti(L, idx, n)` — __fastcall (n on the stack): t[n] = top,
|
||||
// pops it. Calls luaH_setnum (0x006FAD80); stamps the GC context tag.
|
||||
LUA_RAW_SET_I = 0x6F3F60,
|
||||
// `luaA_indexAcceptable(L, idx)` — the API's index walk: stack, pseudo
|
||||
// (registry / globals) or upvalue index → TObject*, or NULL for a
|
||||
// positive index at or past the top. __fastcall(L /*ecx*/, idx /*edx*/),
|
||||
@@ -8448,6 +8471,20 @@ enum Offsets {
|
||||
// `rawset(nil)` clear leaves it stale and subsequent `table.insert`
|
||||
// appends past the wiped slots.
|
||||
LUAL_SETN = 0x6F4EA0,
|
||||
// `luaL_addlstring(B, s, len)` — __fastcall (len on the stack, ret 4) —
|
||||
// and `luaL_addvalue(B)` — __fastcall, pops the string on top into B.
|
||||
// Both as called by str_gsub (0x007FD26A) and add_s (0x007FD35E).
|
||||
LUAL_ADD_LSTRING = 0x6F51F0,
|
||||
LUAL_ADD_VALUE = 0x6F5270,
|
||||
// `luaL_checklstring(L, narg, size_t *len)` — __fastcall (len on the
|
||||
// stack, ret 4; null is allowed). As str_gsub calls it (0x007FD0F7).
|
||||
LUAL_CHECK_LSTRING = 0x6F4BE0,
|
||||
// `luaL_optnumber(L, narg, double def)` — __fastcall (def on the stack,
|
||||
// ret 8), returns the double in ST0. As str_gsub calls it (0x007FD12F).
|
||||
LUAL_OPT_NUMBER = 0x6F4CD0,
|
||||
// `luaL_checknumber(L, narg)` — __fastcall, returns the double in ST0.
|
||||
// As str_format calls it (0x007FD570).
|
||||
LUAL_CHECK_NUMBER = 0x6F4C80,
|
||||
// `luaL_getn(L, tableIndex)` — Lua 5.0 auxlib table-length getter,
|
||||
// `int __fastcall(L /*ecx*/, idx /*edx*/)`, plain RET (no stack
|
||||
// args), length in eax. Verified by disassembly: a negative idx is
|
||||
@@ -8878,14 +8915,28 @@ enum Offsets {
|
||||
// entry before the registration table's NULL terminator, matching
|
||||
// stock 5.0 order). Verified by decode: args (s, pat, repl, maxN),
|
||||
// `^` anchor pre-strip, and the luaL_argerror(3) "string or
|
||||
// function expected" gate. Its add_value helper (`FUN_007fd2a0`)
|
||||
// matches stock 5.0: a FUNCTION replacement's non-string result is
|
||||
// popped and NOTHING is appended (match → empty) — 5.1 instead
|
||||
// keeps the original match for nil/false. That difference is why
|
||||
// `BaseLib::StringLib`'s 5.1 table-replacement shim wraps the
|
||||
// pattern in an outer capture (arg 1 = whole match) so its adapter
|
||||
// closure can hand the match back for missing keys.
|
||||
// function expected" gate. BaseLib::StringGsub replaces the gate and
|
||||
// the add_s call below.
|
||||
FUN_LUA_STR_GSUB = 0x007FD0E0,
|
||||
// str_gsub's replacement-type gate, ECX = EDI = L on entry: `call
|
||||
// lua_gettop; cmp eax,3; jl` → argerror, `lua_isstring(L,3)` → pass,
|
||||
// `lua_type(L,3) == 6` → pass, else `luaL_argerror(L, 3, "string or
|
||||
// function expected")`. 60 bytes, ending at 0x007FD19A `lea edx,[b]`
|
||||
// (luaL_buffinit). Nothing outside the range jumps into it.
|
||||
SITE_LUA_STR_GSUB_REPL_CHECK = 0x007FD15E,
|
||||
// `add_s(ms, b, s, e)` — __fastcall (ECX = MatchState, EDX = luaL_Buffer,
|
||||
// s/e on the stack, ret 8): Lua 5.0's add_s, which also does the
|
||||
// function replacement (a non-string result is popped and nothing is
|
||||
// added). Each `%n` goes through check_capture (0x007FCE20), so `%0`,
|
||||
// and `%1` in a pattern with no captures, raise "invalid capture index".
|
||||
// Its one caller is the call below; BaseLib::StringGsub still calls it
|
||||
// for a function replacement.
|
||||
FUN_LUA_STR_ADD_S = 0x007FD2A0,
|
||||
CALL_LUA_STR_GSUB_ADD_S = 0x007FD203,
|
||||
// `push_onecapture(ms, i)` — __fastcall, pushes capture `i`: "unfinished
|
||||
// capture" for length -1, the position for -2, else the substring. 5.0's
|
||||
// has no `i >= level` test (5.1's pushes the whole match for i == 0).
|
||||
FUN_LUA_STR_PUSH_ONECAPTURE = 0x007FCF40,
|
||||
// `str_format` — the Lua 5.0 `string.format` C function (`int
|
||||
// __fastcall(void *L)`), strlib entry at `0x00822dc0`. Verified by decode:
|
||||
// reads the format at arg 1, handles `%%`, `%d/i/c`, `%e/E/f/g/G`,
|
||||
@@ -8895,6 +8946,16 @@ enum Offsets {
|
||||
// — so an addon replacing `string.format` cannot hijack or break it (matches
|
||||
// retail, whose SetFormattedText formats in C).
|
||||
FUN_LUA_STR_FORMAT = 0x007FD3D0,
|
||||
// str_format's `%n$` argument number (Lua 4.0's one-digit form): after
|
||||
// the engine's isdigit(*ESI) passes, `cmp byte [esi+1],'$'; jnz +0C;
|
||||
// movsx eax,byte [esi]; sub eax,'0'; mov [ebp-8],eax; lea ebx,[esi+2]`
|
||||
// (80 7E 01 24 75 0C 0F BE 06 83 E8 30 89 45 F8 8D 5E 02), ending at
|
||||
// 0x007FD4BE `inc dword [ebp-8]`. ESI = the spec after '%', EBX = where
|
||||
// the flags scan starts, [ebp-8] = the argument number. EAX/ECX/EDX are
|
||||
// dead (0x007FD4C1 `or ecx,-1`, 0x007FD4CA `xor eax,eax`, 0x007FD4E2
|
||||
// `mov edx,ebx`). BaseLib::StringFormat replaces the 18 bytes.
|
||||
SITE_LUA_STR_FORMAT_ARG_NUMBER = 0x007FD4AC,
|
||||
LUA_STR_FORMAT_FRAME_ARG = 0x8,
|
||||
// `math_fmod` — the Lua 5.0 `math.mod` C function (mathlib entry at
|
||||
// `0x00822cb0`): `luaL_checknumber(1/2)` → `fmod(x, y)` → `lua_pushnumber`.
|
||||
// 5.1 renamed the Lua-facing name to `math.fmod` with the identical C
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// 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/>.
|
||||
|
||||
// Where a spell sits on the bars.
|
||||
//
|
||||
// FindSpellActionButtons(spell [, set]) the 1-based slots holding the
|
||||
// spell as a spell action, or nothing when none does.
|
||||
// HasSpellActionButtons(spell [, set]) whether any slot does.
|
||||
// IsOnBarOrSpecialBar(spell [, set]) the same, or the spell is on the
|
||||
// shapeshift bar or the pet bar.
|
||||
//
|
||||
// A slot matches on the spell ID it stores, as 4.3.4's PutActionInSlot
|
||||
// compares (IsSpell && GetSpell == spellId): a macro or an item that casts
|
||||
// the spell is not the spell, and a rank matches only itself. The special
|
||||
// bars are the engine's own lists — the shapeshift bar's spell array that
|
||||
// GetShapeshiftFormInfo reads, and the pet bar's spell entries
|
||||
// (GetPetActionInfo's types 1-5) while a pet bar is up.
|
||||
//
|
||||
// `set` is Enum.ActionBarSet, a mask whose Mkb bit is the one set of slots
|
||||
// this client has; Gamepad alone finds no slot.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "spell/Arg.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::Find {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int ACTION_BAR_SET_MKB = 1;
|
||||
constexpr int ACTION_BAR_SET_GAMEPAD = 2;
|
||||
constexpr int ACTION_BAR_SET_ALL = ACTION_BAR_SET_MKB | ACTION_BAR_SET_GAMEPAD;
|
||||
|
||||
// The spell at stack[1] and whether the set at stack[2] (default All)
|
||||
// covers this client's slots. 0 when the spell does not resolve.
|
||||
int Args(void *L, bool *searchSlots) {
|
||||
const int set = Game::Lua::IsNumber(L, 2) ? static_cast<int>(Game::Lua::ToNumber(L, 2))
|
||||
: ACTION_BAR_SET_ALL;
|
||||
*searchSlots = (set & ACTION_BAR_SET_MKB) != 0;
|
||||
return Spell::Arg::ResolveSpellID(L, 1);
|
||||
}
|
||||
|
||||
bool SlotHolds(int slot0, int spellID) {
|
||||
const uint32_t entry = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTION_TABLE))[slot0];
|
||||
return (entry & Offsets::ACTION_TYPE_MASK) == Offsets::ACTION_TYPE_SPELL &&
|
||||
entry == static_cast<uint32_t>(spellID);
|
||||
}
|
||||
|
||||
bool AnySlotHolds(int spellID) {
|
||||
for (int slot0 = 0; slot0 < Offsets::ACTION_TABLE_MAX_SLOTS; ++slot0) {
|
||||
if (SlotHolds(slot0, spellID))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OnShapeshiftBar(int spellID) {
|
||||
const int count = Game::Read<int>(Offsets::VAR_SHAPESHIFT_BAR_SPELL_COUNT);
|
||||
const auto *spells = Game::Read<const uint32_t *>(Offsets::VAR_SHAPESHIFT_BAR_SPELLS);
|
||||
if (spells == nullptr)
|
||||
return false;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (spells[i] == static_cast<uint32_t>(spellID))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OnPetBar(int spellID) {
|
||||
if (Game::Read<uint64_t>(Offsets::VAR_PET_GUID) == 0)
|
||||
return false;
|
||||
const auto *slots = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PET_ACTION_SLOTS));
|
||||
for (int i = 0; i < Offsets::PET_ACTION_SLOT_COUNT; ++i) {
|
||||
const uint32_t type = (slots[i] >> 24) & 0x3F;
|
||||
if (type >= Offsets::PET_ACTION_TYPE_SPELL_FIRST &&
|
||||
type <= Offsets::PET_ACTION_TYPE_SPELL_LAST &&
|
||||
(slots[i] & 0xFFFF) == static_cast<uint32_t>(spellID))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int __fastcall Script_FindSpellActionButtons(void *L) {
|
||||
bool searchSlots;
|
||||
const int spellID = Args(L, &searchSlots);
|
||||
if (spellID <= 0 || !searchSlots || !AnySlotHolds(spellID))
|
||||
return 0;
|
||||
Game::Lua::NewTable(L);
|
||||
int index = 0;
|
||||
for (int slot0 = 0; slot0 < Offsets::ACTION_TABLE_MAX_SLOTS; ++slot0) {
|
||||
if (!SlotHolds(slot0, spellID))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(++index));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot0 + 1));
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_HasSpellActionButtons(void *L) {
|
||||
bool searchSlots;
|
||||
const int spellID = Args(L, &searchSlots);
|
||||
Game::Lua::PushBool(L, spellID > 0 && searchSlots && AnySlotHolds(spellID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsOnBarOrSpecialBar(void *L) {
|
||||
bool searchSlots;
|
||||
const int spellID = Args(L, &searchSlots);
|
||||
Game::Lua::PushBool(L, spellID > 0 && ((searchSlots && AnySlotHolds(spellID)) ||
|
||||
OnShapeshiftBar(spellID) || OnPetBar(spellID)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("spellID", "SpellIdentifier", "A spell ID, spell link, or spell name."),
|
||||
Game::Doc::Opt("setToSearch", "ActionBarSet", "All"),
|
||||
};
|
||||
const Game::Doc::Field kFindRets[] = {
|
||||
Game::Doc::Req("slots", "table", "The 1-based action slots; nothing when no slot "
|
||||
"holds the spell."),
|
||||
};
|
||||
const Game::Doc::Function kFindSpellActionButtons{
|
||||
"The action slots that hold the spell as a spell action.",
|
||||
kArgs, kFindRets};
|
||||
|
||||
const Game::Doc::Field kHasRets[] = {
|
||||
Game::Doc::Req("hasSpellActionButtons", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kHasSpellActionButtons{
|
||||
"Whether any action slot holds the spell as a spell action.",
|
||||
kArgs, kHasRets};
|
||||
|
||||
const Game::Doc::Field kOnBarRets[] = {
|
||||
Game::Doc::Req("isOnBarOrSpecialBar", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsOnBarOrSpecialBar{
|
||||
"Whether the spell is in an action slot, on the shapeshift bar, or on the pet bar.",
|
||||
kArgs, kOnBarRets};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kActionBarSet[] = {
|
||||
{"None", 0},
|
||||
{"Mkb", ACTION_BAR_SET_MKB},
|
||||
{"Gamepad", ACTION_BAR_SET_GAMEPAD},
|
||||
{"All", ACTION_BAR_SET_ALL},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "FindSpellActionButtons",
|
||||
&Script_FindSpellActionButtons, &kFindSpellActionButtons);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "HasSpellActionButtons",
|
||||
&Script_HasSpellActionButtons, &kHasSpellActionButtons);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "IsOnBarOrSpecialBar",
|
||||
&Script_IsOnBarOrSpecialBar, &kIsOnBarOrSpecialBar);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ActionBarSet", kActionBarSet,
|
||||
static_cast<int>(sizeof(kActionBarSet) / sizeof(kActionBarSet[0])),
|
||||
"ActionBar");
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Action::Find
|
||||
@@ -38,11 +38,24 @@
|
||||
// through to an empty return. There is no bag-item tag: items are always
|
||||
// by-itemID (0x80), and an earlier reading of the 0x40 "miss" as a bag item
|
||||
// was wrong.
|
||||
//
|
||||
// `C_ActionBar.GetSpell(slot)` — the spell the slot stands for, 0 for none.
|
||||
// It is the engine's own resolver (FUN_ACTION_SLOT_TO_SPELL), which every
|
||||
// action reader (cooldown, usable, range, current) goes through: a spell
|
||||
// action is its spell, a macro is its cached primary spell, and an item is
|
||||
// its first on-use spell — retail's ActionButton.lua calls GetSpell for
|
||||
// exactly that item case. The spellbook flag it also hands back is not part
|
||||
// of the answer.
|
||||
//
|
||||
// `C_ActionBar.IsMacroActionWithShowTooltip(slot)` — the slot holds a macro
|
||||
// whose body carries `#showtooltip`, by the parse `Macro::ShowTooltip` hands
|
||||
// the tooltip over on. `#show` is not it.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "action/Slot.h"
|
||||
#include "equipmentset/Data.h"
|
||||
#include "macro/ShowTooltip.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -126,10 +139,58 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetSpell(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_ActionBar.GetSpell(actionID)");
|
||||
return 0;
|
||||
}
|
||||
// The resolver bounds-checks the slot itself and answers 0 outside it.
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
uint32_t petFlag = 0;
|
||||
const uint32_t spellID = reinterpret_cast<SlotToSpell_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_ACTION_SLOT_TO_SPELL))(
|
||||
static_cast<uint32_t>(slot0), &petFlag);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsMacroActionWithShowTooltip(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_ActionBar.IsMacroActionWithShowTooltip(actionID)");
|
||||
return 0;
|
||||
}
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
const uint32_t macroID = Action::Slot::MacroIDForSlot(slot0);
|
||||
Game::Lua::PushBool(L, macroID != 0 && Macro::ShowTooltip::HasShowTooltipDirective(macroID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kGetSpellArgs[] = {
|
||||
Game::Doc::Req("actionID", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kGetSpellRets[] = {
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetSpellDoc{
|
||||
"The spell an action stands for: a spell's own ID, a macro's spell, or an item's "
|
||||
"on-use spell; 0 when there is none.",
|
||||
kGetSpellArgs, kGetSpellRets};
|
||||
|
||||
const Game::Doc::Field kIsMacroActionWithShowTooltipRets[] = {
|
||||
Game::Doc::Req("isMacroActionWithShowTooltip", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsMacroActionWithShowTooltipDoc{
|
||||
"Whether the action is a macro with an explicit #showtooltip line.",
|
||||
kGetSpellArgs, kIsMacroActionWithShowTooltipRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetActionInfo", &Script_GetActionInfo);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "GetSpell", &Script_GetSpell, &kGetSpellDoc);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "IsMacroActionWithShowTooltip",
|
||||
&Script_IsMacroActionWithShowTooltip,
|
||||
&kIsMacroActionWithShowTooltipDoc);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The item behind an action slot, 4.3.4's CGActionBar::GetItem: an item-by-ID
|
||||
// action's item, or the item a macro's `#showtooltip` / `#show` resolved to
|
||||
// (1.12 keeps no item per macro, so `Macro::ShowTooltip` holds it). Both
|
||||
// readers below peek the item cache and never query it, as 4.3.4 does.
|
||||
//
|
||||
// IsStackableAction the item's max stack is more than 1 —
|
||||
// 4.3.4's IsStackableAucItem. The global
|
||||
// answers 1/nil, the C_ActionBar form a bool.
|
||||
// GetItemActionOnEquipSpellID the item's first on-equip spell (trigger 1),
|
||||
// nil when it has none.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Record.h"
|
||||
#include "macro/ShowTooltip.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::SlotItem {
|
||||
|
||||
namespace {
|
||||
|
||||
uint32_t ItemForSlot(int slot0) {
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return 0;
|
||||
const uint32_t entry = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTION_TABLE))[slot0];
|
||||
if ((entry & Offsets::ACTION_TYPE_MASK) == Offsets::ACTION_TYPE_ITEM_BY_ID)
|
||||
return entry & Offsets::ACTION_PAYLOAD_MASK_ITEM_BY_ID;
|
||||
Macro::ShowTooltip::Info info;
|
||||
if (Macro::ShowTooltip::ForSlot(slot0, &info) &&
|
||||
info.target == Macro::ShowTooltip::Target::Item)
|
||||
return static_cast<uint32_t>(info.itemID);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Reads the 1-based slot at stack[1]; false (after the usage error) when it
|
||||
// is not a number.
|
||||
bool SlotArg(void *L, const char *usage, int *slot0) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return false;
|
||||
}
|
||||
*slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsStackable(int slot0) {
|
||||
const uint8_t *record = ::Item::PeekRecord(ItemForSlot(slot0));
|
||||
return record != nullptr && Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_STACKABLE) > 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsStackableAction(void *L) {
|
||||
int slot0;
|
||||
if (!SlotArg(L, "Usage: IsStackableAction(slot)", &slot0))
|
||||
return 0;
|
||||
if (IsStackable(slot0))
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_C_IsStackableAction(void *L) {
|
||||
int slot0;
|
||||
if (!SlotArg(L, "Usage: C_ActionBar.IsStackableAction(actionID)", &slot0))
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, IsStackable(slot0));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetItemActionOnEquipSpellID(void *L) {
|
||||
int slot0;
|
||||
if (!SlotArg(L, "Usage: C_ActionBar.GetItemActionOnEquipSpellID(actionID)", &slot0))
|
||||
return 0;
|
||||
const uint8_t *record = ::Item::PeekRecord(ItemForSlot(slot0));
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; ++i) {
|
||||
const uint32_t spellID = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_ID + i * 4);
|
||||
const uint32_t trigger = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_TRIGGER + i * 4);
|
||||
if (spellID != 0 && trigger == Offsets::ITEM_SPELLTRIGGER_ON_EQUIP) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellID));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kSlotArgs[] = {
|
||||
Game::Doc::Req("slot", "number", "The action slot, 1 to 120."),
|
||||
};
|
||||
const Game::Doc::Field kIsStackableRets[] = {
|
||||
Game::Doc::Opt("isStackable", "number", nullptr,
|
||||
"1 if the slot's item stacks, else nil."),
|
||||
};
|
||||
const Game::Doc::Function kIsStackableAction{
|
||||
"Returns 1 if an action slot holds an item that stacks.",
|
||||
kSlotArgs, kIsStackableRets, "ActionBar"};
|
||||
|
||||
const Game::Doc::Field kActionIDArgs[] = {
|
||||
Game::Doc::Req("actionID", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kCIsStackableRets[] = {
|
||||
Game::Doc::Req("isStackableAction", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kCIsStackableAction{
|
||||
"Whether the action's item stacks.",
|
||||
kActionIDArgs, kCIsStackableRets};
|
||||
|
||||
const Game::Doc::Field kOnEquipRets[] = {
|
||||
Game::Doc::Opt("onEquipSpellID", "number", nullptr,
|
||||
"Nil when the action has no item or its item has no on-equip spell."),
|
||||
};
|
||||
const Game::Doc::Function kGetItemActionOnEquipSpellID{
|
||||
"The spell the action's item grants while equipped.",
|
||||
kActionIDArgs, kOnEquipRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("IsStackableAction", &Script_IsStackableAction,
|
||||
&kIsStackableAction);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "IsStackableAction",
|
||||
&Script_C_IsStackableAction, &kCIsStackableAction);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "GetItemActionOnEquipSpellID",
|
||||
&Script_GetItemActionOnEquipSpellID,
|
||||
&kGetItemActionOnEquipSpellID);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Action::SlotItem
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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/>.
|
||||
|
||||
// `difftime(t2 [, t1])` — the seconds from `t1` (0 by default) to `t2`, the
|
||||
// companion of the engine's `date` and `time` globals. 4.3.4's
|
||||
// Script_difftime: C `difftime` over the two arguments as time_t.
|
||||
//
|
||||
// Registered on BOTH states, as the engine registers `date` and `time`.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
namespace BaseLib::DiffTime {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_difftime(void *L) {
|
||||
const auto t2 = static_cast<time_t>(Game::Lua::CheckNumber(L, 1));
|
||||
const auto t1 = static_cast<time_t>(Game::Lua::OptNumber(L, 2, 0));
|
||||
Game::Lua::PushNumber(L, std::difftime(t2, t1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kDifftimeArgs[] = {
|
||||
Game::Doc::Req("time2", "number", "A time in seconds, as `time()` returns."),
|
||||
Game::Doc::Opt("time1", "number", "0"),
|
||||
};
|
||||
const Game::Doc::Field kDifftimeRets[] = {
|
||||
Game::Doc::Req("seconds", "number", "time2 minus time1."),
|
||||
};
|
||||
const Game::Doc::Function kDifftime{"The number of seconds from time1 to time2.",
|
||||
kDifftimeArgs, kDifftimeRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("difftime", &Script_difftime, &kDifftime);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("difftime", &Script_difftime, &kDifftime);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::DiffTime
|
||||
+22
-1
@@ -16,13 +16,16 @@
|
||||
// - `math.fmod(x, y)` — floating-point remainder of `x / y`.
|
||||
// - `math.modf(x)` — split into integral + fractional parts.
|
||||
// - `math.huge` — positive infinity constant.
|
||||
// - `math.cosh(x)`, `math.sinh(x)`, `math.tanh(x)` — hyperbolic functions.
|
||||
//
|
||||
// `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.
|
||||
// (not in 1.12's mathlib table), so we implement/define them here, as are the
|
||||
// hyperbolic functions (5.1's math_cosh/sinh/tanh: luaL_checknumber, the C
|
||||
// function, one number).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -53,9 +56,27 @@ int __fastcall Script_math_modf(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
int __fastcall Script_math_cosh(void *L) {
|
||||
Game::Lua::PushNumber(L, std::cosh(Game::Lua::CheckNumber(L, 1)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_math_sinh(void *L) {
|
||||
Game::Lua::PushNumber(L, std::sinh(Game::Lua::CheckNumber(L, 1)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_math_tanh(void *L) {
|
||||
Game::Lua::PushNumber(L, std::tanh(Game::Lua::CheckNumber(L, 1)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("math", "fmod", Script_math_fmod);
|
||||
Game::Lua::RegisterTableFunction("math", "modf", &Script_math_modf);
|
||||
Game::Lua::RegisterTableFunction("math", "cosh", &Script_math_cosh);
|
||||
Game::Lua::RegisterTableFunction("math", "sinh", &Script_math_sinh);
|
||||
Game::Lua::RegisterTableFunction("math", "tanh", &Script_math_tanh);
|
||||
|
||||
// `math.huge` — a numeric constant, not a function. Fetch the (now
|
||||
// guaranteed-present) `math` table and set the field directly.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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/>.
|
||||
|
||||
// `newproxy([false | true | proxy])` — Lua 5.1's empty userdata (luaB_newproxy,
|
||||
// lbaselib.c). `false` or nothing: no metatable. `true`: a new metatable of
|
||||
// its own. Another proxy: share that proxy's metatable. Only metatables made
|
||||
// by `newproxy(true)` count; any other argument raises "boolean or proxy
|
||||
// expected". 5.0 has userdata and their `__gc`, only not this function.
|
||||
//
|
||||
// As in 5.1, the closure's upvalue is a weak table that marks the valid
|
||||
// metatables (weak[m] = true), and is its own metatable with __mode "kv".
|
||||
//
|
||||
// Registered on BOTH states (language-level helper, both are 5.0).
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::NewProxy {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
int __fastcall Script_newproxy(void *L) {
|
||||
Lua::SetTop(L, 1);
|
||||
Lua::NewUserdata(L, 0); // the proxy, at 2
|
||||
if (Lua::ToBoolean(L, 1) == 0)
|
||||
return 1; // no metatable
|
||||
if (Lua::Type(L, 1) == Lua::TYPE_BOOLEAN) {
|
||||
Lua::NewTable(L); // a new metatable m ...
|
||||
Lua::PushValue(L, -1);
|
||||
Lua::PushBoolean(L, 1);
|
||||
Lua::RawSet(L, Lua::UpvalueIndex(1)); // ... marked valid: weak[m] = true
|
||||
} else {
|
||||
bool valid = false;
|
||||
if (Lua::GetMetatable(L, 1)) {
|
||||
Lua::RawGet(L, Lua::UpvalueIndex(1));
|
||||
valid = Lua::ToBoolean(L, -1) != 0;
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
if (!valid)
|
||||
Lua::ArgError(L, 1, "boolean or proxy expected");
|
||||
Lua::GetMetatable(L, 1);
|
||||
}
|
||||
Lua::SetMetatable(L, 2);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
void *L = Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
Lua::PushString(L, "newproxy");
|
||||
Lua::NewTable(L); // the weak table w
|
||||
Lua::PushString(L, "__mode");
|
||||
Lua::PushString(L, "kv");
|
||||
Lua::RawSet(L, -3);
|
||||
Lua::PushValue(L, -1);
|
||||
Lua::SetMetatable(L, -2); // w is its own metatable
|
||||
Lua::PushCClosure(L, &Script_newproxy, 1);
|
||||
Lua::SetTable(L, Lua::GLOBALS_INDEX);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::NewProxy
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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/>.
|
||||
|
||||
// `scrub(...)` — returns its arguments, each one that is not a boolean,
|
||||
// number or string replaced by nil. 4.3.4's Script_scrub.
|
||||
//
|
||||
// Registered on both Lua states, like the base-library helpers.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::Scrub {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
int __fastcall Script_scrub(void *L) {
|
||||
const int top = Lua::GetTop(L);
|
||||
Lua::CheckStack(L, 1);
|
||||
for (int i = 1; i <= top; i++) {
|
||||
const int type = Lua::Type(L, i);
|
||||
if (type != Lua::TYPE_BOOLEAN && type != Lua::TYPE_NUMBER && type != Lua::TYPE_STRING) {
|
||||
Lua::PushNil(L);
|
||||
Lua::Replace(L, i);
|
||||
}
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kScrubArgs[] = {
|
||||
Game::Doc::Vararg("any"),
|
||||
};
|
||||
const Game::Doc::Field kScrubRets[] = {
|
||||
Game::Doc::Vararg("any", "Each argument, or nil when it is not a boolean, number or string."),
|
||||
};
|
||||
const Game::Doc::Function kScrub{
|
||||
"Returns its arguments, with each one that is not a boolean, number or string replaced by nil.",
|
||||
kScrubArgs, kScrubRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("scrub", &Script_scrub, &kScrub);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("scrub", &Script_scrub, &kScrub);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::Scrub
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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/>.
|
||||
|
||||
// `string.format` takes two-digit argument numbers (`%10$s`).
|
||||
//
|
||||
// 1.12's str_format (0x7FD3D0) is Lua 4.0's: a spec may start with `n$`, but
|
||||
// only one digit, `isdigit(p[0]) && p[1] == '$'`. In "%10$s" the "10" is then
|
||||
// read as a width and the '$' as the conversion, which raises "invalid
|
||||
// option in `format'". 4.3.4's str_format adds a second branch for two
|
||||
// digits, `isdigit(p[1]) && p[2] == '$'`, and its combat log strings need it
|
||||
// (ACTION_SPELL_PERIODIC_LEECH_RESULT is "(%10$s %8$s Gained)").
|
||||
//
|
||||
// The thunk below stands in for the engine's one-digit branch and adds the
|
||||
// two-digit one. Everything else stays the engine's: its locale isdigit on
|
||||
// the first digit (already tested before the site), `arg++` after the site,
|
||||
// the flag/width/precision scan from EBX, and every conversion. Every caller
|
||||
// goes through it: `string.format`, `format`, and the C callers of
|
||||
// FUN_LUA_STR_FORMAT (Frame::FormattedText, Text::NumericFormatter).
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace BaseLib::StringFormat {
|
||||
|
||||
namespace {
|
||||
|
||||
static_assert(Offsets::LUA_STR_FORMAT_FRAME_ARG == 8, "ArgNumberThunk writes [ebp - 8]");
|
||||
|
||||
// ESI = the spec after '%', whose first byte is a digit. Sets the argument
|
||||
// number in [ebp - 8] and moves EBX past the `n$`; leaves both alone when
|
||||
// the spec has no `n$`.
|
||||
__declspec(naked) void ArgNumberThunk() {
|
||||
__asm {
|
||||
cmp byte ptr [esi + 1], '$'
|
||||
jne twoDigits
|
||||
movsx eax, byte ptr [esi]
|
||||
sub eax, '0'
|
||||
mov dword ptr [ebp - 8], eax
|
||||
lea ebx, [esi + 2]
|
||||
ret
|
||||
twoDigits:
|
||||
movzx eax, byte ptr [esi + 1]
|
||||
sub eax, '0'
|
||||
cmp eax, 9
|
||||
ja done
|
||||
cmp byte ptr [esi + 2], '$'
|
||||
jne done
|
||||
movsx ecx, byte ptr [esi]
|
||||
sub ecx, '0'
|
||||
lea ecx, [ecx + ecx * 4]
|
||||
lea eax, [eax + ecx * 2]
|
||||
mov dword ptr [ebp - 8], eax
|
||||
lea ebx, [esi + 3]
|
||||
done:
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// Runs on every registration pass; a site already patched no longer matches
|
||||
// and is skipped.
|
||||
void Apply() {
|
||||
static const uint8_t kOneDigit[18] = {0x80, 0x7E, 0x01, 0x24, 0x75, 0x0C, 0x0F, 0xBE, 0x06,
|
||||
0x83, 0xE8, 0x30, 0x89, 0x45, 0xF8, 0x8D, 0x5E, 0x02};
|
||||
Common::ReplaceWithCall(Offsets::SITE_LUA_STR_FORMAT_ARG_NUMBER, kOneDigit, sizeof kOneDigit,
|
||||
reinterpret_cast<const void *>(&ArgNumberThunk));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Apply};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&Apply};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::StringFormat
|
||||
@@ -0,0 +1,163 @@
|
||||
// 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/>.
|
||||
|
||||
// `string.gsub` takes 5.1's string and table replacements.
|
||||
//
|
||||
// 1.12's str_gsub (0x7FD0E0) is Lua 5.0's, and differs from 5.1 in its
|
||||
// replacement argument:
|
||||
// - it takes a string or a function, not a table;
|
||||
// - `%0` in a replacement string, and `%1` when the pattern has no
|
||||
// captures, raise "invalid capture index" (5.1: the whole match);
|
||||
// - a function that returns nil or false deletes the match (5.1 keeps it),
|
||||
// and one that returns a table deletes it too (5.1 raises an error).
|
||||
//
|
||||
// The first two only ever raised errors, so 5.1's behavior cannot break a
|
||||
// 1.12 addon. The third does: SuperAPI's CombatText_AddMessage returns nil
|
||||
// to delete the player's own " [0x...]" GUID from heal text. A function
|
||||
// replacement therefore stays the engine's own add_s.
|
||||
//
|
||||
// str_gsub itself stays: its match loop, anchor, count and buffer are the
|
||||
// same in both. Two parts are replaced:
|
||||
// - the argument gate (SITE_LUA_STR_GSUB_REPL_CHECK) becomes CheckReplacement;
|
||||
// - the one add_s call (CALL_LUA_STR_GSUB_ADD_S) goes to AddValue: 5.1's
|
||||
// add_s for a string or number, 5.1's table lookup for a table, and the
|
||||
// engine's add_s for a function. They use the engine's capture helpers.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace BaseLib::StringGsub {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
// Lua 5.0's MatchState, as str_gsub builds it on its stack (str_format's
|
||||
// spec scan reads the same layout: level at +0x0C, capture[0].len at +0x14).
|
||||
struct MatchState {
|
||||
const char *srcInit;
|
||||
const char *srcEnd;
|
||||
void *L;
|
||||
int level;
|
||||
struct {
|
||||
const char *init;
|
||||
int len;
|
||||
} capture[32];
|
||||
};
|
||||
|
||||
using add_s_t = void(__fastcall *)(MatchState *ms, void *b, const char *s, const char *e);
|
||||
using push_onecapture_t = void(__fastcall *)(MatchState *ms, int i);
|
||||
using luaL_addlstring_t = void(__fastcall *)(void *b, const char *s, unsigned int len);
|
||||
using luaL_addvalue_t = void(__fastcall *)(void *b);
|
||||
|
||||
const auto EngineAddS = reinterpret_cast<add_s_t>(Offsets::FUN_LUA_STR_ADD_S);
|
||||
const auto EnginePushOneCapture =
|
||||
reinterpret_cast<push_onecapture_t>(Offsets::FUN_LUA_STR_PUSH_ONECAPTURE);
|
||||
const auto AddLString = reinterpret_cast<luaL_addlstring_t>(Offsets::LUAL_ADD_LSTRING);
|
||||
const auto AddBufferValue = reinterpret_cast<luaL_addvalue_t>(Offsets::LUAL_ADD_VALUE);
|
||||
|
||||
// 5.1's push_onecapture: capture `i`, or the whole match for `i == 0` when
|
||||
// the pattern has no captures.
|
||||
void PushOneCapture(MatchState *ms, int i, const char *s, const char *e) {
|
||||
if (i < ms->level)
|
||||
EnginePushOneCapture(ms, i);
|
||||
else if (i == 0)
|
||||
Lua::PushLString(ms->L, s, static_cast<unsigned int>(e - s));
|
||||
else
|
||||
Lua::Error(ms->L, "invalid capture index");
|
||||
}
|
||||
|
||||
// 5.1's add_s: a replacement string, where `%0` is the whole match and `%1`
|
||||
// to `%9` are captures.
|
||||
void AddString(MatchState *ms, void *b, const char *s, const char *e) {
|
||||
void *L = ms->L;
|
||||
const char *news = Lua::ToString(L, 3);
|
||||
const unsigned int l = Lua::StrLen(L, 3);
|
||||
for (unsigned int i = 0; i < l; i++) {
|
||||
if (news[i] != '%') {
|
||||
AddLString(b, news + i, 1);
|
||||
continue;
|
||||
}
|
||||
i++; // skip the escape
|
||||
const char c = news[i];
|
||||
if (c < '0' || c > '9') {
|
||||
AddLString(b, news + i, 1);
|
||||
} else if (c == '0') {
|
||||
AddLString(b, s, static_cast<unsigned int>(e - s));
|
||||
} else {
|
||||
PushOneCapture(ms, c - '1', s, e);
|
||||
AddBufferValue(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5.1's add_value for a string or table, the engine's add_s for a function.
|
||||
// Called as add_s: __fastcall, ret 8.
|
||||
void __fastcall AddValue(MatchState *ms, void *b, const char *s, const char *e) {
|
||||
void *L = ms->L;
|
||||
switch (Lua::Type(L, 3)) {
|
||||
case Lua::TYPE_FUNCTION:
|
||||
// A result that is not a string or number deletes the match.
|
||||
EngineAddS(ms, b, s, e);
|
||||
return;
|
||||
case Lua::TYPE_TABLE:
|
||||
break;
|
||||
default: // number or string
|
||||
AddString(ms, b, s, e);
|
||||
return;
|
||||
}
|
||||
PushOneCapture(ms, 0, s, e);
|
||||
Lua::GetTable(L, 3);
|
||||
if (!Lua::ToBoolean(L, -1)) {
|
||||
Lua::SetTop(L, -2);
|
||||
Lua::PushLString(L, s, static_cast<unsigned int>(e - s)); // keep the match
|
||||
} else if (!Lua::IsString(L, -1)) {
|
||||
Lua::Error(L, "invalid replacement value (a %s)", Lua::TypeName(L, Lua::Type(L, -1)));
|
||||
}
|
||||
AddBufferValue(b);
|
||||
}
|
||||
|
||||
// 5.1's gate on the replacement argument. Called in place of the engine's,
|
||||
// with ECX = L.
|
||||
void __fastcall CheckReplacement(void *L) {
|
||||
const int t = Lua::Type(L, 3);
|
||||
if (t != Lua::TYPE_NUMBER && t != Lua::TYPE_STRING && t != Lua::TYPE_FUNCTION &&
|
||||
t != Lua::TYPE_TABLE)
|
||||
Lua::ArgError(L, 3, "string/function/table expected");
|
||||
}
|
||||
|
||||
// Runs on every registration pass; a site already patched no longer matches
|
||||
// and is skipped.
|
||||
void Apply() {
|
||||
static const uint8_t kGate[60] = {
|
||||
0xE8, 0x0D, 0x5F, 0xEF, 0xFF, 0x83, 0xF8, 0x03, 0x7C, 0x21, 0xBA, 0x03,
|
||||
0x00, 0x00, 0x00, 0x8B, 0xCF, 0xE8, 0x9C, 0x63, 0xEF, 0xFF, 0x85, 0xC0,
|
||||
0x75, 0x22, 0xBA, 0x03, 0x00, 0x00, 0x00, 0x8B, 0xCF, 0xE8, 0x7C, 0x62,
|
||||
0xEF, 0xFF, 0x83, 0xF8, 0x06, 0x74, 0x11, 0x68, 0x84, 0x23, 0x88, 0x00,
|
||||
0xBA, 0x03, 0x00, 0x00, 0x00, 0x8B, 0xCF, 0xE8, 0x76, 0x76, 0xEF, 0xFF,
|
||||
};
|
||||
Common::ReplaceWithCall(Offsets::SITE_LUA_STR_GSUB_REPL_CHECK, kGate, sizeof kGate,
|
||||
reinterpret_cast<const void *>(&CheckReplacement));
|
||||
Common::RedirectCall(Offsets::CALL_LUA_STR_GSUB_ADD_S, Offsets::FUN_LUA_STR_ADD_S,
|
||||
reinterpret_cast<const void *>(&AddValue));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Apply};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&Apply};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::StringGsub
|
||||
+38
-124
@@ -15,13 +15,13 @@
|
||||
//
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction (5.1),
|
||||
// also the global `strmatch`, as `string.find` is `strfind`.
|
||||
// - `string.gmatch(s, pattern)` — match iterator (5.1).
|
||||
// - `string.gsub` TABLE replacement — 5.1 form (see the shim below).
|
||||
// - `string.gmatch(s, pattern)` — match iterator (5.1), also `gmatch`.
|
||||
// - `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.
|
||||
// - `string.byte(s, i, j)` / `strbyte` — 5.1's range form (5.0 reads only i).
|
||||
// - `strlenutf8(s)` — WoW global; characters, not bytes.
|
||||
// - `strcmputf8i(a, b)` — WoW global; case-insensitive UTF-8 order.
|
||||
// - `strsplittable(sep, str [, pieces])`— WoW global; `strsplit` into a table.
|
||||
@@ -341,126 +341,39 @@ int __fastcall Script_string_reverse(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- string.gsub table-replacement upgrade (Lua 5.1) ------------------------
|
||||
//
|
||||
// 5.1 allows a TABLE as gsub's replacement: each match looks up the first
|
||||
// capture (the whole match when the pattern declares no captures) in the
|
||||
// table; a nil/false value keeps the original match; string/number values
|
||||
// substitute. This engine's gsub (FUN_LUA_STR_GSUB) is stock 5.0 —
|
||||
// string/function only ("string or function expected" otherwise), and its
|
||||
// add_value appends NOTHING when a replacement function returns a
|
||||
// non-string (match → empty; verified by decode, see Offsets.h). So a
|
||||
// plain table→closure adapter would EAT unmatched tokens where 5.1 keeps
|
||||
// them, and the closure can't reconstruct the whole match from capture
|
||||
// args alone.
|
||||
//
|
||||
// The shim therefore rewrites the pattern with one OUTER capture —
|
||||
// `pat` → `(pat)`, keeping a leading `^` and an unescaped trailing `$`
|
||||
// outside (both are anchors only at their positional ends; inside the
|
||||
// parens they'd turn literal) — so the adapter closure always receives
|
||||
// the whole match as arg 1, the pattern's own first capture as arg 2, and
|
||||
// can hand arg 1 back to reproduce 5.1's keep-match for missing keys.
|
||||
// Patterns containing a `%1`..`%9` back-reference can't be wrapped (the
|
||||
// outer capture would renumber what `%N` refers to); those fall back to
|
||||
// an unwrapped adapter whose missing-key behavior is the engine's own
|
||||
// (match → empty) — the one documented divergence, and the combination
|
||||
// (table replacement + pattern back-references) is essentially absent
|
||||
// from real code. String/function replacements tail-call the engine gsub
|
||||
// untouched.
|
||||
// `string.byte(s [, i [, j]])` / `strbyte` — the codes of bytes i..j, as
|
||||
// 4.3.4's str_byte (j defaults to i). 1.12's (0x7FC210) is 5.0's: it reads
|
||||
// only `i` and pushes one code. With `j` absent the two give the same
|
||||
// result for every `i`. Positions truncate toward zero as the engine's
|
||||
// str_byte and str_sub do. The engine's lua_checkstack (0x6F2F30) refuses
|
||||
// more than 2048 stack slots, so the longest range is about 2000 bytes.
|
||||
|
||||
// True when the pattern contains a `%1`..`%9` back-reference. Escape-aware:
|
||||
// the char after any `%` is consumed, so `%%1` (literal percent, then '1')
|
||||
// doesn't count.
|
||||
bool PatternHasBackref(const char *p, unsigned len) {
|
||||
for (unsigned i = 0; i + 1 < len; ++i) {
|
||||
if (p[i] != '%')
|
||||
continue;
|
||||
const char c = p[i + 1];
|
||||
if (c >= '1' && c <= '9')
|
||||
return true;
|
||||
++i; // skip the escaped char
|
||||
}
|
||||
return false;
|
||||
// 5.1's posrelat: a negative position counts from the end; before the start is 0.
|
||||
long PosRelat(long pos, long len) {
|
||||
if (pos < 0)
|
||||
pos += len + 1;
|
||||
return pos >= 0 ? pos : 0;
|
||||
}
|
||||
|
||||
// Adapter for the wrapped-pattern path: arg 1 = whole match, arg 2 = the
|
||||
// pattern's own first capture (when it declared any). Upvalue 1 = the
|
||||
// replacement table. Missing/false value → return the whole match (5.1's
|
||||
// keep-match).
|
||||
int __fastcall GsubTableLookupWrapped(void *L) {
|
||||
const int nargs = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushValue(L, nargs >= 2 ? 2 : 1); // the 5.1 lookup key
|
||||
Game::Lua::GetTable(L, Game::Lua::UpvalueIndex(1)); // tbl[key]
|
||||
const int t = Game::Lua::Type(L, -1);
|
||||
if (t == Game::Lua::TYPE_NIL ||
|
||||
(t == Game::Lua::TYPE_BOOLEAN && Game::Lua::ToBoolean(L, -1) == 0))
|
||||
Game::Lua::PushValue(L, 1); // keep the original match
|
||||
return 1; // top value is the result
|
||||
}
|
||||
|
||||
// Adapter for back-reference patterns (unwrapped): arg 1 is already the
|
||||
// 5.1 key. A missing key returns nil → the engine appends nothing.
|
||||
int __fastcall GsubTableLookupPlain(void *L) {
|
||||
Game::Lua::PushValue(L, 1);
|
||||
Game::Lua::GetTable(L, Game::Lua::UpvalueIndex(1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_string_gsub(void *L) {
|
||||
const auto engineGsub =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_GSUB);
|
||||
if (Game::Lua::Type(L, 3) != Game::Lua::TYPE_TABLE)
|
||||
return engineGsub(L); // string/function/error paths untouched
|
||||
|
||||
const char *pat = Game::Lua::ToString(L, 2);
|
||||
if (pat == nullptr)
|
||||
return engineGsub(L); // let the engine raise its own arg error
|
||||
const unsigned patLen = Game::Lua::StrLen(L, 2);
|
||||
|
||||
Game::Lua::SetTop(L, 4); // normalize to (s, pat, tbl, n) — gsub reads ≤4
|
||||
|
||||
if (!PatternHasBackref(pat, patLen)) {
|
||||
// Build `(body)` with the anchors outside the capture.
|
||||
unsigned b = 0, e = patLen;
|
||||
std::string wrapped;
|
||||
wrapped.reserve(patLen + 3);
|
||||
if (e > b && pat[0] == '^') {
|
||||
wrapped += '^';
|
||||
b = 1;
|
||||
}
|
||||
bool anchoredBack = false;
|
||||
if (e > b && pat[e - 1] == '$') {
|
||||
// `$` is an anchor only when not %-escaped: an even number of
|
||||
// `%` immediately before it means the `$` itself is unescaped.
|
||||
unsigned pc = 0;
|
||||
for (int idx = static_cast<int>(e) - 2;
|
||||
idx >= static_cast<int>(b) && pat[idx] == '%'; --idx)
|
||||
++pc;
|
||||
anchoredBack = (pc % 2) == 0;
|
||||
if (anchoredBack)
|
||||
--e;
|
||||
}
|
||||
wrapped += '(';
|
||||
wrapped.append(pat + b, e - b);
|
||||
wrapped += ')';
|
||||
if (anchoredBack)
|
||||
wrapped += '$';
|
||||
|
||||
Game::Lua::PushLString(L, wrapped.data(),
|
||||
static_cast<unsigned int>(wrapped.size())); // 5
|
||||
Game::Lua::PushValue(L, 3); // 6: tbl
|
||||
Game::Lua::PushCClosure(L, &GsubTableLookupWrapped, 1); // 6: closure
|
||||
Game::Lua::Insert(L, 2); // closure→2: s, closure, pat, tbl, n, wrapped
|
||||
Game::Lua::Insert(L, 2); // wrapped→2: s, wrapped, closure, pat, tbl, n
|
||||
Game::Lua::Remove(L, 4); // drop the original pattern
|
||||
Game::Lua::Remove(L, 4); // drop the table → (s, wrapped, closure, n)
|
||||
} else {
|
||||
Game::Lua::PushValue(L, 3); // 5: tbl
|
||||
Game::Lua::PushCClosure(L, &GsubTableLookupPlain, 1); // 5: closure
|
||||
Game::Lua::Insert(L, 3); // → s, pat, closure, tbl, n
|
||||
Game::Lua::Remove(L, 4); // → s, pat, closure, n
|
||||
}
|
||||
return engineGsub(L);
|
||||
int __fastcall Script_string_byte(void *L) {
|
||||
unsigned int l = 0;
|
||||
const char *s = Game::Lua::CheckLString(L, 1, &l);
|
||||
const long len = static_cast<long>(l);
|
||||
long posi = PosRelat(static_cast<long>(Game::Lua::OptNumber(L, 2, 1)), len);
|
||||
long pose =
|
||||
PosRelat(static_cast<long>(Game::Lua::OptNumber(L, 3, static_cast<double>(posi))), len);
|
||||
if (posi <= 0)
|
||||
posi = 1;
|
||||
if (pose > len)
|
||||
pose = len;
|
||||
if (posi > pose)
|
||||
return 0;
|
||||
const int n = static_cast<int>(pose - posi + 1);
|
||||
if (!Game::Lua::CheckStack(L, n))
|
||||
Game::Lua::Error(L, "stack overflow (%s)", "string slice too long");
|
||||
for (int i = 0; i < n; i++)
|
||||
Game::Lua::PushNumber(L, static_cast<unsigned char>(s[posi + i - 1]));
|
||||
return n;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
@@ -515,27 +428,28 @@ const Game::Doc::Function kStrconcat{
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "match", &Script_string_match);
|
||||
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch);
|
||||
Game::Lua::RegisterTableFunction("string", "gsub", &Script_string_gsub);
|
||||
Game::Lua::RegisterTableFunction("string", "reverse", &Script_string_reverse);
|
||||
Game::Lua::RegisterTableFunction("string", "byte", &Script_string_byte);
|
||||
Game::Lua::RegisterTableFunction("string", "split", &Script_strsplit);
|
||||
Game::Lua::RegisterTableFunction("string", "trim", &Script_strtrim);
|
||||
Game::Lua::RegisterTableFunction("string", "join", &Script_strjoin);
|
||||
Game::Lua::RegisterTableFunction("string", "concat", &Script_strconcat, &kStrconcat);
|
||||
// 4.3.4's compat.lua binds each short global and its string-table twin
|
||||
// to ONE function by assignment (`gsub = str.gsub`, `strmatch =
|
||||
// to ONE function by assignment (`gmatch = str.gmatch`, `strmatch =
|
||||
// str.match`, `strrev = str.reverse`, `str.trim = strtrim`, `str.split =
|
||||
// strsplit`, `str.join = strjoin`), so `strtrim == string.trim`. A second
|
||||
// Register* call would mint a second closure over the same C function, so
|
||||
// the globals alias the table entries by value instead. The string-table
|
||||
// half is what lets `(" x "):trim()` work and libraries capture it at
|
||||
// file scope (AceConfigCmd-3.0: `strtrim = string.trim`); the bare `gsub`
|
||||
// gets the table-replacement upgrade by being the same function.
|
||||
Game::Lua::RegisterGlobalAlias("gsub", "string", "gsub");
|
||||
// file scope (AceConfigCmd-3.0: `strtrim = string.trim`). 1.12's own
|
||||
// compat.lua set `strbyte` from the old string.byte, so it is re-aliased.
|
||||
Game::Lua::RegisterGlobalAlias("gmatch", "string", "gmatch");
|
||||
Game::Lua::RegisterGlobalAlias("strmatch", "string", "match");
|
||||
Game::Lua::RegisterGlobalAlias("strsplit", "string", "split");
|
||||
Game::Lua::RegisterGlobalAlias("strjoin", "string", "join");
|
||||
Game::Lua::RegisterGlobalAlias("strtrim", "string", "trim");
|
||||
Game::Lua::RegisterGlobalAlias("strrev", "string", "reverse");
|
||||
Game::Lua::RegisterGlobalAlias("strbyte", "string", "byte");
|
||||
Game::Lua::RegisterGlobalAlias("strconcat", "string", "concat", &kStrconcat);
|
||||
Game::Lua::RegisterGlobalFunction("strreplace", &Script_strreplace);
|
||||
Game::Lua::RegisterGlobalFunction("strlenutf8", &Script_strlenutf8, &kStrlenutf8);
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "console/Registry.h"
|
||||
#include "input/Modifier.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -63,7 +64,6 @@ namespace {
|
||||
using Handler_t = int(__fastcall *)(const void *data, void *param);
|
||||
using RegisterEx_t = void(__fastcall *)(int eventId, Handler_t handler, void *param,
|
||||
float priority);
|
||||
using IsKeyDown_t = int(__fastcall *)(int key);
|
||||
using GetInputLine_t = uintptr_t(__fastcall *)();
|
||||
using MakeCommandCurrent_t = void(__fastcall *)(uintptr_t line, const char *text);
|
||||
using SetInputString_t = void(__fastcall *)(const char *text);
|
||||
@@ -205,7 +205,7 @@ int __fastcall OnKeyDown(const void *data, void * /*param*/) {
|
||||
return 1;
|
||||
if (Game::Read<uint32_t>(Offsets::VAR_CONSOLE_VISIBLE) == 0)
|
||||
return 1;
|
||||
if (reinterpret_cast<IsKeyDown_t>(Offsets::FUN_GAMEUI_IS_KEY_DOWN)(toggleKey) != 0)
|
||||
if (Input::Modifier::IsKeyDown(toggleKey))
|
||||
return 1;
|
||||
|
||||
const uintptr_t line =
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// 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 "container/Bag.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "equipmentset/Locations.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
namespace Container::Bag {
|
||||
|
||||
namespace {
|
||||
|
||||
bool PlayerRange(int first, int count, View *out) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, first, count, 0};
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The main bank resolves only while a banker is open — 3.3.5 gates it on
|
||||
// `CGBankInfo::m_banker` the same way. Its GUIDs sit in the player's list
|
||||
// from login, but the server refuses every bank move without a banker and
|
||||
// the engine's own slot reader (`0x6228a0`) hides slots 39..68 on the same
|
||||
// banker-GUID test. Bank bags get that gate from `BagContainer`, whose
|
||||
// engine GUID getter answers 0 for them while the bank is closed.
|
||||
bool ViewFor(int bagID, View *out) {
|
||||
if (bagID == Offsets::BAG_INDEX_BACKPACK)
|
||||
return PlayerRange(Offsets::INVMGR_BACKPACK_FIRST_SLOT,
|
||||
Offsets::BACKPACK_NUM_SLOTS, out);
|
||||
if (bagID == Offsets::BAG_INDEX_BANK) {
|
||||
if (!EquipmentSet::Locations::BankerOpen())
|
||||
return false;
|
||||
return PlayerRange(Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT -
|
||||
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT + 1,
|
||||
out);
|
||||
}
|
||||
|
||||
const uint8_t *bag = Item::Location::BagContainer(bagID);
|
||||
auto *inv = static_cast<const uint8_t *>(Item::Location::ContainerInventory(bag));
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
const int slots = static_cast<int>(
|
||||
Game::Read<uint32_t>(inv, Offsets::OFF_INVMGR_SLOT_COUNT));
|
||||
if (slots <= 0)
|
||||
return false;
|
||||
|
||||
// The bag's own family, as 3.3.5's `CGItem_C::GetBagFamily` reads it (0
|
||||
// while its record isn't cached). `BitmaskForRecord` supplies the family
|
||||
// a vanilla bag's empty field leaves out, so a quiver reports one rather
|
||||
// than reading as general-purpose, and agrees with `C_Item.GetItemFamily`.
|
||||
uint32_t family = 0;
|
||||
const int bagItemID = Item::ID::FromCGItem(bag);
|
||||
if (bagItemID > 0) {
|
||||
if (auto *rec = Item::PeekRecord(static_cast<uint32_t>(bagItemID)))
|
||||
family = Item::BagFamily::BitmaskForRecord(rec);
|
||||
}
|
||||
*out = {inv, 0, slots, family};
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t SlotGuid(const View &view, int slotIndex0) {
|
||||
const uint32_t linear = static_cast<uint32_t>(view.firstLinear + slotIndex0);
|
||||
if (linear >= Game::Read<uint32_t>(view.invMgr, Offsets::OFF_INVMGR_SLOT_COUNT))
|
||||
return 0;
|
||||
auto *guids = Game::Read<const uint64_t *>(view.invMgr, Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
return guids != nullptr ? guids[linear] : 0;
|
||||
}
|
||||
|
||||
int CountFree(const View &view) {
|
||||
if (view.invMgr != Unit::Identity::PlayerInventoryManager()) {
|
||||
using CountFreeSlots_t = uint32_t(__thiscall *)(const uint8_t *invMgr);
|
||||
auto countFreeSlots =
|
||||
reinterpret_cast<CountFreeSlots_t>(Offsets::FUN_INVMGR_COUNT_FREE_SLOTS);
|
||||
return static_cast<int>(countFreeSlots(view.invMgr));
|
||||
}
|
||||
int free = 0;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
if (SlotGuid(view, i) == 0)
|
||||
free++;
|
||||
}
|
||||
return free;
|
||||
}
|
||||
|
||||
} // namespace Container::Bag
|
||||
@@ -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
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Container::Bag {
|
||||
|
||||
// A container's storage, addressed without Lua: the inventory list holding
|
||||
// its slot GUIDs, the linear index its first slot sits at, its slot count,
|
||||
// and the BagFamily bitmask it accepts (0 = anything). The backpack and the
|
||||
// main bank live inside the player's own inventory list at different linear
|
||||
// bases; equipped bags and bank bags each have a list of their own.
|
||||
struct View {
|
||||
const uint8_t *invMgr;
|
||||
int firstLinear;
|
||||
int slotCount;
|
||||
uint32_t family;
|
||||
};
|
||||
|
||||
// Resolves Lua container `bagID` — backpack 0, bags 1..4, main bank -1, bank
|
||||
// bags 5..10 — to its `View`. False for the keyring, an out-of-range ID, an
|
||||
// empty bag slot, and any bank container while no banker is open. Pure C — no
|
||||
// Lua stack.
|
||||
bool ViewFor(int bagID, View *out);
|
||||
|
||||
// GUID of the item in 0-based slot `slotIndex0` of `view`, 0 when empty.
|
||||
uint64_t SlotGuid(const View &view, int slotIndex0);
|
||||
|
||||
// Empty slots in `view`. A bag's own list is counted by the engine
|
||||
// (`FUN_INVMGR_COUNT_FREE_SLOTS`, 3.3.5's `CGContainer_C::GetFreeSlots`); a
|
||||
// range of the player's list (backpack, main bank) is walked slot by slot,
|
||||
// the way 3.3.5's `Script_GetContainerNumFreeSlots` walks it.
|
||||
int CountFree(const View &view);
|
||||
|
||||
} // namespace Container::Bag
|
||||
+29
-115
@@ -12,94 +12,21 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "container/Bag.h"
|
||||
|
||||
namespace Container::FreeSlots {
|
||||
|
||||
namespace {
|
||||
|
||||
// Resolves the bag's metadata: total slot count + BagFamily bitfield.
|
||||
// `C_Container.GetContainerNumFreeSlots(bagID)` — `(numFreeSlots,
|
||||
// bagFamily)`. 3.3.5's `Script_GetContainerNumFreeSlots` (`0x005D7590`):
|
||||
// the backpack and, at a banker, the main bank report family 0; bags 1..4
|
||||
// and bank bags 5..10 report the bag's own family. A container that isn't
|
||||
// there — keyring, an empty bag slot, the bank while it's closed — returns
|
||||
// only `0`, as 3.3.5 does and as retail's nilable `bagFamily` documents.
|
||||
//
|
||||
// - bagID 0 (backpack) → (16, 0). Vanilla 1.12 backpack is fixed at 16
|
||||
// slots and unfamilied (0). 3.3.5's `Script_GetContainerNumFreeSlots`
|
||||
// hardcodes both values for the backpack path too.
|
||||
// - bagID 1..4 → looks up the equipped bag at INVSLOT_BAGn, reads
|
||||
// `m_containerSlots` and `m_bagFamily` from the cached `ItemStats_C`
|
||||
// record. If no bag equipped or item not cached: (0, 0).
|
||||
// - other bagIDs (bank, keyring) → (0, 0). Not supported in this
|
||||
// implementation.
|
||||
struct BagInfo {
|
||||
int slotCount;
|
||||
int bagType;
|
||||
};
|
||||
|
||||
BagInfo ResolveBagInfo(int bagID) {
|
||||
if (bagID == 0)
|
||||
return {Offsets::BACKPACK_NUM_SLOTS, 0};
|
||||
if (bagID < 1 || bagID > 4)
|
||||
return {0, 0};
|
||||
|
||||
const int equipSlot = Offsets::INVSLOT_BAG1 + bagID - 1;
|
||||
auto *bagItem = Item::Location::ResolveEquipmentSlot(equipSlot);
|
||||
if (bagItem == nullptr)
|
||||
return {0, 0};
|
||||
|
||||
const int itemID = Item::ID::FromCGItem(bagItem);
|
||||
if (itemID == 0)
|
||||
return {0, 0};
|
||||
|
||||
auto *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return {0, 0};
|
||||
|
||||
const int slots = static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CONTAINER_SLOTS));
|
||||
// BagFamily bitmask: the item's field if populated, else derived from
|
||||
// the container subclass — Turtle leaves bags' m_bagFamily at 0, so a
|
||||
// Soul Bag would otherwise report family 0 and get miscounted as a
|
||||
// general-purpose bag by CalculateTotalNumberOfFreeBagSlots. Shared
|
||||
// with C_Item.GetItemFamily so a bag and its contents agree. See
|
||||
// `Item::BagFamily`.
|
||||
const int family =
|
||||
static_cast<int>(Item::BagFamily::BitmaskForRecord(record));
|
||||
return {slots, family};
|
||||
}
|
||||
|
||||
// Walks the bag and counts empty slots. Each `Item::Location::ResolveBag`
|
||||
// call stomps Lua stack[1]/[2] (a requirement of the engine's
|
||||
// `PackBagSlot` helper), but we own the stack here — Lua just called us
|
||||
// — and the caller's bagID arg has already been read into a local. So
|
||||
// it's safe to repeatedly clobber the stack across iterations.
|
||||
int CountFreeSlotsInBag(void *L, int bagID, int slotCount) {
|
||||
int free = 0;
|
||||
for (int slot = 1; slot <= slotCount; slot++) {
|
||||
if (Item::Location::ResolveBag(L, bagID, slot) == nullptr)
|
||||
free++;
|
||||
}
|
||||
return free;
|
||||
}
|
||||
|
||||
// `C_Container.GetContainerNumFreeSlots(bagID)` — returns
|
||||
// `(numberOfFreeSlots, bagType)`. `bagID` is 0..4 (0 = backpack,
|
||||
// 1..4 = equipped bag slots). For invalid or unsupported bag IDs
|
||||
// (negative bank/keyring indices, out-of-range values), returns
|
||||
// `(0, 0)` — matching 3.3.5's behavior of always pushing two values
|
||||
// rather than nil/nothing.
|
||||
//
|
||||
// `bagType` is the `BagFamily` bitmask in modern encoding. Bags leave the
|
||||
// raw family field empty in Turtle's data (Soul/Herb/quiver/ammo pouch/…
|
||||
// all read 0), so it's derived from the bag's class + subclass via
|
||||
// `Item::BagFamily`: a Soul Bag reports `0x4`, a quiver `0x1`, an ammo
|
||||
// pouch `0x2` — matching what `C_Item.GetItemFamily` reports for both the
|
||||
// bag and the items it holds. See that helper for the Turtle custom-family
|
||||
// caveat (Meat/Fish/Leather/Mining, IDs 10–13).
|
||||
// `bagFamily` is the modern bitmask; see `Container::Bag::ViewFor` for how
|
||||
// a vanilla bag with an empty family field gets one.
|
||||
int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
@@ -108,11 +35,13 @@ int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
const BagInfo info = ResolveBagInfo(bagID);
|
||||
const int free = (info.slotCount > 0) ? CountFreeSlotsInBag(L, bagID, info.slotCount) : 0;
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(free));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(info.bagType));
|
||||
Container::Bag::View view;
|
||||
if (!Container::Bag::ViewFor(bagID, &view)) {
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(Container::Bag::CountFree(view)));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(view.family));
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -120,11 +49,8 @@ int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
// as an array of slot numbers in ascending order, rather than
|
||||
// `GetContainerNumFreeSlots`'s count of them. A full bag returns an
|
||||
// empty table; a bag that isn't there returns nothing, which is how
|
||||
// callers tell "no free slots" from "no such bag".
|
||||
//
|
||||
// `bagID` is 0..4 like the rest of this file (0 = backpack, 1..4 =
|
||||
// equipped bag slots); bank and keyring indices are the "no such bag"
|
||||
// case.
|
||||
// callers tell "no free slots" from "no such bag". Takes the same
|
||||
// containers as `GetContainerNumFreeSlots`.
|
||||
int __fastcall Script_C_Container_GetContainerFreeSlots(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
@@ -133,24 +59,17 @@ int __fastcall Script_C_Container_GetContainerFreeSlots(void *L) {
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
const BagInfo info = ResolveBagInfo(bagID);
|
||||
if (info.slotCount <= 0)
|
||||
Container::Bag::View view;
|
||||
if (!Container::Bag::ViewFor(bagID, &view))
|
||||
return 0; // nothing — no such bag
|
||||
|
||||
// Collect before building the table. Each `ResolveBag` stomps stack
|
||||
// slots 1 and 2, so the table cannot be on the stack during the walk.
|
||||
std::vector<int> freeSlots;
|
||||
freeSlots.reserve(static_cast<size_t>(info.slotCount));
|
||||
for (int slot = 1; slot <= info.slotCount; ++slot) {
|
||||
if (Item::Location::ResolveBag(L, bagID, slot) == nullptr)
|
||||
freeSlots.push_back(slot);
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
for (size_t i = 0; i < freeSlots.size(); ++i) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
if (Container::Bag::SlotGuid(view, i) != 0)
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(++n));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(freeSlots[i]));
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
return 1;
|
||||
@@ -165,18 +84,13 @@ int __fastcall Script_C_Container_GetContainerFreeSlots(void *L) {
|
||||
// free, family = C_Container.GetContainerNumFreeSlots(bag)
|
||||
// if family == 0 then total = total + free end
|
||||
//
|
||||
// Takes no arguments. Specialty bags are correctly excluded: their family
|
||||
// comes from `Item::BagFamily` (container-subclass fallback), so a Soul
|
||||
// Bag / Herb Bag / Meat Bag reports a non-zero family and its slots are
|
||||
// left out of the general-purpose total — matching retail's FrameXML
|
||||
// behavior. (An earlier version miscounted them because Turtle leaves the
|
||||
// raw family field at 0 for bags.)
|
||||
// Takes no arguments.
|
||||
int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
|
||||
int total = 0;
|
||||
for (int bagID = 0; bagID <= 4; bagID++) {
|
||||
const BagInfo info = ResolveBagInfo(bagID);
|
||||
if (info.slotCount > 0 && info.bagType == 0)
|
||||
total += CountFreeSlotsInBag(L, bagID, info.slotCount);
|
||||
Container::Bag::View view;
|
||||
if (Container::Bag::ViewFor(bagID, &view) && view.family == 0)
|
||||
total += Container::Bag::CountFree(view);
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(total));
|
||||
return 1;
|
||||
|
||||
@@ -60,18 +60,16 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "container/Bag.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "equipmentset/Locations.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/Swap.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -82,6 +80,8 @@ namespace Container::SortBags {
|
||||
|
||||
namespace {
|
||||
|
||||
using BagView = Container::Bag::View;
|
||||
|
||||
// The two sets this can arrange. Both go through the same merge,
|
||||
// snapshot, comparator and placement.
|
||||
constexpr int kInventoryBags[] = {0, 1, 2, 3, 4};
|
||||
@@ -258,96 +258,13 @@ bool Precedes(const Entry &a, const Entry &b) {
|
||||
return a.itemID < b.itemID; // total order, so the result is stable
|
||||
}
|
||||
|
||||
// A bag's storage, addressed without Lua: the invMgr holding its GUID
|
||||
// array, the linear index its first slot sits at, and the family it
|
||||
// accepts. The backpack lives inside the player's own invMgr; bags 1..4
|
||||
// each have one of their own.
|
||||
struct BagView {
|
||||
const uint8_t *invMgr;
|
||||
int firstLinear;
|
||||
int slotCount;
|
||||
uint32_t family;
|
||||
};
|
||||
|
||||
// The backpack and the main bank both live inside the player's own
|
||||
// invMgr, at different linear bases. Equipped bags and bank bags each
|
||||
// have an invMgr of their own, reached through the bag item.
|
||||
//
|
||||
// The bank resolves only while a banker is open: the server refuses every
|
||||
// bank move without one, and the engine's own slot reader (`0x6228a0`)
|
||||
// hides slots 39..68 on the same banker-GUID test. The main bank's GUIDs
|
||||
// sit in the player's invMgr from login, so without this gate a closed-
|
||||
// bank sort would plan and send moves the server rejects one by one.
|
||||
bool ViewForBag(int bagID, BagView *out) {
|
||||
if ((bagID == -1 || bagID >= 5) && !EquipmentSet::Locations::BankerOpen())
|
||||
return false;
|
||||
if (bagID == 0) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, Offsets::INVMGR_BACKPACK_FIRST_SLOT,
|
||||
Offsets::BACKPACK_NUM_SLOTS, 0};
|
||||
return true;
|
||||
}
|
||||
if (bagID == -1) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT -
|
||||
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT + 1,
|
||||
0};
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolve the bag item that owns the container.
|
||||
int equipSlot = 0;
|
||||
if (bagID >= 1 && bagID <= 4)
|
||||
equipSlot = Offsets::INVSLOT_BAG1 + bagID - 1;
|
||||
else if (bagID >= 5 && bagID <= 10)
|
||||
equipSlot = Offsets::INVMGR_BANK_BAG_FIRST_SLOT + 1 + (bagID - 5);
|
||||
else
|
||||
return false;
|
||||
|
||||
auto *bagItem = Item::Location::ResolveEquipmentSlot(equipSlot);
|
||||
if (bagItem == nullptr)
|
||||
return false;
|
||||
auto *bagInv = static_cast<const uint8_t *>(
|
||||
Item::Location::ContainerInventory(bagItem));
|
||||
if (bagInv == nullptr)
|
||||
return false;
|
||||
const int slots = static_cast<int>(
|
||||
Game::Read<uint32_t>(bagInv, Offsets::OFF_INVMGR_SLOT_COUNT));
|
||||
if (slots <= 0)
|
||||
return false;
|
||||
|
||||
// A specialty bag only accepts its own family, and Turtle leaves the
|
||||
// raw field empty on bags — `BitmaskForRecord` derives it from the
|
||||
// container subclass, so a quiver reports a family here rather than
|
||||
// reading as general-purpose. Shared with `C_Item.GetItemFamily`, so
|
||||
// a bag and the items it holds agree.
|
||||
uint32_t family = 0;
|
||||
const int bagItemID = Item::ID::FromCGItem(bagItem);
|
||||
if (bagItemID > 0) {
|
||||
auto *rec = Item::PeekRecord(static_cast<uint32_t>(bagItemID));
|
||||
if (rec != nullptr)
|
||||
family = Item::BagFamily::BitmaskForRecord(rec);
|
||||
}
|
||||
*out = {bagInv, 0, slots, family};
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint8_t *ItemByGuid(uint64_t guid) {
|
||||
return static_cast<const uint8_t *>(
|
||||
Object::ByGuid(Offsets::TYPEMASK_ITEM, guid, "SortBags", 0));
|
||||
}
|
||||
|
||||
const uint8_t *ItemAt(const BagView &v, int slotIndex0, uint64_t *guidOut) {
|
||||
auto *guids = *reinterpret_cast<const uint64_t *const *>(
|
||||
v.invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (guids == nullptr)
|
||||
return nullptr;
|
||||
const uint64_t guid = guids[v.firstLinear + slotIndex0];
|
||||
const uint64_t guid = Container::Bag::SlotGuid(v, slotIndex0);
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
*guidOut = guid;
|
||||
@@ -417,7 +334,7 @@ std::vector<Group> GatherGroups(const int *bags, int bagCount) {
|
||||
for (int b = 0; b < bagCount; ++b) {
|
||||
const int bag = bags[b];
|
||||
BagView view;
|
||||
if (!ViewForBag(bag, &view))
|
||||
if (!Container::Bag::ViewFor(bag, &view))
|
||||
continue;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
uint64_t guid = 0;
|
||||
@@ -590,7 +507,7 @@ bool Place(const int *bags, int bagCount, int mergesPending) {
|
||||
for (int b = 0; b < bagCount; ++b) {
|
||||
const int bag = bags[b];
|
||||
BagView view;
|
||||
if (!ViewForBag(bag, &view))
|
||||
if (!Container::Bag::ViewFor(bag, &view))
|
||||
continue;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
const int slot = i + 1;
|
||||
@@ -958,7 +875,7 @@ int __fastcall Script_SortBagsBags(void *L) {
|
||||
int pushed = 0;
|
||||
for (int b = 0; b < count; ++b) {
|
||||
BagView view;
|
||||
if (!ViewForBag(bags[b], &view))
|
||||
if (!Container::Bag::ViewFor(bags[b], &view))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, bags[b]);
|
||||
Game::Lua::PushNumber(L, view.slotCount);
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
-- ClassicAPI's own entry in the AddOn Options window
|
||||
-- (FrameXML\InterfaceOptions.lua): a ClassicAPI page with one page under it
|
||||
-- for each feature. Every cvar that ClassicAPI registers has a control here,
|
||||
-- and so does the player cache.
|
||||
-- and so do the player cache and ClassicAPI's key bindings.
|
||||
--
|
||||
-- Each control reads and writes exactly one setting, and writes it only when
|
||||
-- the player uses that control. So the pages have no okay, cancel or default:
|
||||
-- Okay, Cancel and Escape only close the window, and Defaults does nothing
|
||||
-- here. A page reads its settings again each time it is shown.
|
||||
-- here. A page reads its settings again each time it is shown. A key binding
|
||||
-- saves at once into the binding set in use (account or character).
|
||||
--
|
||||
-- A page's controls scroll under its heading in a ScrollBox, so a page holds
|
||||
-- as many as its feature has. Dropdowns and number boxes sit on the same row
|
||||
-- as their label.
|
||||
--
|
||||
-- Every string is a global from locales\enUS.lua: retail's GlobalStrings key
|
||||
-- where retail has the string (ENABLE_INTERACT_TEXT and the like), a
|
||||
-- CLASSICAPI_ one where it does not.
|
||||
-- Every string is a global: the client's own (KEY_BOUND, NOT_BOUND), or one
|
||||
-- from locales\enUS.lua - retail's GlobalStrings key where retail has the
|
||||
-- string (ENABLE_INTERACT_TEXT and the like), a CLASSICAPI_ one where it
|
||||
-- does not.
|
||||
--
|
||||
-- Lua 5.0: no `...` expression or select().
|
||||
|
||||
@@ -72,13 +74,20 @@ local rememberSetting = {
|
||||
local LABEL_X = 48
|
||||
local CONTROL_X = 200
|
||||
|
||||
-- pfUI's Interface Options skin finds the controls by these names.
|
||||
-- pfUI's Interface Options skin finds the controls and the page scroll bars
|
||||
-- by these names.
|
||||
local numControls = 0
|
||||
local function NextName()
|
||||
numControls = numControls + 1
|
||||
return "ClassicAPIOptionsControl" .. numControls
|
||||
end
|
||||
|
||||
local numScrollBars = 0
|
||||
local function NextScrollBarName()
|
||||
numScrollBars = numScrollBars + 1
|
||||
return "ClassicAPIOptionsScrollBar" .. numScrollBars
|
||||
end
|
||||
|
||||
local function RefreshControls(page)
|
||||
page.updating = true
|
||||
for _, control in ipairs(page.controls) do
|
||||
@@ -114,7 +123,7 @@ local function NewPage(name, parent, title, subtitle)
|
||||
scrollBox:SetPoint("TOPLEFT", page, "TOPLEFT", 0, top)
|
||||
scrollBox:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", -20, 8)
|
||||
scrollBox:EnableMouseWheel(true)
|
||||
local scrollBar = CreateFrame("EventFrame", nil, page, "MinimalScrollBar")
|
||||
local scrollBar = CreateFrame("EventFrame", NextScrollBarName(), page, "MinimalScrollBar")
|
||||
scrollBar:SetPoint("TOPLEFT", scrollBox, "TOPRIGHT", 4, 0)
|
||||
scrollBar:SetPoint("BOTTOMLEFT", scrollBox, "BOTTOMRIGHT", 4, 0)
|
||||
|
||||
@@ -302,6 +311,215 @@ local function AddSlider(page, label, tooltip, setting, minValue, maxValue, step
|
||||
AddControl(page, slider, setting, 56)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Key bindings
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- A key binding row, as on retail's settings pages: the binding's name, then
|
||||
-- a button for each of its two keys. Left click a button and press a key, a
|
||||
-- mouse button or the wheel to bind it there; Escape cancels. Right click a
|
||||
-- button to unbind its key.
|
||||
--
|
||||
-- While a button listens, `listener` covers the screen above the window and
|
||||
-- takes every key, click and wheel turn, as retail's KeybindListener and its
|
||||
-- input blocker do. A left or right click on it cancels. The binding itself
|
||||
-- is 1.12's Key Bindings window's: the same chord order, the same check for
|
||||
-- an action that loses its last key, the same put-back when SetBinding
|
||||
-- refuses a key.
|
||||
--
|
||||
-- Messages go on the page's output line, under its controls.
|
||||
|
||||
local LISTEN_BUTTONS = {
|
||||
MiddleButton = "BUTTON3",
|
||||
Button4 = "BUTTON4",
|
||||
Button5 = "BUTTON5",
|
||||
}
|
||||
|
||||
-- Keys that only modify another key, as the Key Bindings window skips them.
|
||||
local IGNORED_KEYS = { UNKNOWN = true, SHIFT = true, CTRL = true, ALT = true }
|
||||
|
||||
local listener = CreateFrame("Button", nil, UIParent)
|
||||
listener:SetAllPoints(UIParent)
|
||||
listener:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
listener:EnableMouse(true)
|
||||
listener:EnableMouseWheel(true)
|
||||
listener:EnableKeyboard(true)
|
||||
listener:RegisterForClicks("LeftButtonUp", "RightButtonUp", "MiddleButtonUp", "Button4Up", "Button5Up")
|
||||
listener:Hide()
|
||||
|
||||
local function BindingName(action)
|
||||
return GetBindingText(action, "BINDING_NAME_")
|
||||
end
|
||||
|
||||
-- GetBindingKey's keys as a list: a row's button `i` shows keys[i].
|
||||
local function BindingKeys(action)
|
||||
return { GetBindingKey(action) }
|
||||
end
|
||||
|
||||
local function StopListening()
|
||||
local row = listener.row
|
||||
if not row then return end
|
||||
listener.row = nil
|
||||
listener:Hide()
|
||||
row.page.output:SetText("")
|
||||
RefreshControls(row.page)
|
||||
end
|
||||
|
||||
local function StartListening(row, index)
|
||||
-- A number box that keeps the focus would take the key instead.
|
||||
for _, control in ipairs(row.page.controls) do
|
||||
if control.ClearFocus then control:ClearFocus() end
|
||||
end
|
||||
listener.row = row
|
||||
listener.index = index
|
||||
listener:Show()
|
||||
row.page.output:SetText(format(SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL, BindingName(row.action), GetBindingText("ESCAPE", "KEY_")))
|
||||
RefreshControls(row.page)
|
||||
end
|
||||
|
||||
-- 1.12's chord order: ALT-CTRL-SHIFT-key.
|
||||
local function Chord(key)
|
||||
if IsShiftKeyDown() then key = "SHIFT-" .. key end
|
||||
if IsControlKeyDown() then key = "CTRL-" .. key end
|
||||
if IsAltKeyDown() then key = "ALT-" .. key end
|
||||
return key
|
||||
end
|
||||
|
||||
local function Bind(key)
|
||||
if IGNORED_KEYS[key] then return end
|
||||
local chord = Chord(key)
|
||||
local current = GetBindingAction(chord)
|
||||
if current == "SCREENSHOT" then
|
||||
Screenshot()
|
||||
return
|
||||
end
|
||||
|
||||
local row, index = listener.row, listener.index
|
||||
StopListening()
|
||||
if key == "ESCAPE" and current == "TOGGLEGAMEMENU" then return end
|
||||
|
||||
local action = row.action
|
||||
local message = KEY_BOUND
|
||||
if current ~= "" and current ~= action then
|
||||
local old1, old2 = GetBindingKey(current)
|
||||
if (not old1 or old1 == chord) and (not old2 or old2 == chord) then
|
||||
message = format(KEY_UNBOUND_ERROR, BindingName(current))
|
||||
end
|
||||
end
|
||||
|
||||
-- Unbind both keys, then bind them back in order with the new key in
|
||||
-- the slot that was clicked.
|
||||
local keys = BindingKeys(action)
|
||||
for i = 1, 2 do
|
||||
if keys[i] then SetBinding(keys[i]) end
|
||||
end
|
||||
for i = 1, 2 do
|
||||
local bind = (i == index) and chord or keys[i]
|
||||
if bind and not SetBinding(bind, action) then
|
||||
if keys[i] then SetBinding(keys[i], action) end
|
||||
message = KEYBINDINGFRAME_MOUSEWHEEL_ERROR
|
||||
end
|
||||
end
|
||||
SaveBindings(GetCurrentBindingSet())
|
||||
|
||||
RefreshControls(row.page)
|
||||
row.page.output:SetText(message)
|
||||
end
|
||||
|
||||
listener:SetScript("OnKeyDown", function() Bind(arg1) end)
|
||||
listener:SetScript("OnMouseWheel", function()
|
||||
Bind(arg1 > 0 and "MOUSEWHEELUP" or "MOUSEWHEELDOWN")
|
||||
end)
|
||||
listener:SetScript("OnClick", function()
|
||||
local button = LISTEN_BUTTONS[arg1]
|
||||
if button then Bind(button) else StopListening() end
|
||||
end)
|
||||
|
||||
local function Unbind(row, index)
|
||||
local key = BindingKeys(row.action)[index]
|
||||
if key then
|
||||
SetBinding(key)
|
||||
SaveBindings(GetCurrentBindingSet())
|
||||
end
|
||||
GameTooltip:Hide()
|
||||
row.page.output:SetText("")
|
||||
RefreshControls(row.page)
|
||||
end
|
||||
|
||||
-- The page's output line, made with its first key binding row. It takes the
|
||||
-- bottom of the page from the ScrollBox.
|
||||
local function EnsureOutput(page)
|
||||
if page.output then return end
|
||||
local output = page:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
|
||||
output:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 16, 12)
|
||||
output:SetPoint("RIGHT", page, "RIGHT", -16, 0)
|
||||
output:SetJustifyH("LEFT")
|
||||
page.scrollBox:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", -20, 30)
|
||||
page.output = output
|
||||
page:SetScript("OnHide", function()
|
||||
if listener.row and listener.row.page == this then StopListening() end
|
||||
this.output:SetText("")
|
||||
end)
|
||||
end
|
||||
|
||||
local function AddKeybinding(page, action)
|
||||
EnsureOutput(page)
|
||||
AddRowLabel(page, BindingName(action))
|
||||
|
||||
local row = { page = page, action = action, buttons = {} }
|
||||
for i = 1, 2 do
|
||||
local button = CreateFrame("Button", NextName(), page.content, "UIPanelButtonTemplate2")
|
||||
button:SetWidth(86)
|
||||
button:SetHeight(22)
|
||||
if i == 1 then
|
||||
button:SetPoint("TOPLEFT", page.content, "TOPLEFT", CONTROL_X, page.y - 5)
|
||||
else
|
||||
button:SetPoint("LEFT", row.buttons[1], "RIGHT", 0, 0)
|
||||
end
|
||||
button:SetTextFontObject(GameFontHighlightSmall)
|
||||
button:SetHighlightFontObject(GameFontHighlightSmall)
|
||||
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
button.index = i
|
||||
button:SetScript("OnClick", function()
|
||||
if arg1 == "RightButton" then
|
||||
Unbind(row, this.index)
|
||||
else
|
||||
StartListening(row, this.index)
|
||||
end
|
||||
end)
|
||||
button:SetScript("OnEnter", function()
|
||||
local key = BindingKeys(action)[this.index]
|
||||
if not key then return end
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(format(KEY_BINDING_NAME_AND_KEY, BindingName(action), GetBindingText(key, "KEY_")),
|
||||
HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
|
||||
GameTooltip:AddLine(KEY_BINDING_TOOLTIP, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
button:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
row.buttons[i] = button
|
||||
end
|
||||
|
||||
row.Update = function(self)
|
||||
local keys = BindingKeys(action)
|
||||
for i, button in ipairs(self.buttons) do
|
||||
if keys[i] then
|
||||
button:SetText(GetBindingText(keys[i], "KEY_"))
|
||||
button:SetAlpha(1)
|
||||
else
|
||||
button:SetText(NORMAL_FONT_COLOR_CODE .. NOT_BOUND .. FONT_COLOR_CODE_CLOSE)
|
||||
button:SetAlpha(0.8)
|
||||
end
|
||||
if listener.row == self and listener.index == i then
|
||||
button:LockHighlight()
|
||||
else
|
||||
button:UnlockHighlight()
|
||||
end
|
||||
end
|
||||
end
|
||||
AddControl(page, row, nil, 32)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ClassicAPI
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -316,6 +534,8 @@ local main = NewPage("ClassicAPI", nil, "ClassicAPI " .. version, CLASSICAPI_OPT
|
||||
local interact = NewPage(CLASSICAPI_INTERACT_KEY, main.name)
|
||||
|
||||
AddCheckbox(interact, ENABLE_INTERACT_TEXT, OPTION_TOOLTIP_ENABLE_INTERACT, SoftTargetSetting("SoftTargetInteract"))
|
||||
AddKeybinding(interact, "INTERACTTARGET")
|
||||
AddKeybinding(interact, "INTERACTMOUSEOVER")
|
||||
AddDropDown(interact, CLASSICAPI_INTERACT_DIRECTION, CLASSICAPI_INTERACT_DIRECTION_TOOLTIP, NumberCVarSetting("SoftTargetInteractArc"), ARC_CHOICES)
|
||||
AddNumberBox(interact, CLASSICAPI_INTERACT_RANGE, CLASSICAPI_RANGE_TOOLTIP, NumberCVarSetting("SoftTargetInteractRange"))
|
||||
AddCheckbox(interact, CLASSICAPI_STRICT_INTERACT_RANGE, CLASSICAPI_STRICT_INTERACT_RANGE_TOOLTIP, CVarSetting("SoftTargetInteractRangeIsHard"))
|
||||
@@ -378,6 +598,9 @@ AddCheckbox(bags, CLASSICAPI_IGNORE_BACKPACK, CLASSICAPI_IGNORE_BACKPACK_TOOLTIP
|
||||
AddCheckbox(bags, CLASSICAPI_IGNORE_BANK, CLASSICAPI_IGNORE_BANK_TOOLTIP, CVarSetting("bankAutosortDisabled"))
|
||||
AddCheckbox(bags, CLASSICAPI_HIDE_EQUIPMENT_SETS, CLASSICAPI_HIDE_EQUIPMENT_SETS_TOOLTIP, CVarSetting("dontShowEquipmentSetsOnItems"))
|
||||
|
||||
AddHeader(bags, LOOT)
|
||||
AddKeybinding(bags, "AOELOOT")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Interface
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
@@ -69,6 +69,9 @@ OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Markieren, um für Abklingzeiten einen
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Erlaubt Interaktion mit NSCs und Objekten per Tastendruck";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Legt fest, welche Symbole über NSCs und Gegenständen angezeigt werden, wenn die Interaktionstastenfunktion aktiviert ist.";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Automatischen Angriff stoppen, sobald das Ziel gewechselt wird.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<Zum Löschen der Belegung rechtsklicken>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Tastaturbelegung für \"%s\" zuweisen oder mit %s abbrechen";
|
||||
SET_FOCUS = "Fokus setzen";
|
||||
FOCUSTARGET = "Ziel als Fokus festlegen";
|
||||
GEARSETS_POPUP_TEXT = "Setnamen eingeben (Max. 16 Zeichen):";
|
||||
|
||||
@@ -159,6 +159,9 @@ OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Check this to show a number countdown
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Allows you to interact with NPCs and objects using a keypress";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Controls which icons appear above NPCs and objects when the Interact Key feature is enabled";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Stop auto attacking when you switch targets.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<Right click to unbind>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Assign Binding for \"%s\" or Press %s to Cancel";
|
||||
SET_FOCUS = "Set Focus";
|
||||
FOCUS_TOKEN_NOT_FOUND = "<no focus>";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
|
||||
@@ -69,6 +69,9 @@ OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Activa esta opción para mostrar una c
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Te permite interactuar con PNJ y objetos pulsando una tecla.";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Controla los iconos que aparecen sobre los PNJ y objetos cuando se habilita la opción de tecla Interactuar";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Detiene el ataque automático al cambiar de objetivo.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<Haz clic derecho para quitar la asignación>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Asignar atajo para \"%s\" o pulsar %s para cancelar";
|
||||
SET_FOCUS = "Establecer foco";
|
||||
FOCUSTARGET = "Establecer objetivo";
|
||||
GEARSETS_POPUP_TEXT = "Nombra este equipamiento (Máx. 16 carac.):";
|
||||
|
||||
@@ -69,6 +69,9 @@ OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Cochez pour afficher les temps de rech
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Vous permet d’interagir avec les PNJ et les objets en appuyant sur une touche";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Contrôle les icônes qui s’affichent au-dessus des PNJ et des objets quand la fonction Commande d’interaction est activée";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Interrompt l'attaque automatique lorsque vous changez de cible.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<Clic droit pour retirer>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Assigner un raccourci pour « %s » ou appuyer sur %s pour annuler";
|
||||
SET_FOCUS = "Définir la focalisation";
|
||||
FOCUSTARGET = "Définir la cible de focalisation";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4objet:objets;";
|
||||
|
||||
@@ -10,3 +10,6 @@ SECOND_NUMBER_CAP = "억";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = "억";
|
||||
THIRD_NUMBER = "억";
|
||||
FOURTH_NUMBER = "조";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<지정을 해제하려면 오른쪽 클릭>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "\"%s\" 단축키 지정 또는 %s 눌러 취소";
|
||||
|
||||
@@ -69,6 +69,9 @@ OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Отображение обратно
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Позволяет взаимодействовать с NPC и объектами по нажатию клавиши.";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Определяет, какие значки появляются над NPC и объектами, если включен параметр клавиши взаимодействия.";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Прекращение автоматической атаки при смене цели.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<ПКМ, чтобы отвязать>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Назначьте клавишу \"%s\"или нажмите %s для отмены";
|
||||
SET_FOCUS = "Запомнить цель";
|
||||
FOCUSTARGET = "Запомнить цель";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
|
||||
|
||||
@@ -69,6 +69,9 @@ OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "勾选此项后,动作条上的按
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "使你可以通过按键与NPC和物体进行交互";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "控制交互按键启用后NPC和物体上方出现的图标";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "当你切换目标时停止自动攻击。";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s(%s)";
|
||||
KEY_BINDING_TOOLTIP = "<右键解除键位>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "设置 \"%s\"的快捷键,或者按 %s 取消";
|
||||
SET_FOCUS = "设置焦点";
|
||||
FOCUSTARGET = "设定焦点目标";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
|
||||
@@ -69,6 +69,9 @@ OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "勾選此選項可在快捷鍵上面
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "可透過按鍵與NPC和物件互動";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "在啟用互動鍵功能後,控制出現在NPC和物件上方的圖示";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "當你切換目標時停止自動攻擊。";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s(%s)";
|
||||
KEY_BINDING_TOOLTIP = "<右鍵點擊以解除綁定>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "為\"%s\"指配綁定或是按下%s以取消";
|
||||
SET_FOCUS = "設定專注目標";
|
||||
FOCUSTARGET = "設定鎖定目標";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
|
||||
@@ -287,6 +287,10 @@ uint32_t CurrentMask() {
|
||||
return g_overrideActive ? g_overrideMask : HeldMask();
|
||||
}
|
||||
|
||||
bool IsKeyDown(int key) {
|
||||
return reinterpret_cast<IsKeyDown_t>(Offsets::FUN_GAMEUI_IS_KEY_DOWN)(key) != 0;
|
||||
}
|
||||
|
||||
uint32_t ParseModifiers(const char *&text) {
|
||||
static const struct {
|
||||
const char *name;
|
||||
|
||||
@@ -30,6 +30,11 @@ namespace Input::Modifier {
|
||||
// CGUIBindings::m_keyStateOverride.
|
||||
uint32_t CurrentMask();
|
||||
|
||||
// The engine's EventIsKeyDown (FUN_GAMEUI_IS_KEY_DOWN), called the way its
|
||||
// own callers do — through the hook above, so a KeyStateOverride applies.
|
||||
// `key` is 0 shift, 1 ctrl, 2 alt, or any other KEY code.
|
||||
bool IsKeyDown(int key);
|
||||
|
||||
// CGUIBindings::AddModifiers: consumes leading modifier names from `text`
|
||||
// ("SHIFT", "LCTRL", "RALT", … each optionally followed by '-') and returns
|
||||
// their bits. `text` is left at the first character that is not one.
|
||||
|
||||
+1
-1
@@ -438,7 +438,7 @@ static void RebuildOwned() {
|
||||
// Contents of each equipped bag (its own invMgr, slot count at +0).
|
||||
for (int bagID = 1; bagID <= 4; ++bagID) {
|
||||
auto *bagInv =
|
||||
static_cast<const uint8_t *>(Item::Location::EquippedBagInventory(bagID));
|
||||
static_cast<const uint8_t *>(Item::Location::BagInventory(bagID));
|
||||
if (bagInv == nullptr)
|
||||
continue;
|
||||
const int count = static_cast<int>(
|
||||
|
||||
+2
-10
@@ -25,6 +25,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Icon.h"
|
||||
#include "item/Location.h"
|
||||
@@ -85,13 +86,6 @@ const char *LookupInvType(uint32_t invType) {
|
||||
return (s != nullptr && s[0] != '\0') ? s : "";
|
||||
}
|
||||
|
||||
// Wrath flipped the bagFamily encoding from raw 1-based IDs to a
|
||||
// bitmask. We surface the modern shape so backported addons compile
|
||||
// cleanly. See `Item::Info::BagFamilyIdToBitmask` for the full story.
|
||||
uint32_t BagFamilyIdToBitmask(uint32_t rawId) {
|
||||
return (rawId == 0) ? 0 : (1u << (rawId - 1));
|
||||
}
|
||||
|
||||
// Shorthand: read a u32 field from the record at `offset`, push it on
|
||||
// the stack as a Lua number, and set `t[key] = value` on the table at
|
||||
// stack[-3]. `SetFieldNumber` already handles the push/set; this is
|
||||
@@ -276,10 +270,8 @@ void PushItemDataTable(void *L, uint32_t itemID, const uint8_t *record) {
|
||||
SetI32(L, record, "maxStackSize", Offsets::OFF_ITEMSTATS_STACKABLE);
|
||||
SetI32(L, record, "maxCount", Offsets::OFF_ITEMSTATS_MAX_COUNT);
|
||||
SetU32(L, record, "containerSlots", Offsets::OFF_ITEMSTATS_CONTAINER_SLOTS);
|
||||
const uint32_t rawBagFamily = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_BAG_FAMILY);
|
||||
Game::Lua::SetFieldNumber(L, "bagFamily",
|
||||
static_cast<double>(BagFamilyIdToBitmask(rawBagFamily)));
|
||||
static_cast<double>(Item::BagFamily::BitmaskForRecord(record)));
|
||||
|
||||
// Economy
|
||||
SetU32(L, record, "buyPrice", Offsets::OFF_ITEMSTATS_BUY_PRICE);
|
||||
|
||||
+11
-9
@@ -107,23 +107,25 @@ void *ContainerInventory(const uint8_t *container) {
|
||||
return getInventory(const_cast<uint8_t *>(container));
|
||||
}
|
||||
|
||||
void *EquippedBagInventory(int bagID) {
|
||||
if (bagID < 1 || bagID > 4)
|
||||
const uint8_t *BagContainer(int bagID) {
|
||||
if (bagID < Offsets::BAG_INDEX_BAG_FIRST || bagID > Offsets::BAG_INDEX_BANK_BAG_LAST)
|
||||
return nullptr;
|
||||
using GetBagGuid_t = uint64_t(__fastcall *)(uint32_t bagIndex0);
|
||||
auto getBagGuid =
|
||||
reinterpret_cast<GetBagGuid_t>(Offsets::FUN_GET_EQUIPPED_BAG_GUID);
|
||||
const uint64_t bagGuid = getBagGuid(static_cast<uint32_t>(bagID - 1));
|
||||
if (bagGuid == 0)
|
||||
return nullptr; // no bag equipped in that slot
|
||||
auto *container = static_cast<const uint8_t *>(
|
||||
return nullptr; // no bag there, or a bank bag with the bank closed
|
||||
return static_cast<const uint8_t *>(
|
||||
Object::ByGuid(Offsets::TYPEMASK_CONTAINER, bagGuid, "ItemMgr", 0x172));
|
||||
return ContainerInventory(container);
|
||||
}
|
||||
|
||||
// Mirrors `Item::Bag::ResolveBagInfo`'s slot-count derivation: backpack
|
||||
// is fixed at 16, equipped bags read `m_containerSlots` off the cache
|
||||
// record. Returns 0 for empty bag slots or out-of-range bagIDs.
|
||||
void *BagInventory(int bagID) {
|
||||
return ContainerInventory(BagContainer(bagID));
|
||||
}
|
||||
|
||||
// Backpack is fixed at 16, equipped bags read `m_containerSlots` off the
|
||||
// cache record. Returns 0 for empty bag slots or out-of-range bagIDs.
|
||||
int GetBagSlotCount(int bagID) {
|
||||
if (bagID == 0)
|
||||
return Offsets::BACKPACK_NUM_SLOTS;
|
||||
@@ -289,7 +291,7 @@ const uint8_t *ResolveBagSlotNoLua(int bagID, int slotIndex) {
|
||||
return static_cast<const uint8_t *>(
|
||||
GetItemBySlot(invMgr, Offsets::BACKPACK_LINEAR_BASE + slotIndex - 1));
|
||||
}
|
||||
void *bagInv = EquippedBagInventory(bagID);
|
||||
void *bagInv = BagInventory(bagID);
|
||||
if (bagInv == nullptr)
|
||||
return nullptr;
|
||||
const int slotCount = static_cast<int>(*reinterpret_cast<const uint32_t *>(bagInv));
|
||||
|
||||
+18
-12
@@ -87,16 +87,22 @@ int GetBagSlotCount(int bagID);
|
||||
// `equipmentset/Locations`, and `item/NewItems`.
|
||||
void *ContainerInventory(const uint8_t *container);
|
||||
|
||||
// Returns the inventory object of the equipped bag in player bag slot `bagID`
|
||||
// (1..4): its container GUID via `FUN_GET_EQUIPPED_BAG_GUID`, resolved to the
|
||||
// CGContainer, then its inventory via `ContainerInventory`. The result has the
|
||||
// player-invMgr shape (slot count at `+0x00`, GUID array at
|
||||
// `+OFF_INVMGR_GUID_ARRAY`) and is what `GetItemBySlot` indexes with a 0-based
|
||||
// slot. Returns null for `bagID` outside 1..4 or when no bag is equipped
|
||||
// there. Pure C — no Lua stack. (The backpack, `bagID` 0, has no container
|
||||
// object; index it directly off the player inventory manager at
|
||||
// `BACKPACK_LINEAR_BASE + slot - 1`.)
|
||||
void *EquippedBagInventory(int bagID);
|
||||
// Returns the bag in container `bagID` — an equipped bag (1..4) or a bank
|
||||
// bag (5..10) — as its CGContainer, which is also its CGItem. The GUID comes
|
||||
// from `FUN_GET_EQUIPPED_BAG_GUID`, which itself answers 0 for a bank bag
|
||||
// while no bank window is open, so bank bags resolve only at a banker, the
|
||||
// way `PackBagSlot` resolves them. Null for any other `bagID` or an empty
|
||||
// bag slot. Pure C — no Lua stack.
|
||||
const uint8_t *BagContainer(int bagID);
|
||||
|
||||
// `ContainerInventory(BagContainer(bagID))`: the inventory object of the bag
|
||||
// in container `bagID` (1..10, bank bags only at a banker). The result has
|
||||
// the player-invMgr shape (slot count at `+0x00`, GUID array at
|
||||
// `+OFF_INVMGR_GUID_ARRAY`) and is what `GetItemBySlot` indexes with a
|
||||
// 0-based slot. Null when `BagContainer` is. Pure C — no Lua stack. (The
|
||||
// backpack, `bagID` 0, has no container object; index it directly off the
|
||||
// player inventory manager at `BACKPACK_LINEAR_BASE + slot - 1`.)
|
||||
void *BagInventory(int bagID);
|
||||
|
||||
// GUID-walk result. `equipmentSlotIndex != 0` means the item was
|
||||
// found in a character-pane equipment slot; otherwise it's in
|
||||
@@ -224,8 +230,8 @@ bool FindItemArgOrLocation(void *L, int idx, ByGUIDResult *out);
|
||||
|
||||
// Resolves `(bagID, slotIndex)` to its `CGItem *` WITHOUT the Lua stack:
|
||||
// the backpack (bag 0) is indexed straight off the player inventory manager
|
||||
// at `BACKPACK_LINEAR_BASE + slot - 1`, bags 1..4 through
|
||||
// `EquippedBagInventory` — both via the engine's `GetItemBySlot`. Returns
|
||||
// at `BACKPACK_LINEAR_BASE + slot - 1`, bags 1..10 through `BagInventory`
|
||||
// (bank bags only at a banker) — both via the engine's `GetItemBySlot`. Returns
|
||||
// nullptr for a bad bag, an out-of-range slot, or an empty slot. Safe from
|
||||
// any context (world tick, packet hooks) — the `PackBagSlot`-based
|
||||
// `ResolveBag` is not, because it reads its inputs off the Lua stack.
|
||||
|
||||
@@ -242,7 +242,7 @@ int ScanBags(const uint8_t *player, uint64_t *out) {
|
||||
const int slots = Item::Location::GetBagSlotCount(bagID);
|
||||
if (slots <= 0)
|
||||
continue;
|
||||
void *bagInv = Item::Location::EquippedBagInventory(bagID);
|
||||
void *bagInv = Item::Location::BagInventory(bagID);
|
||||
if (bagInv == nullptr)
|
||||
continue;
|
||||
for (int slot = 1; slot <= slots; ++slot) {
|
||||
|
||||
@@ -1407,6 +1407,15 @@ bool HasQuestionMarkIcon(uint32_t macroID) {
|
||||
kQuestionMarkIcon) == 0;
|
||||
}
|
||||
|
||||
bool HasShowTooltipDirective(uint32_t macroID) {
|
||||
const uint8_t *entry = EntryForID(macroID);
|
||||
if (entry == nullptr)
|
||||
return false;
|
||||
Parsed parsed;
|
||||
ParseBody(reinterpret_cast<const char *>(entry + Offsets::OFF_MACRO_BODY), &parsed);
|
||||
return parsed.kind == Kind::ShowTooltip;
|
||||
}
|
||||
|
||||
bool ResolvedIconPath(const Info &info, bool activeIcon, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
|
||||
@@ -149,6 +149,11 @@ const char *MacroNameForSlot(int slot0);
|
||||
// action bar and in the Macro UI; any other chosen icon stays.
|
||||
bool HasQuestionMarkIcon(uint32_t macroID);
|
||||
|
||||
// True when the macro's body carries `#showtooltip` by the same parse that
|
||||
// decides the tooltip handover. Reads the body itself, so it answers while
|
||||
// the module yields or a publisher owns the macro.
|
||||
bool HasShowTooltipDirective(uint32_t macroID);
|
||||
|
||||
// Texture path of the resolved target: the spell's icon (its active icon
|
||||
// when `activeIcon`, exactly the engine's toggle-up choice) or the item's
|
||||
// icon. False (out empty) when nothing resolved or no icon record exists.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU 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/>.
|
||||
|
||||
// `table.removemulti(t [, pos [, count]])` — WoW's addition to the table
|
||||
// library (4.3.4's tremovemulti, ltablib.c): removes `count` elements (1 by
|
||||
// default) from position `pos` (the last element by default), shifts the
|
||||
// rest down and returns the removed elements in order.
|
||||
//
|
||||
// 5.0 keeps a table's length apart from its keys, so like the engine's own
|
||||
// table.remove (0x7FB770) this records the new length with luaL_setn.
|
||||
//
|
||||
// Registered on BOTH states (language-level helper, both are 5.0).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Table::RemoveMulti {
|
||||
|
||||
namespace {
|
||||
|
||||
using luaL_getn_t = int(__fastcall *)(void *L, int t);
|
||||
|
||||
int __fastcall Script_table_removemulti(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE)
|
||||
Game::Lua::ArgError(L, 1, "table expected");
|
||||
const int e = reinterpret_cast<luaL_getn_t>(Offsets::LUAL_GETN)(L, 1);
|
||||
int pos = static_cast<int>(Game::Lua::OptNumber(L, 2, e));
|
||||
const int n = static_cast<int>(Game::Lua::OptNumber(L, 3, 1));
|
||||
if (e == 0)
|
||||
return 0;
|
||||
if (pos <= 0 || n < 0 || pos > e || pos + n - 1 > e)
|
||||
Game::Lua::Error(L, "parameters out of bounds");
|
||||
if (!Game::Lua::CheckStack(L, n))
|
||||
Game::Lua::Error(L, "stack overflow");
|
||||
for (int i = pos; i < pos + n; i++)
|
||||
Game::Lua::RawGetI(L, 1, i);
|
||||
for (; pos < e; pos++) {
|
||||
Game::Lua::RawGetI(L, 1, pos + n);
|
||||
Game::Lua::RawSetI(L, 1, pos);
|
||||
}
|
||||
for (; pos < e + n; pos++) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::RawSetI(L, 1, pos);
|
||||
}
|
||||
Game::Lua::SetN(L, 1, e - n);
|
||||
return n;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Game::Lua::RegisterTableFunction("table", "removemulti", &Script_table_removemulti);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Table::RemoveMulti
|
||||
@@ -185,19 +185,34 @@ uint64_t Get() {
|
||||
return g_soft;
|
||||
}
|
||||
|
||||
// Retail 12.1.5 probe, camera still, a soft-interact game object in front:
|
||||
// with an interactable NPC targeted `anyinteract` is the NPC, with no
|
||||
// target it is the game object — the target wins.
|
||||
// Retail 12.1.5 probes, camera still:
|
||||
// - a soft-interact game object in front: with an interactable NPC
|
||||
// targeted `anyinteract` is the NPC, with no target it is the game
|
||||
// object — the target wins;
|
||||
// - an NPC targeted out of reach (the Unable form): facing away from
|
||||
// everything (`softinteract` nil) `anyinteract` is the target; facing
|
||||
// a nearer NPC (`softinteract`) it is that NPC.
|
||||
// So a target in reach wins, an out-of-reach one yields to `softinteract`
|
||||
// and is used only when nothing is picked — the key then still runs the
|
||||
// engine's interact on it ("You need to be closer", or the click-to-move
|
||||
// walk). Being behind the camera does not disqualify the target; the
|
||||
// cursor verdict does not look at facing.
|
||||
uint64_t GetAny() {
|
||||
if (!Object::RegistryReady())
|
||||
return 0;
|
||||
const uint64_t target = Game::Read<uint64_t>(Offsets::VAR_CURRENT_SELECTION_GUID_LO);
|
||||
Reach reach = Reach::None;
|
||||
if (target != 0) {
|
||||
void *object = Object::ByGuid(kTypeMask, target);
|
||||
if (object != nullptr && ReachOf(object) != Reach::None)
|
||||
return target;
|
||||
if (object != nullptr)
|
||||
reach = ReachOf(object);
|
||||
}
|
||||
return Get();
|
||||
if (reach == Reach::Now)
|
||||
return target;
|
||||
const uint64_t soft = Get();
|
||||
if (soft != 0 || reach == Reach::None)
|
||||
return soft;
|
||||
return target;
|
||||
}
|
||||
|
||||
bool InReach(void *object) { return ReachOf(object) == Reach::Now; }
|
||||
|
||||
@@ -21,8 +21,9 @@ namespace Target::SoftInteract {
|
||||
// Read by the token resolver in `unit/TokenExtensions.cpp`.
|
||||
uint64_t Get();
|
||||
|
||||
// The `anyinteract` GUID: the current target when it can be interacted
|
||||
// with (in reach or not, on screen or not), else `softinteract`, else 0.
|
||||
// The `anyinteract` GUID: the current target when the player can interact
|
||||
// with it right now (in reach; on screen or not), else `softinteract`, else
|
||||
// the target when it is interactable but out of reach, else 0.
|
||||
uint64_t GetAny();
|
||||
|
||||
// True when the player can interact with `object` right now: the hover
|
||||
|
||||
+36
-6
@@ -38,13 +38,20 @@
|
||||
// looseTargeting]])` is the modern name the INTERACTTARGET /
|
||||
// INTERACTMOUSEOVER bindings call (with "anyinteract", see
|
||||
// target/SoftInteract.cpp): the same resolve and right-click, returning a
|
||||
// boolean. looseTargeting has no 1.12 counterpart and is read by nothing.
|
||||
// boolean. looseTargeting (default true) keeps the current target: on
|
||||
// retail 12.1.5 the Interact Key on a nearer NPC opens its dialog while a
|
||||
// farther unit stays targeted, and with no target it targets the NPC. Only
|
||||
// a unit other than the target would be selected, so only then does the
|
||||
// right-click run without its select (InteractKeepingTarget); everything
|
||||
// else is the engine's own right-click, which nampower also detours.
|
||||
//
|
||||
// 4.3.4's taint gate is dropped: 1.12 has no secure environment, and every
|
||||
// vanilla action function (TargetUnit, CastSpellByName, …) is callable.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "input/Modifier.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "unit/Identity.h"
|
||||
#include "unit/TokenResolve.h"
|
||||
|
||||
@@ -56,6 +63,7 @@ namespace Unit::Interact {
|
||||
namespace {
|
||||
|
||||
using OnSpriteRightClick_t = int(__stdcall *)(uint32_t guidLo, uint32_t guidHi);
|
||||
using OnRightClick_t = void(__thiscall *)(void *object, int modifier);
|
||||
using TokenToGuid_t = uint64_t(__fastcall *)(const char *token);
|
||||
|
||||
uint64_t ResolveGuid(void *L, const char *token) {
|
||||
@@ -67,8 +75,27 @@ uint64_t ResolveGuid(void *L, const char *token) {
|
||||
Game::Lua::ToFlag(L, 2));
|
||||
}
|
||||
|
||||
bool Interact(void *L) {
|
||||
// FUN_ON_SPRITE_RIGHT_CLICK without its select (FUN_SET_SELECTION_BY_GUID):
|
||||
// the active-player gate, then the object's right-click virtual.
|
||||
bool InteractKeepingTarget(void *object) {
|
||||
if (Object::ByGuid(Offsets::TYPEMASK_PLAYER, Unit::Identity::PlayerGuid()) == nullptr)
|
||||
return true;
|
||||
const int modifier = Input::Modifier::IsKeyDown(0) ? 1 : 0;
|
||||
const auto vtable = *static_cast<uintptr_t *const *>(object);
|
||||
reinterpret_cast<OnRightClick_t>(
|
||||
vtable[Offsets::OFF_OBJECT_VTABLE_ON_RIGHT_CLICK / sizeof(uintptr_t)])(object, modifier);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Interact(void *L, bool loose) {
|
||||
const uint64_t guid = ResolveGuid(L, Game::Lua::ToString(L, 1));
|
||||
const uint64_t target = Game::Read<uint64_t>(Offsets::VAR_CURRENT_SELECTION_GUID_LO);
|
||||
if (loose && target != 0 && guid != target) {
|
||||
// The select stores only units (slot 0x58), so only a unit would move
|
||||
// the target.
|
||||
if (void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid))
|
||||
return InteractKeepingTarget(unit);
|
||||
}
|
||||
return reinterpret_cast<OnSpriteRightClick_t>(Offsets::FUN_ON_SPRITE_RIGHT_CLICK)(
|
||||
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32)) != 0;
|
||||
}
|
||||
@@ -78,7 +105,7 @@ int __fastcall Script_InteractUnit(void *L) {
|
||||
Game::Lua::Error(L, "Usage: InteractUnit(unitToken[, exactMatch])");
|
||||
return 0;
|
||||
}
|
||||
if (Interact(L))
|
||||
if (Interact(L, false))
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
@@ -91,7 +118,8 @@ int __fastcall Script_C_InteractUnit(void *L) {
|
||||
"looseTargeting]])");
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::PushBool(L, Interact(L));
|
||||
const bool loose = Game::Lua::Type(L, 3) <= Game::Lua::TYPE_NIL || Game::Lua::ToFlag(L, 3);
|
||||
Game::Lua::PushBool(L, Interact(L, loose));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -115,13 +143,15 @@ const Game::Doc::Field kManagerArgs[] = {
|
||||
"\"anyinteract\" is what the Interact Key uses."),
|
||||
Game::Doc::Opt("exactMatch", "bool", "false",
|
||||
"When a name is given, require the whole name instead of a prefix."),
|
||||
Game::Doc::Opt("looseTargeting", "bool", "true", "Has no effect."),
|
||||
Game::Doc::Opt("looseTargeting", "bool", "true",
|
||||
"Keep the current target: the unit becomes the target only when there is "
|
||||
"none."),
|
||||
};
|
||||
const Game::Doc::Field kManagerRets[] = {
|
||||
Game::Doc::Req("success", "bool", "True when the interaction ran."),
|
||||
};
|
||||
const Game::Doc::Function kManagerInteractUnit{
|
||||
"Targets the unit or object and interacts with it, as a right-click in the world does.",
|
||||
"Interacts with the unit or object, as a right-click in the world does.",
|
||||
kManagerArgs, kManagerRets, "PlayerInteractionManager"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
|
||||
Reference in New Issue
Block a user