Apply Patch
This commit is contained in:
+330
@@ -0,0 +1,330 @@
|
||||
--[[ ShardCap options window, shown under AddOns in ClassicAPI's AddOn Options
|
||||
(Game Menu > AddOn Options, or /shardcap options).
|
||||
|
||||
Uses the later-client panel protocol that ClassicAPI backports
|
||||
(InterfaceOptions_AddCategory with okay / cancel / default / refresh).
|
||||
Without it this file does nothing and the slash commands still work.
|
||||
|
||||
Changes apply at once. Cancel (and Escape) puts back the settings you had
|
||||
when the window was opened; Defaults resets the page that is shown.
|
||||
Lua 5.0: no `...` expressions, no select().
|
||||
]]
|
||||
|
||||
if type(InterfaceOptions_AddCategory) ~= "function" or not ShardCapAPI then return end
|
||||
|
||||
local API = ShardCapAPI
|
||||
local DEFAULTS = API.DEFAULTS
|
||||
|
||||
local function DB() return API.GetDB() end
|
||||
local function Changed() API.Schedule(0.3) end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Snapshot for Cancel (shared by both pages; one restore is enough)
|
||||
------------------------------------------------------------------------------
|
||||
local KEYS = { "cap", "notify", "sort", "combat", "reagents" }
|
||||
local saved
|
||||
|
||||
local function CopyBags(t)
|
||||
if not t then return nil end
|
||||
local c = {}
|
||||
for k, v in pairs(t) do c[k] = v end
|
||||
return c
|
||||
end
|
||||
|
||||
local function Snapshot()
|
||||
local db = DB()
|
||||
if saved or not db then return end
|
||||
saved = { soulbags = CopyBags(db.soulbags) }
|
||||
for _, k in ipairs(KEYS) do saved[k] = db[k] end
|
||||
end
|
||||
|
||||
local function Restore()
|
||||
local db = DB()
|
||||
if not saved or not db then saved = nil return end
|
||||
for _, k in ipairs(KEYS) do db[k] = saved[k] end
|
||||
db.soulbags = CopyBags(saved.soulbags)
|
||||
saved = nil
|
||||
Changed()
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Widgets (same templates ClassicAPI's own panel uses)
|
||||
------------------------------------------------------------------------------
|
||||
local numControls = 0
|
||||
local function NextName()
|
||||
numControls = numControls + 1
|
||||
return "ShardCapOptionsControl" .. numControls
|
||||
end
|
||||
|
||||
local function RefreshControls(page)
|
||||
if not DB() then return end
|
||||
page.updating = true
|
||||
for _, control in ipairs(page.controls) do control:Update() end
|
||||
if page.OnRefresh then page.OnRefresh() end
|
||||
page.updating = nil
|
||||
end
|
||||
|
||||
local function NewPage(name, parent, title, subtitle)
|
||||
local page = CreateFrame("Frame")
|
||||
page.name = name
|
||||
page.parent = parent
|
||||
page.controls = {}
|
||||
|
||||
local heading = page:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
|
||||
heading:SetPoint("TOPLEFT", page, "TOPLEFT", 16, -16)
|
||||
heading:SetText(title)
|
||||
page.y = -40
|
||||
if subtitle then
|
||||
local text = page:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
text:SetPoint("TOPLEFT", heading, "BOTTOMLEFT", 0, -8)
|
||||
text:SetPoint("RIGHT", page, "RIGHT", -32, 0)
|
||||
text:SetJustifyH("LEFT")
|
||||
text:SetText(subtitle)
|
||||
page.y = -64
|
||||
end
|
||||
|
||||
page.refresh = function() Snapshot(); RefreshControls(page) end
|
||||
page.okay = function() saved = nil end
|
||||
page.cancel = function() Restore() end
|
||||
return page
|
||||
end
|
||||
|
||||
local function AddHeader(page, text)
|
||||
local header = page:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
||||
header:SetPoint("TOPLEFT", page, "TOPLEFT", 16, page.y - 8)
|
||||
header:SetText(text)
|
||||
page.y = page.y - 28
|
||||
end
|
||||
|
||||
local function AddText(page, height)
|
||||
local fs = page:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
fs:SetPoint("TOPLEFT", page, "TOPLEFT", 20, page.y)
|
||||
fs:SetPoint("RIGHT", page, "RIGHT", -16, 0)
|
||||
fs:SetJustifyH("LEFT")
|
||||
page.y = page.y - (height or 16)
|
||||
return fs
|
||||
end
|
||||
|
||||
-- get() / set(value); enabledIf() greys the box out while it returns false.
|
||||
local function AddCheckbox(page, label, tooltip, get, set, enabledIf, x)
|
||||
local check = CreateFrame("CheckButton", NextName(), page, "OptionsCheckButtonTemplate")
|
||||
check:SetPoint("TOPLEFT", page, "TOPLEFT", x or 16, page.y)
|
||||
local text = getglobal(check:GetName() .. "Text")
|
||||
text:SetText(label)
|
||||
check.tooltipText = tooltip
|
||||
check:SetScript("OnClick", function()
|
||||
set(this:GetChecked() and true or false)
|
||||
RefreshControls(page)
|
||||
end)
|
||||
check.Update = function(self)
|
||||
self:SetChecked(get())
|
||||
if not enabledIf or enabledIf() then
|
||||
self:Enable()
|
||||
text:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
|
||||
else
|
||||
self:Disable()
|
||||
text:SetTextColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b)
|
||||
end
|
||||
end
|
||||
table.insert(page.controls, check)
|
||||
if not x then page.y = page.y - 30 end
|
||||
return check, text
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Page 1: ShardCap
|
||||
------------------------------------------------------------------------------
|
||||
local main = NewPage("ShardCap", nil, "ShardCap " .. API.VERSION,
|
||||
"Keeps your Soul Shards at a cap when you leave combat.")
|
||||
|
||||
AddHeader(main, "Carrying")
|
||||
local statusShards = AddText(main)
|
||||
local statusReagents = AddText(main)
|
||||
local statusBags = AddText(main, 22)
|
||||
|
||||
-- Cap: slider for quick changes (0-100), box for an exact value (0-200)
|
||||
AddHeader(main, "Shard cap")
|
||||
local MAX_SLIDER, MAX_CAP = 100, 200
|
||||
local slider = CreateFrame("Slider", NextName(), main, "OptionsSliderTemplate")
|
||||
slider:SetPoint("TOPLEFT", main, "TOPLEFT", 24, main.y - 14)
|
||||
slider:SetWidth(220)
|
||||
slider:SetMinMaxValues(0, MAX_SLIDER)
|
||||
slider:SetValueStep(1)
|
||||
slider.tooltipText = "Shards to keep. Anything above this is deleted when you leave combat."
|
||||
getglobal(slider:GetName() .. "Text"):SetText("Shards to keep")
|
||||
getglobal(slider:GetName() .. "Low"):SetText("0")
|
||||
getglobal(slider:GetName() .. "High"):SetText(MAX_SLIDER)
|
||||
|
||||
local box = CreateFrame("EditBox", NextName(), main, "InputBoxTemplate")
|
||||
box:SetWidth(40)
|
||||
box:SetHeight(20)
|
||||
box:SetPoint("LEFT", slider, "RIGHT", 18, 0)
|
||||
box:SetAutoFocus(false)
|
||||
box:SetNumeric(true)
|
||||
box:SetMaxLetters(3)
|
||||
local boxFocus, boxCancel = false, false
|
||||
|
||||
local function SetCap(v)
|
||||
local db = DB()
|
||||
if not db or v == db.cap then return end
|
||||
db.cap = v
|
||||
Changed()
|
||||
end
|
||||
|
||||
slider:SetScript("OnValueChanged", function()
|
||||
local v = math.floor(this:GetValue() + 0.5)
|
||||
if main.updating then return end
|
||||
box:SetText(v)
|
||||
SetCap(v)
|
||||
if main.OnRefresh then main.OnRefresh() end
|
||||
end)
|
||||
|
||||
local function ApplyBox()
|
||||
local v = tonumber(box:GetText())
|
||||
if v and v >= 0 and v <= MAX_CAP then
|
||||
SetCap(math.floor(v))
|
||||
else
|
||||
API.Print("The cap must be a whole number from 0 to " .. MAX_CAP .. ".")
|
||||
end
|
||||
RefreshControls(main)
|
||||
end
|
||||
box:SetScript("OnEditFocusGained", function() boxFocus = true end)
|
||||
box:SetScript("OnEditFocusLost", function()
|
||||
boxFocus = false
|
||||
if boxCancel then boxCancel = false; RefreshControls(main) else ApplyBox() end
|
||||
end)
|
||||
box:SetScript("OnEnterPressed", function() this:ClearFocus() end)
|
||||
box:SetScript("OnEscapePressed", function() boxCancel = true; this:ClearFocus() end)
|
||||
|
||||
slider.Update = function(self)
|
||||
local cap = DB().cap
|
||||
if cap > MAX_SLIDER then self:SetValue(MAX_SLIDER) else self:SetValue(cap) end
|
||||
if not boxFocus then box:SetText(cap) end
|
||||
end
|
||||
table.insert(main.controls, slider)
|
||||
main.y = main.y - 50
|
||||
|
||||
local hint = AddText(main, 20)
|
||||
hint:SetText("|cffaaaaaaType a value up to " .. MAX_CAP .. " in the box for caps above " .. MAX_SLIDER .. ".|r")
|
||||
|
||||
AddHeader(main, "Options")
|
||||
AddCheckbox(main, "Chat notifications",
|
||||
"Print a line in chat whenever ShardCap deletes or moves something.",
|
||||
function() return DB().notify end, function(v) DB().notify = v end)
|
||||
AddCheckbox(main, "Trim during combat",
|
||||
"Also delete shards above the cap while you are in combat. Off: wait until combat ends.",
|
||||
function() return DB().combat end, function(v) DB().combat = v; Changed() end)
|
||||
|
||||
local trim = CreateFrame("Button", NextName(), main, "UIPanelButtonTemplate")
|
||||
trim:SetWidth(120)
|
||||
trim:SetHeight(22)
|
||||
trim:SetPoint("TOPLEFT", main, "TOPLEFT", 20, main.y - 8)
|
||||
trim:SetText("Trim now")
|
||||
trim:SetScript("OnClick", function() API.Schedule(0, true) end)
|
||||
trim:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText("Delete shards above the cap right now, even in combat.", nil, nil, nil, nil, 1)
|
||||
end)
|
||||
trim:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
|
||||
main.OnRefresh = function()
|
||||
local shards, reagents, bags = API.StatusLines()
|
||||
statusShards:SetText(shards)
|
||||
statusReagents:SetText(reagents)
|
||||
statusBags:SetText(bags)
|
||||
end
|
||||
|
||||
main.default = function()
|
||||
local db = DB()
|
||||
db.cap, db.notify, db.combat = DEFAULTS.cap, DEFAULTS.notify, DEFAULTS.combat
|
||||
Changed()
|
||||
RefreshControls(main)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Page 2: Soul Bag
|
||||
------------------------------------------------------------------------------
|
||||
local bagPage = NewPage("Soul Bag", main.name, "Soul Bag",
|
||||
"What goes in your soul bag, and which bags count as one.")
|
||||
|
||||
AddHeader(bagPage, "Sorting")
|
||||
AddCheckbox(bagPage, "Move shards into the soul bag",
|
||||
"Move shards from your normal bags into free soul bag slots, and merge partial stacks.",
|
||||
function() return DB().sort end, function(v) DB().sort = v; Changed() end)
|
||||
AddCheckbox(bagPage, "Also move Demonic Figurines and Infernal Stones",
|
||||
"Only uses soul bag slots that are not needed to hold your shard cap. "
|
||||
.. "Skipped on servers where shards don't stack.",
|
||||
function() return DB().reagents end, function(v) DB().reagents = v; Changed() end)
|
||||
|
||||
AddHeader(bagPage, "Soul bags")
|
||||
local function IsAuto() return DB().soulbags == nil end
|
||||
AddCheckbox(bagPage, "Detect soul bags automatically",
|
||||
"Recognizes soul bags by item type. Turn this off to choose the bags yourself.",
|
||||
IsAuto,
|
||||
function(v)
|
||||
local db = DB()
|
||||
if v then
|
||||
db.soulbags = nil
|
||||
else
|
||||
db.soulbags = {}
|
||||
for b = 1, 4 do if API.AutoIsSoulBag(b) then db.soulbags[b] = true end end
|
||||
end
|
||||
Changed()
|
||||
end)
|
||||
|
||||
local function BagName(bag)
|
||||
local link = GetInventoryItemLink("player", ContainerIDToInventoryID(bag))
|
||||
if not link then return nil end
|
||||
local _, _, name = string.find(link, "%[(.-)%]")
|
||||
return name
|
||||
end
|
||||
|
||||
for bag = 1, 4 do
|
||||
local b = bag
|
||||
local check, label = AddCheckbox(bagPage, "Bag " .. b,
|
||||
"Treat this bag as a soul bag. Only available when automatic detection is off.",
|
||||
function() return API.IsSoulBag(b) end,
|
||||
function(v)
|
||||
local db = DB()
|
||||
if not db.soulbags then db.soulbags = {} end
|
||||
if v then db.soulbags[b] = true else db.soulbags[b] = nil end
|
||||
Changed()
|
||||
end,
|
||||
function() return not IsAuto() and BagName(b) ~= nil end,
|
||||
36)
|
||||
local update = check.Update
|
||||
check.Update = function(self)
|
||||
label:SetText("Bag " .. b .. ": " .. (BagName(b) or "|cff808080empty slot|r"))
|
||||
update(self)
|
||||
end
|
||||
bagPage.y = bagPage.y - 26
|
||||
end
|
||||
|
||||
bagPage.default = function()
|
||||
local db = DB()
|
||||
db.sort, db.reagents, db.soulbags = DEFAULTS.sort, DEFAULTS.reagents, nil
|
||||
Changed()
|
||||
RefreshControls(bagPage)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Live refresh while a page is shown (bags change as you loot)
|
||||
------------------------------------------------------------------------------
|
||||
local function LiveRefresh(page)
|
||||
local elapsed = 0
|
||||
page:SetScript("OnUpdate", function()
|
||||
elapsed = elapsed + arg1
|
||||
if elapsed < 0.5 then return end
|
||||
elapsed = 0
|
||||
if not boxFocus then RefreshControls(this) end
|
||||
end)
|
||||
end
|
||||
LiveRefresh(main)
|
||||
LiveRefresh(bagPage)
|
||||
|
||||
InterfaceOptions_AddCategory(main)
|
||||
InterfaceOptions_AddCategory(bagPage)
|
||||
|
||||
API.OpenOptions = function()
|
||||
InterfaceOptionsFrame_OpenToCategory(main)
|
||||
end
|
||||
+595
@@ -0,0 +1,595 @@
|
||||
--[[ 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.0"
|
||||
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
|
||||
|
||||
local function SlotInfo(bag, slot)
|
||||
if api.classic then
|
||||
local info = C_Container.GetContainerItemInfo(bag, slot)
|
||||
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 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
|
||||
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
|
||||
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
|
||||
MoveShards(src, dst, n)
|
||||
Notify("Merged " .. 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 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)
|
||||
else
|
||||
MoveShards(s, dst, s.count)
|
||||
end
|
||||
Notify("Moved " .. 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
|
||||
C_Container.SwapItems(r.bag, r.slot, dst.bag, dst.slot)
|
||||
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("Moved " .. (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()
|
||||
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 ReagentStep(stacks, freeSoul, reagents) then return true 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
|
||||
local function StatusLines()
|
||||
local _, freeSoul, total, _, reagentTotals = Scan()
|
||||
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),
|
||||
"Soul bags: " .. SoulBagList() .. (db.soulbags and " (manual)" or " (auto)")
|
||||
.. ", " .. table.getn(freeSoul) .. " 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
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
## Interface: 11200
|
||||
## Title: ShardCap |cff9482c92.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
|
||||
## SavedVariablesPerCharacter: ShardCapDB, SHARDCAP_CAP_VALUE, SHARDCAP_SPAM
|
||||
ShardCap.lua
|
||||
Options.lua
|
||||
+471
@@ -0,0 +1,471 @@
|
||||
-- Mock WoW 1.12 client + scenario tests for ShardCap. Run with Lua 5.0.
|
||||
local ADDON_FILE = arg[1] or "ShardCap.lua"
|
||||
local fails, passes = 0, 0
|
||||
local function check(cond, name)
|
||||
if cond then passes = passes + 1; print(" ok " .. name)
|
||||
else fails = fails + 1; print(" FAIL " .. name) end
|
||||
end
|
||||
|
||||
local now, bags, bagItem, cursor, combat, frames, chat, maxStack
|
||||
local soulOK = {}
|
||||
local function isSoulBagItem(id) return id == 22243 or id == 99999 end
|
||||
local function fits(b, id) return not isSoulBagItem(bagItem[b]) or soulOK[id] end
|
||||
local itemNames = { [6265] = "Soul Shard", [16265] = "Some Other Item", [6948] = "Hearthstone" }
|
||||
|
||||
local function reset(opts)
|
||||
opts = opts or {}
|
||||
now, cursor, combat, frames, chat = 0, nil, false, {}, {}
|
||||
maxStack = opts.maxStack or 3
|
||||
soulOK = { [6265] = true }
|
||||
if opts.reagentsOK ~= false then soulOK[16583] = true; soulOK[5565] = true end
|
||||
familyKnown = opts.familyKnown
|
||||
bags, bagItem = {}, {}
|
||||
for b = 0, 4 do bags[b] = { size = 0, slots = {} } end
|
||||
ShardCapDB, SHARDCAP_CAP_VALUE, SHARDCAP_SPAM = nil, nil, nil
|
||||
C_Container, C_Item, GetBagItem, CLASSIC_API_VERSION = nil, nil, nil, nil
|
||||
bank = nil
|
||||
end
|
||||
local function setBag(b, size, itemID) bags[b].size = size; bagItem[b] = itemID end
|
||||
local function put(b, s, id, n) bags[b].slots[s] = { id = id, count = n or 1 } end
|
||||
|
||||
-- vanilla API
|
||||
function GetTime() return now end
|
||||
function GetContainerNumSlots(b) return bags[b] and bags[b].size or 0 end
|
||||
function GetContainerItemLink(b, s)
|
||||
local it = bags[b].slots[s]; if not it then return nil end
|
||||
return "|cffffffff|Hitem:" .. it.id .. ":0:0:0|h[" .. (itemNames[it.id] or "?") .. "]|h|r"
|
||||
end
|
||||
function GetContainerItemInfo(b, s)
|
||||
local it = bags[b].slots[s]; if not it then return nil end
|
||||
return "tex", it.count, it.locked
|
||||
end
|
||||
function ContainerIDToInventoryID(b) return 19 + b end
|
||||
function GetInventoryItemLink(u, inv)
|
||||
local id = bagItem[inv - 19]; if not id then return nil end
|
||||
return "|Hitem:" .. id .. ":0:0:0|h[bag]|h"
|
||||
end
|
||||
function GetItemInfo(id)
|
||||
id = tonumber(id)
|
||||
if id == 6265 then return "Soul Shard", "item:6265", 1, 0, "Reagent", "Reagent", maxStack end
|
||||
if id == 16583 then return "Demonic Figurine", "item:16583", 1, 0, "Reagent", "Reagent", 10 end
|
||||
if id == 5565 then return "Infernal Stone", "item:5565", 1, 0, "Reagent", "Reagent", 10 end
|
||||
if id == 22243 then return "Small Soul Pouch", "item:22243", 2, 0, "Container", "Soul Bag", 1 end
|
||||
if id == 99999 then return "Custom Soul Bag", "item:99999", 2, 0, "Container", "Soul Bag", 1 end
|
||||
return "Bag", "item:" .. id, 1, 0, "Container", "Bag", 1
|
||||
end
|
||||
function CursorHasItem() return cursor ~= nil end
|
||||
function SpellIsTargeting() return false end
|
||||
function PickupContainerItem(b, s)
|
||||
local it = bags[b].slots[s]
|
||||
if cursor then
|
||||
if not fits(b, cursor.id) then return end -- "That item doesn't go in that container."
|
||||
if it and it.id == cursor.id and it.count + cursor.count <= maxStack then
|
||||
it.count = it.count + cursor.count; cursor = nil
|
||||
elseif not it then
|
||||
bags[b].slots[s] = cursor; cursor = nil
|
||||
else
|
||||
bags[b].slots[s] = cursor; cursor = it
|
||||
end
|
||||
elseif it then
|
||||
bags[b].slots[s] = nil; cursor = it
|
||||
end
|
||||
end
|
||||
function SplitContainerItem(b, s, n)
|
||||
local it = bags[b].slots[s]
|
||||
if cursor or not it or n >= it.count then return end
|
||||
it.count = it.count - n; cursor = { id = it.id, count = n }
|
||||
end
|
||||
function DeleteCursorItem() cursor = nil end
|
||||
function UnitAffectingCombat() return combat end
|
||||
function IsAddOnLoaded() return false end
|
||||
DEFAULT_CHAT_FRAME = { AddMessage = function(self, m) table.insert(chat, m) end }
|
||||
SlashCmdList = {}
|
||||
-- Minimal widget mock: enough of Frame/CheckButton/Slider/EditBox/FontString for Options.lua
|
||||
local W = {}
|
||||
local function call(w, script)
|
||||
local fn = w.scripts[script]
|
||||
if fn then local old = this; this = w; fn(); this = old end
|
||||
end
|
||||
function W:RegisterEvent(e) self.events[e] = true end
|
||||
function W:SetScript(k, fn) self.scripts[k] = fn end
|
||||
function W:Show() self.shown = true end
|
||||
function W:Hide() self.shown = false end
|
||||
function W:GetName() return self.name end
|
||||
function W:SetText(t) self.text = t and tostring(t) or "" end
|
||||
function W:GetText() return self.text end
|
||||
function W:SetChecked(v) if v then self.checked = 1 else self.checked = nil end end
|
||||
function W:GetChecked() return self.checked end
|
||||
function W:Enable() self.enabled = true end
|
||||
function W:Disable() self.enabled = false end
|
||||
function W:SetMinMaxValues(lo, hi) self.lo, self.hi = lo, hi end
|
||||
function W:GetValue() return self.value end
|
||||
function W:SetValue(v)
|
||||
if self.lo and v < self.lo then v = self.lo end
|
||||
if self.hi and v > self.hi then v = self.hi end
|
||||
if v ~= self.value then self.value = v; call(self, "OnValueChanged") end
|
||||
end
|
||||
function W:SetFocus() self.focus = true; call(self, "OnEditFocusGained") end
|
||||
function W:ClearFocus() if self.focus then self.focus = false; call(self, "OnEditFocusLost") end end
|
||||
function W:CreateFontString() return newWidget("FontString") end
|
||||
for _, m in ipairs({ "SetPoint", "SetWidth", "SetHeight", "SetJustifyH", "SetTextColor", "SetValueStep",
|
||||
"SetAutoFocus", "SetNumeric", "SetMaxLetters", "SetOwner" }) do W[m] = function() end end
|
||||
local WMT = { __index = W }
|
||||
function newWidget(kind, name)
|
||||
local w = setmetatable({ kind = kind, name = name, scripts = {}, events = {}, shown = true, text = "", value = 0, enabled = true }, WMT)
|
||||
if name then _G[name] = w end
|
||||
table.insert(allWidgets, w)
|
||||
return w
|
||||
end
|
||||
allWidgets = {}
|
||||
function getglobal(n) return _G[n] end
|
||||
function CreateFrame(kind, name, parent, template)
|
||||
local f = newWidget(kind, name)
|
||||
if name and template then
|
||||
for _, sub in ipairs({ "Text", "Low", "High" }) do newWidget("FontString", name .. sub) end
|
||||
end
|
||||
table.insert(frames, f)
|
||||
return f
|
||||
end
|
||||
GameTooltip = newWidget("GameTooltip")
|
||||
NORMAL_FONT_COLOR = { r = 1, g = 0.82, b = 0 }
|
||||
GRAY_FONT_COLOR = { r = 0.5, g = 0.5, b = 0.5 }
|
||||
|
||||
local F
|
||||
local function fire(e, a1)
|
||||
if not F.events[e] then return end
|
||||
event, arg1 = e, a1
|
||||
this = F; F.scripts.OnEvent()
|
||||
end
|
||||
local function run(seconds)
|
||||
local stop = now + (seconds or 10)
|
||||
while now < stop do
|
||||
now = now + 0.05
|
||||
if F.shown and F.scripts.OnUpdate then this = F; F.scripts.OnUpdate() end
|
||||
end
|
||||
end
|
||||
local OPTIONS_FILE = string.gsub(ADDON_FILE, "ShardCap%.lua$", "Options.lua")
|
||||
local categories, bank
|
||||
local function load(classic, withOptions)
|
||||
categories = {}
|
||||
if withOptions then
|
||||
InterfaceOptions_AddCategory = function(p) p:Hide(); table.insert(categories, p) end
|
||||
InterfaceOptionsFrame_OpenToCategory = function(p) p:Show(); p.refresh(p); categories.opened = p end
|
||||
else
|
||||
InterfaceOptions_AddCategory, InterfaceOptionsFrame_OpenToCategory = nil, nil
|
||||
end
|
||||
if classic then
|
||||
CLASSIC_API_VERSION = 10000
|
||||
C_Container = {
|
||||
GetContainerItemInfo = function(b, s)
|
||||
local it = bags[b].slots[s]; if not it then return nil end
|
||||
return { itemID = it.id, stackCount = it.count, isLocked = it.locked and true or false }
|
||||
end,
|
||||
GetContainerNumFreeSlots = function(b)
|
||||
local id = bagItem[b]
|
||||
local t = (id == 22243 or id == 99999) and 4 or 0
|
||||
return 0, t
|
||||
end,
|
||||
SwapItems = function(sb, ss, db_, ds)
|
||||
local a, b2 = bags[sb].slots[ss], bags[db_].slots[ds]
|
||||
if (a and not fits(db_, a.id)) or (b2 and not fits(sb, b2.id)) then return true end -- server refuses silently
|
||||
bags[sb].slots[ss], bags[db_].slots[ds] = b2, a; return true
|
||||
end,
|
||||
MoveItem = function(sb, ss, db_, ds, n)
|
||||
local src, dst = bags[sb].slots[ss], bags[db_].slots[ds]
|
||||
if not src or n > src.count then return false end
|
||||
if dst and (dst.id ~= src.id or dst.count + n > maxStack) then return false end
|
||||
if dst then dst.count = dst.count + n else bags[db_].slots[ds] = { id = src.id, count = n } end
|
||||
src.count = src.count - n; if src.count == 0 then bags[sb].slots[ss] = nil end
|
||||
return true
|
||||
end,
|
||||
}
|
||||
C_Item = { GetItemMaxStackSizeByID = function(id) return maxStack end,
|
||||
GetItemFamily = function(id)
|
||||
if not familyKnown then return 0 end -- family not reported: fall back to heuristics
|
||||
return soulOK[id] and 4 or 0
|
||||
end }
|
||||
end
|
||||
if classic then
|
||||
C_Item.GetItemCount = function(id, includeBank)
|
||||
local t = 0
|
||||
for b = 0, 4 do for s = 1, bags[b].size do
|
||||
local it = bags[b].slots[s]; if it and it.id == id then t = t + it.count end
|
||||
end end
|
||||
if includeBank and bank then t = t + (bank[id] or 0) end
|
||||
return t
|
||||
end
|
||||
end
|
||||
dofile(ADDON_FILE)
|
||||
dofile(OPTIONS_FILE)
|
||||
F = frames[1]
|
||||
fire("ADDON_LOADED", "ShardCap")
|
||||
fire("PLAYER_ENTERING_WORLD")
|
||||
end
|
||||
local function count(onlyBag)
|
||||
local t = 0
|
||||
for b = 0, 4 do
|
||||
if not onlyBag or onlyBag == b then
|
||||
for s = 1, bags[b].size do
|
||||
local it = bags[b].slots[s]
|
||||
if it and it.id == 6265 then t = t + it.count end
|
||||
end
|
||||
end
|
||||
end
|
||||
return t
|
||||
end
|
||||
local function where(id)
|
||||
local t = {}
|
||||
for b = 0, 4 do for s = 1, bags[b].size do
|
||||
local it = bags[b].slots[s]; if it and it.id == id then table.insert(t, b) end
|
||||
end end
|
||||
return t
|
||||
end
|
||||
local function slotsUsed()
|
||||
local t = 0
|
||||
for b = 0, 4 do for s = 1, bags[b].size do
|
||||
local it = bags[b].slots[s]; if it and it.id == 6265 then t = t + 1 end
|
||||
end end
|
||||
return t
|
||||
end
|
||||
|
||||
for _, classic in ipairs({ false, true }) do
|
||||
local tag = classic and "[ClassicAPI] " or "[vanilla] "
|
||||
print(tag)
|
||||
|
||||
-- 1. WoW 1.18.1 Clients stacks of 3: trims to exactly the cap, backpack first
|
||||
reset(); setBag(0, 16); setBag(4, 12, 22243)
|
||||
put(0, 2, 6265, 3); put(4, 1, 6265, 3); put(4, 2, 6265, 3); put(4, 3, 6265, 2)
|
||||
load(classic); ShardCapDB.cap = 6; ShardCapDB.sort = false; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run()
|
||||
check(count() == 6, tag .. "trim 11 -> 6 with stacks of 3 (got " .. count() .. ")")
|
||||
check(count(0) == 0, tag .. "deleted from backpack before soul bag")
|
||||
check(cursor == nil, tag .. "cursor left empty")
|
||||
|
||||
-- 2. Exactness: 13 shards, cap 12 -> deletes exactly 1 (ShardCapPlus deleted a whole stack of 3)
|
||||
reset(); setBag(0, 16); setBag(4, 12, 22243)
|
||||
put(0, 1, 6265, 1); for s = 1, 4 do put(4, s, 6265, 3) end
|
||||
load(classic); ShardCapDB.cap = 12; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run()
|
||||
check(count() == 12, tag .. "13 -> exactly 12 (got " .. count() .. ")")
|
||||
|
||||
reset(); setBag(0, 16); setBag(4, 12, 22243)
|
||||
put(0, 1, 6265, 3); for s = 1, 4 do put(4, s, 6265, 3) end
|
||||
load(classic); ShardCapDB.cap = 13; ShardCapDB.sort = false; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run()
|
||||
check(count() == 13 and count(0) == 1, tag .. "15 -> 13 splits the backpack stack (got " .. count() .. ")")
|
||||
|
||||
-- 3. No false positives: item 16265 is never touched
|
||||
reset(); setBag(0, 16)
|
||||
put(0, 1, 16265, 1); put(0, 2, 6265, 1); put(0, 3, 6265, 1)
|
||||
load(classic); ShardCapDB.cap = 1; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run()
|
||||
check(bags[0].slots[1] and bags[0].slots[1].id == 16265, tag .. "item 16265 kept")
|
||||
check(count() == 1, tag .. "shards trimmed to 1")
|
||||
|
||||
-- 4. Merge partial stacks into the soul bag stack
|
||||
reset(); setBag(0, 16); setBag(4, 12, 22243)
|
||||
put(0, 5, 6265, 1); put(4, 1, 6265, 2); put(4, 2, 6265, 1)
|
||||
load(classic); ShardCapDB.cap = 20; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run()
|
||||
check(count() == 4, tag .. "merge keeps all 4 shards")
|
||||
check(slotsUsed() == 2, tag .. "4 shards in 2 slots after merging (got " .. slotsUsed() .. ")")
|
||||
check(count(0) == 0, tag .. "backpack emptied into soul bag")
|
||||
|
||||
-- 5. Blizzlike (no stacking): move shards into free soul bag slots
|
||||
reset({ maxStack = 1 }); setBag(0, 16); setBag(2, 16, 22243)
|
||||
put(0, 1, 6265); put(0, 2, 6265); put(2, 1, 6265)
|
||||
load(classic); ShardCapDB.cap = 20; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run()
|
||||
check(count(2) == 3 and count(0) == 0, tag .. "shards moved into soul bag in bag 2")
|
||||
|
||||
-- 6. Combat: nothing deleted in combat, trimmed on leaving it
|
||||
reset(); setBag(0, 16)
|
||||
for s = 1, 5 do put(0, s, 6265, 1) end
|
||||
load(classic); run(); ShardCapDB.cap = 2
|
||||
combat = true; fire("PLAYER_REGEN_DISABLED"); put(0, 6, 6265, 1); fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run()
|
||||
check(count() == 6, tag .. "no deletion during combat")
|
||||
combat = false; fire("PLAYER_REGEN_ENABLED"); run()
|
||||
check(count() == 2, tag .. "trimmed after combat (got " .. count() .. ")")
|
||||
|
||||
-- 7. Manual /shardcap delete works in combat
|
||||
reset(); setBag(0, 16); for s = 1, 4 do put(0, s, 6265, 1) end
|
||||
load(classic); combat = true; fire("PLAYER_REGEN_DISABLED"); ShardCapDB.cap = 1
|
||||
SlashCmdList.SHARDCAP("delete"); run()
|
||||
check(count() == 1, tag .. "manual delete in combat")
|
||||
|
||||
-- 8. Holding an item on the cursor blocks deletion
|
||||
reset(); setBag(0, 16); for s = 1, 4 do put(0, s, 6265, 1) end
|
||||
cursor = { id = 6948, count = 1 }
|
||||
load(classic); ShardCapDB.cap = 1; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run(2)
|
||||
check(count() == 4 and cursor.id == 6948, tag .. "waits while the cursor holds an item")
|
||||
cursor = nil; run()
|
||||
check(count() == 1, tag .. "proceeds once the cursor is free")
|
||||
|
||||
-- 9. Locked stack is skipped until unlocked
|
||||
reset(); setBag(0, 16); put(0, 1, 6265, 1); put(0, 2, 6265, 1)
|
||||
bags[0].slots[2].locked = true; bags[0].slots[1].locked = true
|
||||
load(classic); ShardCapDB.cap = 0; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run(1)
|
||||
check(count() == 2, tag .. "locked stacks untouched")
|
||||
bags[0].slots[2].locked = nil; bags[0].slots[1].locked = nil; run()
|
||||
check(count() == 0, tag .. "deleted after unlock")
|
||||
|
||||
-- 10. Reagents go into the soul bag when there's room beyond the shard reserve
|
||||
reset({ familyKnown = true }); setBag(0, 16); setBag(4, 12, 22243)
|
||||
put(0, 1, 16583, 2); put(0, 2, 5565, 7); put(4, 1, 6265, 3)
|
||||
load(classic); ShardCapDB.cap = 12; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run()
|
||||
check(where(16583)[1] == 4 and where(5565)[1] == 4, tag .. "figurine and infernal stone moved to soul bag")
|
||||
check(bags[4].slots[12] ~= nil and bags[4].slots[11] ~= nil, tag .. "reagents fill soul bag from the end")
|
||||
check(count() == 3, tag .. "shards untouched by reagent moves")
|
||||
|
||||
-- 11. Reserve: cap 12 / 3 per slot = 4 slots kept for shards
|
||||
reset({ familyKnown = true }); setBag(0, 16); setBag(4, 5, 22243)
|
||||
put(0, 1, 16583, 1); put(0, 2, 5565, 1); put(4, 1, 6265, 1)
|
||||
load(classic); ShardCapDB.cap = 12; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run()
|
||||
local moved = 0
|
||||
if where(16583)[1] == 4 then moved = moved + 1 end
|
||||
if where(5565)[1] == 4 then moved = moved + 1 end
|
||||
check(moved == 1, tag .. "only 1 reagent moved into a 5-slot soul bag (4 kept for shards), got " .. moved)
|
||||
|
||||
-- 12. Shards outside the soul bag get the free slots first
|
||||
reset({ familyKnown = true }); setBag(0, 16); setBag(4, 2, 22243)
|
||||
put(0, 1, 16583, 1); put(0, 2, 6265, 1); put(0, 3, 6265, 1)
|
||||
load(classic); ShardCapDB.cap = 6; ShardCapDB.sort = true; fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run()
|
||||
check(where(16583)[1] == 0, tag .. "reagent waits when shards need the space")
|
||||
|
||||
-- 13. Server that doesn't allow reagents in soul bags: gives up, no endless retries
|
||||
reset({ reagentsOK = false }); setBag(0, 16); setBag(4, 12, 22243)
|
||||
put(0, 1, 16583, 1); put(0, 2, 5565, 1)
|
||||
load(classic); ShardCapDB.cap = 3; local before = table.getn(chat)
|
||||
fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run(20)
|
||||
check(where(16583)[1] == 0 and where(5565)[1] == 0 and cursor == nil, tag .. "refused reagents stay put, cursor empty")
|
||||
check(table.getn(chat) - before == 2, tag .. "one notice per refused item (got " .. (table.getn(chat) - before) .. ")")
|
||||
check(not F.shown, tag .. "scheduler goes idle after giving up")
|
||||
|
||||
-- 14. Blizzlike server (shards don't stack): reagents left alone
|
||||
reset({ maxStack = 1, reagentsOK = false }); setBag(0, 16); setBag(4, 12, 22243)
|
||||
put(0, 1, 16583, 1)
|
||||
load(classic); fire("BAG_UPDATE"); fire("BAG_UPDATE_DELAYED"); run()
|
||||
check(where(16583)[1] == 0 and table.getn(chat) == 0, tag .. "no attempt on Blizzlike servers")
|
||||
|
||||
-- 15. Toggle off
|
||||
reset({ familyKnown = true }); setBag(0, 16); setBag(4, 12, 22243); put(0, 1, 5565, 1)
|
||||
load(classic); SlashCmdList.SHARDCAP("reagents"); run()
|
||||
check(where(5565)[1] == 0 and ShardCapDB.reagents == false, tag .. "/shardcap reagents turns it off")
|
||||
end
|
||||
|
||||
-- 10. Migration from ShardCap 1.x and slash parsing
|
||||
print("[misc]")
|
||||
reset(); setBag(0, 16)
|
||||
dofile(ADDON_FILE); F = frames[1]
|
||||
SHARDCAP_CAP_VALUE, SHARDCAP_SPAM = 7, true
|
||||
fire("ADDON_LOADED", "ShardCap")
|
||||
check(ShardCapDB.cap == 7 and ShardCapDB.notify == true, "migrated 1.x cap and spam setting")
|
||||
check(SHARDCAP_CAP_VALUE == nil, "old globals cleared")
|
||||
SlashCmdList.SHARDCAP("-3"); check(ShardCapDB.cap == 7, "negative cap rejected")
|
||||
SlashCmdList.SHARDCAP("2.5"); check(ShardCapDB.cap == 7, "fractional cap rejected")
|
||||
SlashCmdList.SHARDCAP(" 15 "); check(ShardCapDB.cap == 15, "cap set with whitespace")
|
||||
SlashCmdList.SHARDCAP("soulbag 3"); check(ShardCapDB.soulbags and ShardCapDB.soulbags[3], "manual soul bag")
|
||||
SlashCmdList.SHARDCAP("soulbag auto"); check(ShardCapDB.soulbags == nil, "back to auto")
|
||||
SlashCmdList.SHARDCAP("status"); SlashCmdList.SHARDCAP("help")
|
||||
check(table.getn(chat) > 5, "status/help print")
|
||||
reset(); setBag(0, 16); put(0, 1, 6265, 3); put(0, 2, 6265, 2)
|
||||
load(false); ShardCapDB.cap = 12; chat = {}; SlashCmdList.SHARDCAP("")
|
||||
check(string.find(chat[2] or "", "Demonic Figurines: |cffffffff0|r Infernal Stones: |cffffffff0|r", 1, true) ~= nil, "reagent line with none carried")
|
||||
reset(); setBag(0, 16); setBag(4, 12, 22243); put(0, 1, 6265, 3); put(0, 2, 6265, 2)
|
||||
put(0, 3, 16583, 2); put(4, 5, 16583, 1); put(0, 4, 5565, 7)
|
||||
load(false); ShardCapDB.cap = 12; ShardCapDB.reagents = false; chat = {}; SlashCmdList.SHARDCAP("")
|
||||
check(string.find(chat[2] or "", "Demonic Figurines: |cffffffff3|r Infernal Stones: |cffffffff7|r", 1, true) ~= nil, "reagents counted across all bags incl. soul bag (got: " .. (chat[2] or "nil") .. ")")
|
||||
check(string.find(chat[1] or "", "Soul Shards: |cffffffff5/12|r", 1, true) ~= nil, "/shardcap shows 5/12 (got: " .. (chat[1] or "nil") .. ")")
|
||||
|
||||
-- 11. Unknown custom soul bag found through its "Soul Bag" subtype
|
||||
reset(); setBag(0, 16); setBag(3, 20, 99999); put(0, 1, 6265, 1)
|
||||
load(false); ShardCapDB.cap = 10; fire("BAG_UPDATE"); run()
|
||||
check(count(3) == 1, "soul bag detected by subtype")
|
||||
|
||||
-- 12. Options window (ClassicAPI AddOn Options)
|
||||
print("[options]")
|
||||
local function findControl(page, label)
|
||||
for _, c in ipairs(page.controls) do
|
||||
local t = c.name and _G[c.name .. "Text"]
|
||||
if t and string.find(t.text, label, 1, true) then return c end
|
||||
end
|
||||
end
|
||||
local function click(c) if c.checked then c.checked = nil else c.checked = 1 end; call(c, "OnClick") end
|
||||
local function findKind(page, kind)
|
||||
for _, c in ipairs(page.controls) do if c.kind == kind then return c end end
|
||||
end
|
||||
|
||||
reset(); setBag(0, 16); setBag(4, 12, 22243)
|
||||
put(0, 1, 6265, 3); put(0, 2, 6265, 2); put(4, 1, 16583, 2); put(0, 3, 5565, 4)
|
||||
bank = { [6265] = 4, [5565] = 10 }
|
||||
load(true, true); ShardCapDB.cap = 12
|
||||
local main, bp = categories[1], categories[2]
|
||||
check(main and main.name == "ShardCap" and bp and bp.name == "Soul Bag" and bp.parent == "ShardCap", "two pages registered, Soul Bag under ShardCap")
|
||||
|
||||
SlashCmdList.SHARDCAP("options")
|
||||
check(categories.opened == main and main.shown, "/shardcap options opens the ShardCap page")
|
||||
local texts = {}
|
||||
for _, f in ipairs(allWidgets) do if f.kind == "FontString" and f.text ~= "" then table.insert(texts, f.text) end end
|
||||
local joined = table.concat(texts, "\n")
|
||||
check(string.find(joined, "5/12", 1, true) ~= nil, "status shows 5/12")
|
||||
check(string.find(joined, "(+4 in bank)", 1, true) ~= nil, "status shows shards in bank")
|
||||
check(string.find(joined, "Infernal Stones: |cffffffff4|r |cffaaaaaa(+10 in bank)|r", 1, true) ~= nil, "status shows stones with bank count")
|
||||
|
||||
local slider = findKind(main, "Slider")
|
||||
check(slider.value == 12, "slider shows the cap")
|
||||
slider:SetValue(30)
|
||||
check(ShardCapDB.cap == 30, "slider sets the cap")
|
||||
local box
|
||||
for _, f in ipairs(frames) do if f.kind == "EditBox" then box = f end end
|
||||
check(box.text == "30", "box follows the slider")
|
||||
box:SetFocus(); box:SetText("150"); call(box, "OnEnterPressed")
|
||||
check(ShardCapDB.cap == 150 and slider.value == 100, "box sets caps above the slider range")
|
||||
box:SetFocus(); box:SetText("250"); call(box, "OnEnterPressed")
|
||||
check(ShardCapDB.cap == 150 and box.text == "150", "out-of-range box value rejected")
|
||||
box:SetFocus(); box:SetText("7"); call(box, "OnEscapePressed")
|
||||
check(ShardCapDB.cap == 150 and box.text == "150", "Escape in the box discards the edit")
|
||||
|
||||
local notify = findControl(main, "Chat notifications")
|
||||
click(notify); check(ShardCapDB.notify == true, "notifications checkbox")
|
||||
|
||||
-- Cancel restores what was there when the window opened; Okay keeps changes
|
||||
main.cancel(main); bp.cancel(bp)
|
||||
check(ShardCapDB.cap == 12 and ShardCapDB.notify == false, "Cancel restores settings")
|
||||
SlashCmdList.SHARDCAP("options"); slider:SetValue(20); main.okay(main); bp.okay(bp)
|
||||
SlashCmdList.SHARDCAP("options"); main.cancel(main)
|
||||
check(ShardCapDB.cap == 20, "Okay keeps settings")
|
||||
|
||||
-- Defaults reset only the page shown
|
||||
ShardCapDB.notify = true; ShardCapDB.reagents = false
|
||||
main.default(main)
|
||||
check(ShardCapDB.cap == 12 and ShardCapDB.notify == false and ShardCapDB.reagents == false, "Defaults on ShardCap page leave Soul Bag settings")
|
||||
bp.default(bp)
|
||||
check(ShardCapDB.reagents == true, "Defaults on Soul Bag page")
|
||||
|
||||
-- Soul bag page
|
||||
bp:Show(); bp.refresh(bp)
|
||||
local auto = findControl(bp, "Detect soul bags automatically")
|
||||
local bag4 = findControl(bp, "Bag 4")
|
||||
local bag2 = findControl(bp, "Bag 2")
|
||||
check(auto.checked and bag4.checked and not bag4.enabled, "auto detection on; bag 4 shown as soul bag but locked")
|
||||
check(not bag2.enabled and string.find(_G[bag2.name .. "Text"].text, "empty slot", 1, true) ~= nil, "empty bag slot greyed out")
|
||||
click(auto)
|
||||
check(type(ShardCapDB.soulbags) == "table" and ShardCapDB.soulbags[4] and bag4.enabled, "manual mode seeded from detection")
|
||||
click(bag4)
|
||||
check(not ShardCapDB.soulbags[4], "bag checkbox clears a soul bag")
|
||||
click(auto)
|
||||
check(ShardCapDB.soulbags == nil, "back to auto")
|
||||
local reag = findControl(bp, "Demonic Figurines and Infernal Stones")
|
||||
click(reag); check(ShardCapDB.reagents == false, "reagents checkbox")
|
||||
|
||||
-- Live refresh only while shown
|
||||
put(0, 4, 6265, 1)
|
||||
this = main; arg1 = 0.6; main.scripts.OnUpdate()
|
||||
texts = {}
|
||||
for _, f in ipairs(allWidgets) do if f.kind == "FontString" then table.insert(texts, f.text) end end
|
||||
check(string.find(table.concat(texts, "\n"), "6/12", 1, true) ~= nil, "status refreshes while open")
|
||||
|
||||
-- Without ClassicAPI's options window
|
||||
reset(); setBag(0, 16)
|
||||
load(false, false); chat = {}
|
||||
SlashCmdList.SHARDCAP("options")
|
||||
check(table.getn(categories) == 0 and string.find(chat[1] or "", "needs ClassicAPI", 1, true) ~= nil, "no panel without ClassicAPI; slash explains")
|
||||
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")
|
||||
|
||||
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