630 lines
23 KiB
Lua
630 lines
23 KiB
Lua
--[[ ShardCap 2.0 -- keep your Soul Shards at a fixed cap.
|
|
WoW 1.12.1 (Lua 5.0). Based on ShardCap by "The Johnnest of them all" (dogmax),
|
|
with ideas from ShardCapPlus (akzkak/Paduk) and voxan-dev's fork.
|
|
|
|
Optional client-mod support (auto-detected, never required):
|
|
ClassicAPI - exact item IDs/stack counts, locale-free soul bag detection,
|
|
cursor-free item moves, BAG_UPDATE_DELAYED batching
|
|
Nampower - exact item IDs/stack counts via GetBagItem
|
|
]]
|
|
|
|
local ADDON = "ShardCap"
|
|
local VERSION = "2.1.1"
|
|
local SHARD_ID = 6265
|
|
|
|
-- Known soul bags (vanilla): Soul Pouch, Felcloth Bag, Core Felcloth Bag,
|
|
-- Small Soul Pouch, Box of Souls. Anything else is caught by the subtype check
|
|
-- or ClassicAPI; /shardcap soulbag <n> covers whatever slips through.
|
|
local SOULBAG_IDS = { [21340]=true, [21341]=true, [21342]=true, [22243]=true, [22244]=true }
|
|
local SOULBAG_SUBTYPES = { ["Soul Bag"]=true, ["Seelentasche"]=true }
|
|
-- Warlock reagents that WoW 1.18.1 Clients let you keep in a soul bag
|
|
local REAGENT_IDS = { [16583]=true, [5565]=true } -- Demonic Figurine, Infernal Stone
|
|
|
|
local DEFAULTS = {
|
|
cap = 12, -- shards to keep
|
|
notify = false, -- chat output when something is deleted/moved
|
|
sort = true, -- move shards from normal bags into free soul bag slots
|
|
combat = false, -- also trim while in combat
|
|
reagents = true, -- move Demonic Figurines / Infernal Stones into soul bags
|
|
soulbags = nil, -- nil = auto-detect, otherwise { [bagID]=true }
|
|
}
|
|
|
|
local db
|
|
local inCombat = false
|
|
local api = {}
|
|
|
|
------------------------------------------------------------------------------
|
|
-- Output
|
|
------------------------------------------------------------------------------
|
|
local function Print(msg)
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff9482c9ShardCap|r - " .. msg)
|
|
end
|
|
|
|
local function Notify(msg)
|
|
if db and db.notify then Print(msg) end
|
|
end
|
|
|
|
local function Plural(n, word)
|
|
if n == 1 then return n .. " " .. word end
|
|
return n .. " " .. word .. "s"
|
|
end
|
|
|
|
------------------------------------------------------------------------------
|
|
-- Client-mod detection
|
|
------------------------------------------------------------------------------
|
|
local function DetectAPIs()
|
|
api.classic = type(C_Container) == "table"
|
|
and type(C_Container.GetContainerItemInfo) == "function"
|
|
api.classicSwap = api.classic and type(C_Container.SwapItems) == "function"
|
|
api.classicBagType = api.classic and type(C_Container.GetContainerNumFreeSlots) == "function"
|
|
api.nampower = (not api.classic) and type(GetBagItem) == "function"
|
|
api.superwow = SUPERWOW_VERSION ~= nil
|
|
api.unitxp = type(UnitXP) == "function" and pcall(UnitXP, "nop", "nop") or false
|
|
end
|
|
|
|
------------------------------------------------------------------------------
|
|
-- Slot reading: returns itemID, stackCount, locked (or nil for empty)
|
|
------------------------------------------------------------------------------
|
|
local function SlotInfoVanilla(bag, slot)
|
|
local link = GetContainerItemLink(bag, slot)
|
|
if not link then return nil end
|
|
-- Exact ID match. The original string.find(link, 6265) also matched any
|
|
-- item whose link merely contained "6265" (e.g. item 16265) and deleted it.
|
|
local _, _, id = string.find(link, "item:(%d+)")
|
|
local _, count, locked = GetContainerItemInfo(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, slotBuffer)
|
|
if not info then return nil end
|
|
return info.itemID, info.stackCount or 1, info.isLocked
|
|
elseif api.nampower then
|
|
local ok, t = pcall(GetBagItem, bag, slot)
|
|
if not ok then
|
|
api.nampower = false -- unexpected API shape; fall back for good
|
|
return SlotInfoVanilla(bag, slot)
|
|
end
|
|
if not t then return nil end
|
|
local _, _, locked = GetContainerItemInfo(bag, slot)
|
|
return t.itemId, t.stackCount or 1, locked
|
|
end
|
|
return SlotInfoVanilla(bag, slot)
|
|
end
|
|
|
|
------------------------------------------------------------------------------
|
|
-- Soul bag detection
|
|
------------------------------------------------------------------------------
|
|
local function IsBit4Set(n) -- Lua 5.0 has no bit ops in every client
|
|
return math.mod(math.floor(n / 4), 2) == 1
|
|
end
|
|
|
|
local function AutoIsSoulBag(bag)
|
|
if bag == 0 then return false end
|
|
if api.classicBagType then
|
|
local _, bagType = C_Container.GetContainerNumFreeSlots(bag)
|
|
if bagType and bagType > 0 then return IsBit4Set(bagType) end
|
|
end
|
|
local link = GetInventoryItemLink("player", ContainerIDToInventoryID(bag))
|
|
if not link then return false end
|
|
local _, _, id = string.find(link, "item:(%d+)")
|
|
id = tonumber(id)
|
|
if not id then return false end
|
|
if SOULBAG_IDS[id] then return true end
|
|
local _, _, _, _, _, subType = GetItemInfo(id)
|
|
return subType ~= nil and SOULBAG_SUBTYPES[subType] == true
|
|
end
|
|
|
|
local function IsSoulBag(bag)
|
|
if db.soulbags then return db.soulbags[bag] == true end
|
|
return AutoIsSoulBag(bag)
|
|
end
|
|
|
|
------------------------------------------------------------------------------
|
|
-- Inventory scan
|
|
------------------------------------------------------------------------------
|
|
local function Scan()
|
|
local stacks, freeSoul, total, reagents, reagentTotals = {}, {}, 0, {}, {}
|
|
for bag = 0, 4 do
|
|
local soul = IsSoulBag(bag)
|
|
for slot = 1, GetContainerNumSlots(bag) do
|
|
local id, count, locked = SlotInfo(bag, slot)
|
|
if id == SHARD_ID then
|
|
table.insert(stacks, { bag = bag, slot = slot, count = count, locked = locked, soul = soul })
|
|
total = total + count
|
|
elseif not id and soul then
|
|
table.insert(freeSoul, { bag = bag, slot = slot })
|
|
elseif id and REAGENT_IDS[id] then
|
|
reagentTotals[id] = (reagentTotals[id] or 0) + count
|
|
end
|
|
if id and REAGENT_IDS[id] and not soul then
|
|
table.insert(reagents, { id = id, bag = bag, slot = slot, count = count, locked = locked })
|
|
end
|
|
end
|
|
end
|
|
return stacks, freeSoul, total, reagents, reagentTotals
|
|
end
|
|
|
|
-- Deletion order: shards outside soul bags first, backpack first,
|
|
-- and from the end of each bag; soul bag shards go last.
|
|
local function DeleteOrder(a, b)
|
|
if a.soul ~= b.soul then return not a.soul end
|
|
if a.bag ~= b.bag then return a.bag < b.bag end
|
|
return a.slot > b.slot
|
|
end
|
|
|
|
------------------------------------------------------------------------------
|
|
-- Actions (one per pass; the next pass runs after the server answers)
|
|
------------------------------------------------------------------------------
|
|
local function CursorBusy()
|
|
return CursorHasItem() or (SpellIsTargeting and SpellIsTargeting())
|
|
end
|
|
|
|
-- returns true if it did something
|
|
local function TrimStep(stacks, total)
|
|
local excess = total - db.cap
|
|
if excess <= 0 then return false end
|
|
table.sort(stacks, DeleteOrder)
|
|
for _, s in ipairs(stacks) do
|
|
if not s.locked then
|
|
local n = s.count
|
|
if n > excess then n = excess end
|
|
if n < s.count then
|
|
SplitContainerItem(s.bag, s.slot, n) -- delete exactly the excess
|
|
else
|
|
PickupContainerItem(s.bag, s.slot)
|
|
end
|
|
if CursorHasItem() then
|
|
DeleteCursorItem()
|
|
Notify("Deleted " .. Plural(n, "Soul Shard") .. " from bag " .. s.bag
|
|
.. " slot " .. s.slot .. " (" .. total .. " -> " .. (total - n) .. ", cap " .. db.cap .. ").")
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Max shards per slot: 3 on WoW 1.18.1 Clients, 1 on Blizzlike servers.
|
|
local function MaxStack()
|
|
if api.classic and C_Item and type(C_Item.GetItemMaxStackSizeByID) == "function" then
|
|
local ok, n = pcall(C_Item.GetItemMaxStackSizeByID, SHARD_ID)
|
|
if ok and type(n) == "number" and n > 0 then return n end
|
|
end
|
|
local _, _, _, _, _, _, n = GetItemInfo(SHARD_ID)
|
|
if type(n) == "number" and n > 0 then return n end
|
|
return 1
|
|
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
|
|
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)
|
|
else
|
|
PickupContainerItem(src.bag, src.slot)
|
|
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.
|
|
-- Fills the most-preferred partial stack (soul bag first) from the least-preferred one.
|
|
local function MergeStep(stacks, maxStack)
|
|
if maxStack <= 1 then return false end
|
|
local partial = {}
|
|
for _, s in ipairs(stacks) do
|
|
if s.count < maxStack and not s.locked then table.insert(partial, s) end
|
|
end
|
|
if table.getn(partial) < 2 then return false end
|
|
table.sort(partial, DeleteOrder) -- first = least preferred, last = most preferred
|
|
local src, dst = partial[1], partial[table.getn(partial)]
|
|
local n = maxStack - dst.count
|
|
if n > src.count then n = src.count end
|
|
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
|
|
if C_Container.SwapItems(s.bag, s.slot, dst.bag, dst.slot) == false then
|
|
moveRefused = true
|
|
return false
|
|
end
|
|
else
|
|
if not MoveShards(s, dst, s.count) then return false end
|
|
end
|
|
Notify("Requested move of " .. Plural(s.count, "Soul Shard") .. " into your soul bag (bag " .. dst.bag .. ").")
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
------------------------------------------------------------------------------
|
|
-- Warlock reagents -> soul bag (WoW 1.18.1 Clients allow these in soul bags)
|
|
------------------------------------------------------------------------------
|
|
local rejected = {} -- [itemID] = true: this server won't put it in a soul bag
|
|
local failures = {} -- [itemID] = moves the server silently refused
|
|
local lastReagentMove -- { id, bag, slot } of the last move we sent
|
|
|
|
local function ReagentAllowed(id)
|
|
if rejected[id] then return false end
|
|
if api.classic and C_Item and type(C_Item.GetItemFamily) == "function" then
|
|
local ok, fam = pcall(C_Item.GetItemFamily, id)
|
|
if ok and type(fam) == "number" and fam > 0 then return IsBit4Set(fam) end
|
|
end
|
|
-- Only servers where shards stack (WoW 1.18.1 Clients) allow this; on Blizzlike
|
|
-- servers the client would refuse the move with an error every time.
|
|
return MaxStack() > 1
|
|
end
|
|
|
|
local function Reject(id)
|
|
if rejected[id] then return end
|
|
rejected[id] = true
|
|
local name = GetItemInfo(id) or ("item " .. id)
|
|
Print(name .. " can't go in a soul bag on this server; won't try again this session.")
|
|
end
|
|
|
|
-- Soul bag slots to keep free for shards: enough to hold the cap,
|
|
-- and never less than the shard stacks still waiting outside.
|
|
local function ShardReserve(stacks)
|
|
local inSoul, outside = 0, 0
|
|
for _, s in ipairs(stacks) do
|
|
if s.soul then inSoul = inSoul + 1 else outside = outside + 1 end
|
|
end
|
|
local need = math.ceil(db.cap / MaxStack()) - inSoul
|
|
if need < outside then need = outside end
|
|
if need < 0 then need = 0 end
|
|
return need
|
|
end
|
|
|
|
local function ReagentStep(stacks, freeSoul, reagents)
|
|
if not db.reagents then return false end
|
|
-- did the last move silently fail (server refused, item still where it was)?
|
|
local last = lastReagentMove
|
|
lastReagentMove = nil
|
|
if last then
|
|
for _, r in ipairs(reagents) do
|
|
if r.id == last.id and r.bag == last.bag and r.slot == last.slot and not r.locked then
|
|
failures[r.id] = (failures[r.id] or 0) + 1
|
|
if failures[r.id] >= 2 then Reject(r.id) end
|
|
end
|
|
end
|
|
end
|
|
local free = table.getn(freeSoul)
|
|
if free <= ShardReserve(stacks) then return false end
|
|
for _, r in ipairs(reagents) do
|
|
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
|
|
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)
|
|
if CursorHasItem() then -- client refused it: put it back, stop trying
|
|
PickupContainerItem(r.bag, r.slot)
|
|
Reject(r.id)
|
|
return true
|
|
end
|
|
end
|
|
lastReagentMove = { id = r.id, bag = r.bag, slot = r.slot }
|
|
Notify("Requested move of " .. (GetItemInfo(r.id) or "reagent") .. " into your soul bag (bag " .. dst.bag .. ").")
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
------------------------------------------------------------------------------
|
|
-- Scheduler (hidden OnUpdate frame: zero cost while idle)
|
|
------------------------------------------------------------------------------
|
|
local frame = CreateFrame("Frame")
|
|
local nextRun, forced, passes = nil, false, 0
|
|
local MAX_PASSES = 60
|
|
|
|
local function Schedule(delay, force)
|
|
local t = GetTime() + (delay or 0.3)
|
|
if not nextRun or t < nextRun then nextRun = t end
|
|
if force then forced = true end
|
|
passes = 0
|
|
frame:Show()
|
|
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
|
|
return false -- PLAYER_REGEN_ENABLED will reschedule
|
|
end
|
|
if CursorBusy() then return true end -- try again shortly
|
|
local stacks, freeSoul, total, reagents = Scan()
|
|
if TrimStep(stacks, total) then return true end
|
|
-- 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
|
|
for _, r in ipairs(reagents) do if r.locked then return true end end
|
|
return lastReagentMove ~= nil -- one more pass to confirm the last move landed
|
|
end
|
|
|
|
frame:Hide()
|
|
frame:SetScript("OnUpdate", function()
|
|
if not nextRun or GetTime() < nextRun then return end
|
|
nextRun = nil
|
|
passes = passes + 1
|
|
local again = Pass()
|
|
if again and passes < MAX_PASSES then
|
|
nextRun = GetTime() + 0.3
|
|
else
|
|
forced = false
|
|
this:Hide()
|
|
end
|
|
end)
|
|
|
|
------------------------------------------------------------------------------
|
|
-- Saved variables & migration
|
|
------------------------------------------------------------------------------
|
|
local function InitDB()
|
|
if type(ShardCapDB) ~= "table" then
|
|
ShardCapDB = {}
|
|
-- migrate settings from ShardCap 1.x
|
|
if type(SHARDCAP_CAP_VALUE) == "number" then ShardCapDB.cap = SHARDCAP_CAP_VALUE end
|
|
if SHARDCAP_SPAM == true then ShardCapDB.notify = true end
|
|
end
|
|
SHARDCAP_CAP_VALUE, SHARDCAP_SPAM = nil, nil
|
|
db = ShardCapDB
|
|
for k, v in pairs(DEFAULTS) do
|
|
if db[k] == nil then db[k] = v end
|
|
end
|
|
end
|
|
|
|
local function WarnDuplicates()
|
|
if IsAddOnLoaded("ShardCapPlus") then
|
|
Print("|cffff5555ShardCapPlus is also enabled.|r Disable it - both would delete shards.")
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------------------
|
|
-- Events
|
|
------------------------------------------------------------------------------
|
|
frame:RegisterEvent("ADDON_LOADED")
|
|
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
frame:RegisterEvent("PLAYER_REGEN_DISABLED")
|
|
frame:RegisterEvent("PLAYER_REGEN_ENABLED")
|
|
|
|
frame:SetScript("OnEvent", function()
|
|
if event == "ADDON_LOADED" then
|
|
if arg1 ~= ADDON then return end
|
|
InitDB()
|
|
DetectAPIs()
|
|
-- ClassicAPI batches all BAG_UPDATEs of a frame into one event
|
|
if api.classic then
|
|
frame:RegisterEvent("BAG_UPDATE_DELAYED")
|
|
else
|
|
frame:RegisterEvent("BAG_UPDATE")
|
|
end
|
|
elseif event == "PLAYER_ENTERING_WORLD" then
|
|
inCombat = UnitAffectingCombat("player") and true or false
|
|
WarnDuplicates()
|
|
Schedule(2)
|
|
elseif event == "PLAYER_REGEN_DISABLED" then
|
|
inCombat = true
|
|
elseif event == "PLAYER_REGEN_ENABLED" then
|
|
inCombat = false
|
|
Schedule(0.5)
|
|
elseif event == "BAG_UPDATE" or event == "BAG_UPDATE_DELAYED" then
|
|
if not inCombat or db.combat then Schedule(0.3) end
|
|
end
|
|
end)
|
|
|
|
------------------------------------------------------------------------------
|
|
-- Slash commands
|
|
------------------------------------------------------------------------------
|
|
local function OnOff(v) if v then return "|cff55ff55on|r" end return "|cffff5555off|r" end
|
|
|
|
local function SoulBagList()
|
|
local list = {}
|
|
for bag = 1, 4 do if IsSoulBag(bag) then table.insert(list, bag) end end
|
|
if table.getn(list) == 0 then return "none" end
|
|
return table.concat(list, ", ")
|
|
end
|
|
|
|
local function Backend()
|
|
local parts = {}
|
|
if api.classic then table.insert(parts, "ClassicAPI")
|
|
elseif api.nampower then table.insert(parts, "Nampower")
|
|
else table.insert(parts, "vanilla API") end
|
|
return table.concat(parts, ", ")
|
|
end
|
|
|
|
-- Items in the bank (ClassicAPI only: C_Item.GetItemCount reads the bank
|
|
-- without it being open). Returns nil when unknown.
|
|
local function BankCount(id)
|
|
if not (api.classic and C_Item and type(C_Item.GetItemCount) == "function") then return nil end
|
|
local ok1, all = pcall(C_Item.GetItemCount, id, true)
|
|
local ok2, carried = pcall(C_Item.GetItemCount, id, false)
|
|
if not ok1 or not ok2 or type(all) ~= "number" or type(carried) ~= "number" then return nil end
|
|
if all > carried then return all - carried end
|
|
return 0
|
|
end
|
|
|
|
local function BankSuffix(id)
|
|
local n = BankCount(id)
|
|
if n and n > 0 then return " |cffaaaaaa(+" .. n .. " in bank)|r" end
|
|
return ""
|
|
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 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" .. figurines .. "|r" .. BankSuffix(16583)
|
|
.. " Infernal Stones: |cffffffff" .. stones .. "|r" .. BankSuffix(5565),
|
|
"Soul bags: " .. SoulBagList() .. (db.soulbags and " (manual)" or " (auto)")
|
|
.. ", " .. free .. " free soul slots"
|
|
end
|
|
|
|
local function PrintStatus()
|
|
local shards, reagents, bags = StatusLines()
|
|
Print(shards .. " (v" .. VERSION .. ")")
|
|
Print(reagents)
|
|
Print(bags)
|
|
Print("Notify " .. OnOff(db.notify) .. ", sort " .. OnOff(db.sort)
|
|
.. ", trim in combat " .. OnOff(db.combat) .. ", reagents " .. OnOff(db.reagents)
|
|
.. " [" .. Backend() .. "]")
|
|
end
|
|
|
|
local function PrintHelp()
|
|
Print("/shardcap |cffffffff<number>|r - set the cap (0-200)")
|
|
Print("/shardcap |cffffffffstatus|r - show cap, shard count and settings")
|
|
Print("/shardcap |cffffffffdelete|r - trim to the cap right now (even in combat)")
|
|
Print("/shardcap |cffffffffspam|r - toggle chat notifications")
|
|
Print("/shardcap |cffffffffsort|r - toggle moving shards into your soul bag")
|
|
Print("/shardcap |cffffffffcombat|r - toggle trimming during combat")
|
|
Print("/shardcap |cffffffffreagents|r - toggle moving Demonic Figurines / Infernal Stones into the soul bag")
|
|
Print("/shardcap |cffffffffsoulbag auto|1-4|r - auto-detect or toggle a bag as soul bag")
|
|
Print("/shardcap |cffffffffoptions|r - open the options window (ClassicAPI)")
|
|
Print("/shardcap |cffffffffreset|r - restore defaults")
|
|
Print("Trims when you leave combat, deleting shards outside your soul bag first.")
|
|
end
|
|
|
|
local function SetSoulBag(arg)
|
|
if arg == "auto" or arg == "" then
|
|
db.soulbags = nil
|
|
Print("Soul bag detection: auto (" .. SoulBagList() .. ").")
|
|
return
|
|
end
|
|
local bag = tonumber(arg)
|
|
if not bag or bag < 1 or bag > 4 or bag ~= math.floor(bag) then
|
|
Print("Use a bag number 1-4, or 'auto'.")
|
|
return
|
|
end
|
|
if not db.soulbags then
|
|
db.soulbags = {}
|
|
for b = 1, 4 do if AutoIsSoulBag(b) then db.soulbags[b] = true end end
|
|
end
|
|
if db.soulbags[bag] then db.soulbags[bag] = nil else db.soulbags[bag] = true end
|
|
Print("Soul bags (manual): " .. SoulBagList() .. ". '/shardcap soulbag auto' to undo.")
|
|
Schedule(0.3)
|
|
end
|
|
|
|
SLASH_SHARDCAP1 = "/shardcap"
|
|
SLASH_SHARDCAP2 = "/scap"
|
|
SlashCmdList["SHARDCAP"] = function(msg)
|
|
if not db then return end
|
|
msg = string.lower(msg or "")
|
|
local _, _, cmd, rest = string.find(msg, "^%s*(%S*)%s*(.-)%s*$")
|
|
cmd, rest = cmd or "", rest or ""
|
|
|
|
local n = tonumber(cmd)
|
|
if n then
|
|
if n < 0 or n > 200 or n ~= math.floor(n) then
|
|
Print("The cap must be a whole number from 0 to 200.")
|
|
return
|
|
end
|
|
db.cap = n
|
|
Print("Cap set to " .. Plural(n, "shard") .. ".")
|
|
Schedule(0.3)
|
|
elseif cmd == "" or cmd == "status" then
|
|
PrintStatus()
|
|
if cmd == "" then Print("Type |cffffffff/shardcap help|r for commands.") end
|
|
elseif cmd == "help" or cmd == "info" then
|
|
PrintHelp()
|
|
elseif cmd == "delete" or cmd == "trim" then
|
|
Schedule(0, true)
|
|
elseif cmd == "spam" or cmd == "notify" then
|
|
db.notify = not db.notify
|
|
Print("Notifications " .. OnOff(db.notify) .. ".")
|
|
elseif cmd == "sort" then
|
|
db.sort = not db.sort
|
|
Print("Moving shards into soul bags " .. OnOff(db.sort) .. ".")
|
|
Schedule(0.3)
|
|
elseif cmd == "combat" then
|
|
db.combat = not db.combat
|
|
Print("Trimming during combat " .. OnOff(db.combat) .. ".")
|
|
elseif cmd == "reagents" then
|
|
db.reagents = not db.reagents
|
|
Print("Moving Demonic Figurines / Infernal Stones into the soul bag " .. OnOff(db.reagents) .. ".")
|
|
Schedule(0.3)
|
|
elseif cmd == "soulbag" then
|
|
SetSoulBag(rest)
|
|
elseif cmd == "options" or cmd == "config" or cmd == "opt" then
|
|
if ShardCapAPI.OpenOptions then
|
|
ShardCapAPI.OpenOptions()
|
|
else
|
|
Print("The options window needs ClassicAPI (AddOn Options). Use |cffffffff/shardcap help|r for commands.")
|
|
end
|
|
elseif cmd == "reset" then
|
|
ShardCapDB = nil
|
|
InitDB()
|
|
Print("Settings reset. Cap is " .. db.cap .. ".")
|
|
else
|
|
PrintHelp()
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------------------
|
|
-- Internal API for Options.lua (not a stable public interface)
|
|
------------------------------------------------------------------------------
|
|
ShardCapAPI = {
|
|
VERSION = VERSION,
|
|
DEFAULTS = DEFAULTS,
|
|
GetDB = function() return db end,
|
|
Schedule = Schedule,
|
|
IsSoulBag = IsSoulBag,
|
|
AutoIsSoulBag = AutoIsSoulBag,
|
|
StatusLines = StatusLines,
|
|
Backend = Backend,
|
|
Print = Print,
|
|
OpenOptions = nil, -- set by Options.lua when an options window is available
|
|
}
|