Improve ClassicAPI scans and move request handling
This commit is contained in:
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
[](https://github.com/brues-code/ClassicAPI)
|
||||
[](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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user