From e23e2c44540949b9bb86f9e68cfe7c3f7b059a29 Mon Sep 17 00:00:00 2001 From: OEVG <950+oevg@noreply.octowow.st> Date: Fri, 6 Nov 2026 16:14:25 -0400 Subject: [PATCH] Improve ClassicAPI scans and move request handling --- CHANGELOG.md | 9 +++++++ README.md | 3 ++- ShardCap.lua | 64 ++++++++++++++++++++++++++++++++++++++------------ ShardCap.toc | 4 ++-- tests/test.lua | 55 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1b47424 --- /dev/null +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 333e02f..20bb6b8 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/ShardCap.lua b/ShardCap.lua index 642ebce..17e3e49 100644 --- a/ShardCap.lua +++ b/ShardCap.lua @@ -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() diff --git a/ShardCap.toc b/ShardCap.toc index b564ce3..df49fe0 100644 --- a/ShardCap.toc +++ b/ShardCap.toc @@ -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 diff --git a/tests/test.lua b/tests/test.lua index 5328e1f..f2f1177 100644 --- a/tests/test.lua +++ b/tests/test.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