Improve ClassicAPI scans and move request handling

This commit is contained in:
2026-11-06 16:14:25 -04:00
parent d66c6e59cb
commit e23e2c4454
5 changed files with 117 additions and 18 deletions
+9
View File
@@ -0,0 +1,9 @@
# Changelog
## 2.1.1 - 2026-11-06
- Reuse ClassicAPI's item-info buffer during bag scans, with compatibility for older builds.
- Use a count-only scan for status text instead of allocating sorting records.
- Stop the current pass when ClassicAPI refuses to send a move request. Do not report or track it as sent; later bag events can retry.
- Describe move and merge notifications as requests, since the server confirms them asynchronously.
- Shard deletion, sorting priorities, and soul-bag reserve rules are unchanged.
+2 -1
View File
@@ -1,4 +1,4 @@
# ShardCap 2.1 (WoW 1.12.1 and WoW 1.18.1 Clients)
# ShardCap 2.1.1 (WoW 1.12.1 and WoW 1.18.1 Clients)
[![ClassicAPI](https://img.shields.io/badge/ClassicAPI-Optional-yellow.svg)](https://github.com/brues-code/ClassicAPI)
[![Nampower](https://img.shields.io/badge/Nampower-Optional-yellow.svg)](https://github.com/brues-code/nampower)
@@ -64,6 +64,7 @@ it — the addon warns you if both are on.
detection (works for any custom soul bag on WoW 1.18.1 Clients), cursor-free moves/merges, and `BAG_UPDATE_DELAYED`
so a loot or trade burst triggers one scan instead of many. Also provides the AddOn Options window
ShardCap's options pages live in, and `C_Item.GetItemCount` for showing what's in your bank.
Newer builds reuse an item-info buffer to reduce scan garbage; older builds still work.
- **Nampower** — used if ClassicAPI isn't: `GetBagItem` gives the item ID without parsing links.
Small gain; the vanilla path is already exact.
- **SuperWoW / UnitXP SP3** — not used. Neither adds anything for bags or items.
+49 -15
View File
@@ -9,7 +9,7 @@
]]
local ADDON = "ShardCap"
local VERSION = "2.1.0"
local VERSION = "2.1.1"
local SHARD_ID = 6265
-- Known soul bags (vanilla): Soul Pouch, Felcloth Bag, Core Felcloth Bag,
@@ -75,9 +75,12 @@ local function SlotInfoVanilla(bag, slot)
return tonumber(id), count or 1, locked
end
-- Older ClassicAPI builds ignore the third argument and return a fresh table.
-- Always read the returned table, and never read the buffer for an empty slot.
local slotBuffer = {}
local function SlotInfo(bag, slot)
if api.classic then
local info = C_Container.GetContainerItemInfo(bag, slot)
local info = C_Container.GetContainerItemInfo(bag, slot, slotBuffer)
if not info then return nil end
return info.itemID, info.stackCount or 1, info.isLocked
elseif api.nampower then
@@ -199,10 +202,14 @@ local function MaxStack()
end
-- Move exactly n shards from src onto dst (dst empty or a partial shard stack).
local moveRefused = false -- stop this pass when no request was sent
local function MoveShards(src, dst, n)
if api.classic and type(C_Container.MoveItem) == "function" then
C_Container.MoveItem(src.bag, src.slot, dst.bag, dst.slot, n)
return
if C_Container.MoveItem(src.bag, src.slot, dst.bag, dst.slot, n) == false then
moveRefused = true
return false
end
return true
end
if n < src.count then
SplitContainerItem(src.bag, src.slot, n)
@@ -211,6 +218,7 @@ local function MoveShards(src, dst, n)
end
PickupContainerItem(dst.bag, dst.slot)
if CursorHasItem() then PickupContainerItem(src.bag, src.slot) end -- put it back
return true
end
-- WoW 1.18.1 Clients: merge partial stacks so shards take as few slots as possible.
@@ -226,24 +234,28 @@ local function MergeStep(stacks, maxStack)
local src, dst = partial[1], partial[table.getn(partial)]
local n = maxStack - dst.count
if n > src.count then n = src.count end
MoveShards(src, dst, n)
Notify("Merged " .. Plural(n, "Soul Shard") .. " into bag " .. dst.bag .. " slot " .. dst.slot .. ".")
if not MoveShards(src, dst, n) then return false end
Notify("Requested merge of " .. Plural(n, "Soul Shard") .. " into bag " .. dst.bag .. " slot " .. dst.slot .. ".")
return true
end
local function SortStep(stacks, freeSoul)
if not db.sort then return false end
if MergeStep(stacks, MaxStack()) then return true end
if moveRefused then return false end
if table.getn(freeSoul) == 0 then return false end
for _, s in ipairs(stacks) do
if not s.soul and not s.locked then
local dst = freeSoul[1]
if api.classicSwap then
C_Container.SwapItems(s.bag, s.slot, dst.bag, dst.slot)
if C_Container.SwapItems(s.bag, s.slot, dst.bag, dst.slot) == false then
moveRefused = true
return false
end
else
MoveShards(s, dst, s.count)
if not MoveShards(s, dst, s.count) then return false end
end
Notify("Moved " .. Plural(s.count, "Soul Shard") .. " into your soul bag (bag " .. dst.bag .. ").")
Notify("Requested move of " .. Plural(s.count, "Soul Shard") .. " into your soul bag (bag " .. dst.bag .. ").")
return true
end
end
@@ -307,7 +319,10 @@ local function ReagentStep(stacks, freeSoul, reagents)
if not r.locked and ReagentAllowed(r.id) then
local dst = freeSoul[free] -- fill from the end, leave the front for shards
if api.classicSwap then
C_Container.SwapItems(r.bag, r.slot, dst.bag, dst.slot)
if C_Container.SwapItems(r.bag, r.slot, dst.bag, dst.slot) == false then
moveRefused = true
return false
end
else
PickupContainerItem(r.bag, r.slot)
PickupContainerItem(dst.bag, dst.slot)
@@ -318,7 +333,7 @@ local function ReagentStep(stacks, freeSoul, reagents)
end
end
lastReagentMove = { id = r.id, bag = r.bag, slot = r.slot }
Notify("Moved " .. (GetItemInfo(r.id) or "reagent") .. " into your soul bag (bag " .. dst.bag .. ").")
Notify("Requested move of " .. (GetItemInfo(r.id) or "reagent") .. " into your soul bag (bag " .. dst.bag .. ").")
return true
end
end
@@ -341,6 +356,7 @@ local function Schedule(delay, force)
end
local function Pass()
moveRefused = false
if not db then return false end
local manual = forced
if (inCombat or UnitAffectingCombat("player")) and not db.combat and not manual then
@@ -352,7 +368,9 @@ local function Pass()
-- don't shuffle bags mid-combat even if combat trimming is on
if not (inCombat or UnitAffectingCombat("player")) then
if SortStep(stacks, freeSoul) then return true end
if moveRefused then return false end
if ReagentStep(stacks, freeSoul, reagents) then return true end
if moveRefused then return false end
end
-- retry while stacks are still locked by a pending server transaction
for _, s in ipairs(stacks) do if s.locked then return true end end
@@ -468,15 +486,31 @@ local function BankSuffix(id)
end
-- Status lines shared by /shardcap and the options panel
-- Counts only: the display does not need sorting records or free-slot tables.
local function StatusCounts()
local shards, figurines, stones, free = 0, 0, 0, 0
for bag = 0, 4 do
local soul = IsSoulBag(bag)
for slot = 1, GetContainerNumSlots(bag) do
local id, count = SlotInfo(bag, slot)
if id == SHARD_ID then shards = shards + count
elseif id == 16583 then figurines = figurines + count
elseif id == 5565 then stones = stones + count
elseif not id and soul then free = free + 1 end
end
end
return shards, figurines, stones, free
end
local function StatusLines()
local _, freeSoul, total, _, reagentTotals = Scan()
local total, figurines, stones, free = StatusCounts()
local color = "|cffffffff"
if total > db.cap then color = "|cffff9900" end -- over the cap (e.g. in combat)
return "Soul Shards: " .. color .. total .. "/" .. db.cap .. "|r" .. BankSuffix(SHARD_ID),
"Demonic Figurines: |cffffffff" .. (reagentTotals[16583] or 0) .. "|r" .. BankSuffix(16583)
.. " Infernal Stones: |cffffffff" .. (reagentTotals[5565] or 0) .. "|r" .. BankSuffix(5565),
"Demonic Figurines: |cffffffff" .. figurines .. "|r" .. BankSuffix(16583)
.. " Infernal Stones: |cffffffff" .. stones .. "|r" .. BankSuffix(5565),
"Soul bags: " .. SoulBagList() .. (db.soulbags and " (manual)" or " (auto)")
.. ", " .. table.getn(freeSoul) .. " free soul slots"
.. ", " .. free .. " free soul slots"
end
local function PrintStatus()
+2 -2
View File
@@ -1,8 +1,8 @@
## Interface: 11200
## Title: ShardCap |cff9482c92.1|r
## Title: ShardCap |cff9482c92.1.1|r
## Notes: Keeps your Soul Shards at a cap. Stack-aware for WoW 1.18.1 Clients, soul bag aware. Type /shardcap
## Author: The Johnnest of them all; stacking from ShardCapPlus (Paduk); 2.0 rework
## Version: 2.1.0
## Version: 2.1.1
## SavedVariablesPerCharacter: ShardCapDB, SHARDCAP_CAP_VALUE, SHARDCAP_SPAM
ShardCap.lua
Options.lua
+55
View File
@@ -467,5 +467,60 @@ check(table.getn(categories) == 0 and string.find(chat[1] or "", "needs ClassicA
SlashCmdList.SHARDCAP("")
check(string.find(chat[2] or "", "5/12", 1, true) == nil and string.find(chat[2] or "", "in bank", 1, true) == nil, "no bank text without ClassicAPI")
-- New ClassicAPI buffer contract: empty slots leave stale buffer fields intact.
reset(); setBag(0, 4); setBag(4, 4, 22243)
put(0, 1, 6265, 3); put(0, 2, 16583, 2); put(4, 1, 5565, 1)
load(true)
local bufferSeen, bufferCalls = nil, 0
C_Container.GetContainerItemInfo = function(b, s, buf)
check(type(buf) == "table" and (not bufferSeen or bufferSeen == buf), "one shared item-info buffer")
bufferSeen = buf; bufferCalls = bufferCalls + 1
local it = bags[b].slots[s]
if not it then return nil end
buf.itemID, buf.stackCount, buf.isLocked = it.id, it.count, false
return buf
end
local shards, reagents, soul = ShardCapAPI.StatusLines()
check(bufferCalls == 8 and string.find(shards, "3/12", 1, true) ~= nil,
"buffer scan ignores stale data in empty slots")
check(string.find(reagents, "2|r", 1, true) ~= nil and string.find(reagents, "1|r", 1, true) ~= nil
and string.find(soul, "3 free soul slots", 1, true) ~= nil, "count-only status includes reagents and free soul slots")
-- Status refresh must not build action records (table.insert is used by Scan).
local oldInsert = table.insert
table.insert = function(t, value)
if type(value) == "table" then error("status allocated an action record") end
return oldInsert(t, value)
end
local statusOK = pcall(ShardCapAPI.StatusLines)
table.insert = oldInsert
check(statusOK, "status avoids sorting records")
run()
check(count() == 3 and count(0) == 0 and not F.shown,
"action scans copy values from the shared buffer before reusing it")
-- Refused sends must stop rather than notify, retry, or reject a reagent.
for _, kind in ipairs({ "merge", "shard", "reagent", "shard MoveItem" }) do
reset(); setBag(0, 4); setBag(4, 8, 22243)
if kind == "merge" then put(0, 1, 6265, 1); put(4, 1, 6265, 1)
elseif kind ~= "reagent" then put(0, 1, 6265, 3)
else put(0, 1, 16583, 2) end
load(true); ShardCapDB.notify = true
if kind == "shard MoveItem" then
C_Container.SwapItems = nil
fire("ADDON_LOADED", "ShardCap") -- detect MoveItem without SwapItems
end
local calls = 0
local function refuse() calls = calls + 1; return false end
local move, swap = C_Container.MoveItem, C_Container.SwapItems
C_Container.MoveItem, C_Container.SwapItems = refuse, refuse
chat = {}; run()
check(calls == 1 and table.getn(chat) == 0 and not F.shown,
kind .. " refused send stops without a success notice or polling")
C_Container.MoveItem, C_Container.SwapItems = move, swap
fire("BAG_UPDATE_DELAYED"); run()
check(not F.shown and (kind == "reagent" and bags[0].slots[1] == nil
or kind ~= "reagent" and count(0) == 0), kind .. " retries on a later bag event")
end
print(string.format("\n%d passed, %d failed", passes, fails))
if fails > 0 then os.exit(1) end