Compare commits

...
10 Commits
Author SHA1 Message Date
Brues 6ed0edc947 feat(baselib): gmatch, table.removemulti, math hyperbolics, difftime, newproxy, scrub
The base-library names that 1.12.1's Lua 5.0 and its compat.lua do not
have:

- gmatch: the global alias of string.gmatch (5.0's str_gfind,
  0x7FCFA0), set by value like strmatch and strrev.
- table.removemulti(t [, pos [, count]]): removes count elements from
  pos, shifts the rest down with lua_rawgeti/lua_rawseti and returns the
  removed elements. 5.0 keeps the length apart from the keys, so like
  the engine's table.remove (0x7FB770) it records the new length with
  luaL_setn (0x6F4EA0).
- math.cosh, math.sinh, math.tanh: luaL_checknumber (0x6F4C80) and the
  C function.
- difftime(t2 [, t1]): C difftime over the two times, next to the
  engine's date and time globals.
- newproxy([false | true | proxy]): an empty userdata from
  lua_newuserdata. The closure's upvalue is a weak table (its own
  metatable, __mode "kv") that marks the metatables newproxy(true)
  makes, so only those can be shared.
- scrub(...): replaces each argument that is not a boolean, number or
  string with nil, in place with lua_replace (0x6F32B0).

All are registered on both Lua states.

Game::Lua gains RawSetI (lua_rawseti, 0x6F3F60), Replace, CheckLString
(luaL_checklstring, 0x6F4BE0), CheckNumber and OptNumber (luaL_optnumber,
0x6F4CD0); string.byte now uses them instead of local typedefs.

issecure, issecurevariable and forceinsecure are left out. 1.12.1 has
no taint, so they could only return constants, and a constant
"secure" from issecurevariable makes AceHook-3.0 refuse every plain
hook.
2026-11-03 14:23:25 -06:00
Brues 1645e6b43c feat(baselib): string.byte returns a range of byte codes
1.12.1's str_byte (0x7FC210) is Lua 5.0's: it reads only the start
position and pushes one code, so string.byte(s, 1, -1) silently returns
the first byte alone.

string.byte and the strbyte global now take (s [, i [, j]]) and push the
codes of bytes i..j, with j defaulting to i. Negative positions count
from the end and the range is clamped to the string. The arguments are
read with the engine's luaL_checklstring (0x6F4BE0) and luaL_optnumber
(0x6F4CD0), and positions truncate toward zero as the engine's str_byte
and str_sub do, so a call without j returns exactly what it did before.

Each code takes a stack slot, reserved with lua_checkstack (0x6F2F30).
The engine refuses more than 2048 slots, so a range longer than about
2000 bytes raises "stack overflow (string slice too long)".

compat.lua set strbyte from the old string.byte when the state opened,
so strbyte is re-aliased to the new table entry.
2026-11-03 14:00:10 -06:00
Brues efcdc54a50 feat(baselib): string.gsub takes 5.1 string and table replacements
1.12.1's str_gsub (0x7FD0E0) is Lua 5.0's. Its replacement argument
differs from 5.1 in four ways: a table is rejected ("string or function
expected"); in a replacement string, %0, and %1 when the pattern has no
captures, raise "invalid capture index" (add_s checks every %n with
check_capture, 0x7FCE20); a function that returns nil or false deletes
the match instead of keeping it; and one that returns a table deletes
it too instead of raising an error.

The first two only ever raised errors, so the 5.1 behavior cannot break
an existing addon. The function rules can: SuperAPI's
CombatText_AddMessage returns nil to delete the player's own " [0x...]"
GUID from heal text. So function replacements keep 1.12.1's behavior,
and only string and table replacements change. A scan of every gsub
call in the installed addons and the stock FrameXML found no other code
that depends on the old string or function results.

str_gsub's match loop, anchor, count and buffer stay the engine's. Two
parts are replaced:

- The 60-byte replacement-type gate at 0x7FD15E (lua_gettop / isstring /
  lua_type == function, then luaL_argerror) becomes a call to a
  __fastcall check with ECX = L that accepts a number, string, function
  or table and raises "string/function/table expected" otherwise.
- The one call to add_s (0x7FD203 -> 0x7FD2A0, __fastcall ms/b + s/e on
  the stack, ret 8) goes to a function with the same signature. A
  function replacement calls the engine's add_s unchanged. A string or
  number gets 5.1's add_s, and a table gets 5.1's lookup: the first
  capture or whole match as the key, a nil or false value keeps the
  match, and any other non-string value raises "invalid replacement
  value". Both use the engine's push_onecapture (0x7FCF40, plus 5.1's
  whole-match case for index 0 with no captures), luaL_addlstring
  (0x6F51F0) and luaL_addvalue (0x6F5270).

Both patches check the original bytes or call target and are skipped
when they differ.

The table-replacement shim in StringLib is removed. It wrapped the
pattern in an outer capture so a closure could return the whole match,
and deleted the match for a missing key when the pattern had a
back-reference. string.gsub and gsub are the engine's function again.
2026-11-03 14:00:04 -06:00
Brues 6a24271cd1 feat(baselib): two-digit %n$ argument numbers in string.format
1.12.1's str_format (0x7FD3D0) only reads a one-digit argument number:
isdigit(p[0]) && p[1] == '$'. In "%10$s" the "10" is read as a width and
the '$' as the conversion, which raises "invalid option in `format'".
Combat log strings such as ACTION_SPELL_PERIODIC_LEECH_RESULT,
"(%10$s %8$s Gained)", cannot be formatted.

Replace the engine's 18-byte one-digit branch at 0x7FD4AC with a call to
a naked thunk that sets the argument number ([ebp-8]) and the flags scan
start (EBX) for both "n$" and "nn$". EAX/ECX/EDX are dead at the site.
The engine still tests the first digit with its own isdigit, increments
the argument after the site, and does every conversion, so string.format,
format and the C callers of str_format all get the change. The patch
checks the original bytes and is skipped when they differ.
2026-11-03 13:07:32 -06:00
MrBCatandBrues a7ce7555f0 fix(softinteract): anyinteract yields when the target is out of reach (#121)
* fix(softinteract): anyinteract yields when the target is out of reach

GetAny treated Reach::Out — the cursor's Unable form — as a usable
target, so with a target too far away the Interact Key pressed the inert
target instead of the nearest object it could interact with.

Prefer the target only while the cursor says it can be used now
(Reach::Now), and fall back to softinteract otherwise. A target behind
the camera still wins while it is in reach: the cursor verdict does not
look at facing.

* docs(api): anyinteract needs the target to be in reach now

The token section said a target that is too far away still counts, which
the implementation no longer does.

* fix(softinteract): anyinteract keeps an out-of-reach target when nothing is picked

Retail 12.1.5, targeting an NPC that is out of reach: facing away from
everything (softinteract nil), anyinteract is the target; facing a nearer
NPC, it is that NPC. The previous change returned 0 in the first case,
so the Interact Key lost the engine's own out-of-reach handling on the
target (the "need to be closer" error, or the click-to-move walk).

GetAny now reads the target's cursor verdict once: in reach wins, then
softinteract, then the target while it is interactable but out of reach.

* feat(interact): honor looseTargeting in C_PlayerInteractionManager.InteractUnit

On retail the Interact Key on a nearer NPC opens its dialog while a
farther unit stays targeted; with no target it targets the NPC. The key
calls InteractUnit("anyinteract") with looseTargeting at its default,
true, which ClassicAPI ignored, so the right-click's select replaced the
target.

FUN_ON_SPRITE_RIGHT_CLICK (0x00492820) is the active-player gate, the
select (FUN_SET_SELECTION_BY_GUID, 0x00493540), then the object's
right-click virtual (vtable +0x60, call at 0x00492881) with
FUN_GAMEUI_IS_KEY_DOWN(0). The select stores only units, so only a unit
other than the current target would move it. With looseTargeting and a
target, that one case runs the gate and the virtual without the select;
every other case still calls the engine function, which nampower also
detours for its right-click guards. The global InteractUnit keeps
targeting.

---------

Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
2026-11-03 12:39:54 -06:00
Brues 04790cb6c1 feat(options): name the page scroll bars for skins
Each options page's MinimalScrollBar is now ClassicAPIOptionsScrollBarN,
numbered like the ClassicAPIOptionsControlN controls, so a skin can find
the scroll bars the same way it finds the controls.
2026-11-03 00:54:42 -06:00
Brues e7ecae56da feat(options): key binding rows for ClassicAPI's bindings
The Interact Key page gets rows for Interact With Target and Interact
With Mouseover, and Bags and Items gets AoE Loot under a Loot header.
A row is retail's settings-page binding row: the binding's name, then a
button for each of its two keys. Left click a button and press a key,
mouse button 3-5 or the wheel to bind it; Escape cancels; right click
unbinds that key. The tooltip on a bound key and the page's output line
use retail's strings, added to every locale with Blizzard's translations.

The binding is 1.12's Key Bindings window's (Blizzard_BindingUI): the
same ALT-CTRL-SHIFT chord order, the same skipped modifier keys, the same
KEY_UNBOUND_ERROR check for an action that loses its last key, and the
same put-back when SetBinding refuses a key. A change saves at once with
SaveBindings(GetCurrentBindingSet()), so it lands in the account or
character set in use, as every other control on these pages writes at
once.

While a button listens, a full-screen button on FULLSCREEN_DIALOG takes
keys, clicks and the wheel, as retail's KeybindListener and its input
blocker do. A left or right click on it cancels, and so does hiding the
page.
2026-11-03 00:54:35 -06:00
Brues 65c6b0b271 feat(action): spell search, slot item and #showtooltip action queries
C_ActionBar.FindSpellActionButtons, HasSpellActionButtons and
IsOnBarOrSpecialBar find a spell on the bars. A slot matches on the
spell ID stored in VAR_ACTION_TABLE under the spell tag, the same test
4.3.4's PutActionInSlot makes, so a macro or item that casts the spell
does not count and a rank matches only itself. A name resolves through
the engine's spellbook resolver. IsOnBarOrSpecialBar also reads the
shapeshift bar's spell array (pointer at 0x00B71108, count at
0x00B71104, as Script_GetShapeshiftFormInfo 0x004B45C0 reads them) and
the pet bar's spell entries (types 1-5, spell ID in the low 16 bits, as
Script_GetPetActionInfo 0x004BDC50 reads them, gated on the pet GUID).
Enum.ActionBarSet is registered; all 120 slots are its Mkb set.

IsStackableAction (global, 1/nil) and C_ActionBar.IsStackableAction
(bool) answer 4.3.4's IsStackableAucItem: the slot's item has a max stack
above 1. C_ActionBar.GetItemActionOnEquipSpellID returns the item's
first spell with trigger 1 (on equip). The item is the item-by-ID
action's, or the one a macro's #showtooltip / #show resolved to, as
4.3.4's CGActionBar::GetItem returns it. Both peek the item cache and
never query it.

C_ActionBar.IsMacroActionWithShowTooltip parses the macro's body with
the parse Macro::ShowTooltip hands the tooltip over on, so it answers
while that module yields or a publisher owns the macro.
2026-11-03 00:37:16 -06:00
Brues 5f94fb98bd feat(action): C_ActionBar.GetSpell
C_ActionBar.GetSpell(actionID) returns the spell an action-bar slot
casts, or 0. It is the engine's own slot resolver (0x004E5A50), which
the cooldown, usable and range readers already go through: a spell
action is its spell, a macro is its cached primary spell (+0x564, the
one #showtooltip sets), and an item-by-ID action is the first on-use
spell in its item cache record. An empty slot, a slot outside 1..120,
an equipment set or a macro whose macro is gone gives 0. The spellbook
flag the resolver also hands back is not part of the answer.
2026-11-03 00:16:02 -06:00
Brues 426f6b6ec9 fix(container): free slots count the bank and use the engine's counter
GetContainerNumFreeSlots and GetContainerFreeSlots now take the main bank
(-1) and the bank bags (5..10) while a banker is open. The main bank is
gated on the banker GUID (0x00BDD038); bank bags get the gate from the
engine's container-GUID getter (0x004F93E0), which answers 0 for them
while the bank is closed.

A bag's empty slots are counted by the engine's own CountFreeSlots
(0x00622910, __thiscall on the bag's inventory list), and its slot count
comes from that live list instead of the item record. The backpack and
main bank are ranges of the player's list, walked slot by slot. Nothing
walks PackBagSlot per slot any more, so the Lua stack is left alone.

A container that is not there (keyring, empty bag slot, closed bank) now
returns only 0, leaving bagFamily nil.

The bag view SortBags kept for itself moves to Container::Bag so both
modules share it, and Item::Location::EquippedBagInventory becomes
BagInventory (1..10) beside a new BagContainer.

C_Item.GetItemData's bagFamily now goes through BitmaskForRecord, so a
bag whose family field is empty reports the same family there as it does
from GetItemFamily (a Soul Pouch was 0 there, 0x4 here).
2026-11-02 21:03:01 -06:00
42 changed files with 1920 additions and 468 deletions
+3 -3
View File
@@ -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
View File
@@ -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"`.
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
+180
View File
@@ -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
+61
View File
@@ -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};
+148
View File
@@ -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
+58
View File
@@ -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
View File
@@ -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.
+78
View File
@@ -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
+63
View File
@@ -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
+86
View File
@@ -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
+163
View File
@@ -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
View File
@@ -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);
+2 -2
View File
@@ -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 =
+104
View File
@@ -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
+47
View File
@@ -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
View File
@@ -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;
+7 -90
View File
@@ -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件物品";
+4
View File
@@ -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;
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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) {
+9
View File
@@ -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;
+5
View File
@@ -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.
+68
View File
@@ -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
+21 -6
View File
@@ -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; }
+3 -2
View File
@@ -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
View File
@@ -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() {