From 6670b11c59ee0cbd1a36299d6792f59f09392a5c Mon Sep 17 00:00:00 2001 From: Raenore <172234435+Raenore@users.noreply.github.com> Date: Sun, 15 Feb 2026 08:12:35 +0100 Subject: [PATCH 01/13] Track correct reflecting prism aura --- Core/Options.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Options.lua b/Core/Options.lua index 5764724..6cee368 100644 --- a/Core/Options.lua +++ b/Core/Options.lua @@ -92,7 +92,7 @@ SIPPYCUP.Options.Data = { NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 393979, itemID = 201428, category = "EFFECT" }, -- QUICKSILVER_SANDS NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1213974, itemID = 234287, category = "EFFECT", preExpiration = true }, -- RADIANT_FOCUS NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1214287, itemID = 234527, category = "HANDHELD", preExpiration = true }, -- SACREDITES_LEDGER - NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 163219, itemID = 112384, category = "PRISM", preExpiration = true }, -- REFLECTING_PRISM + NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 163267, itemID = 112384, category = "PRISM", preExpiration = true }, -- REFLECTING_PRISM NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 279742, itemID = 163695, category = "EFFECT" }, -- SCROLL_OF_INNER_TRUTH NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1222834, itemID = 237332, category = "PLACEMENT", spellTrackable = true }, -- SINGLE_USE_GRILL NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 58479, itemID = 43480, category = "SIZE", preExpiration = true, delayedAura = true }, -- SMALL_FEAST From bbf05d56891d185b948c2fbb20e29c73bd4ce0d4 Mon Sep 17 00:00:00 2001 From: Raenore <172234435+Raenore@users.noreply.github.com> Date: Sun, 15 Feb 2026 08:15:06 +0100 Subject: [PATCH 02/13] Reflecting Prism uses charges (and has no bag update generally) --- Modules/Popups/Popups.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/Popups/Popups.lua b/Modules/Popups/Popups.lua index 6e66bcb..2a3847b 100644 --- a/Modules/Popups/Popups.lua +++ b/Modules/Popups/Popups.lua @@ -734,7 +734,8 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) -- Pre-expiration also does not do any bag changes, so mark as synchronised in case. -- Delayed (e.g. eating x seconds) UNIT_AURA calls, mark bag as synchronized (as it was removed earlier). -- Toys UNIT_AURA calls, mark bag as synchronized (as no items are actually used). - if reason == SIPPYCUP.Popups.Reason.REMOVAL or reason == SIPPYCUP.Popups.Reason.PRE_EXPIRATION or optionData.delayedAura or optionData.type == SIPPYCUP.Options.Type.TOY then + -- Reflecting Prism (spellID == 163267) uses charges, and does not require a bag sync update generally. + if reason == SIPPYCUP.Popups.Reason.REMOVAL or reason == SIPPYCUP.Popups.Reason.PRE_EXPIRATION or optionData.delayedAura or optionData.type == SIPPYCUP.Options.Type.TOY or auraID == 163267 then SIPPYCUP.Items.HandleBagUpdate(); end From 07a0c2a72b45d52c83a71cf1ee7cbccb7f6505a1 Mon Sep 17 00:00:00 2001 From: Raenore <172234435+Raenore@users.noreply.github.com> Date: Sun, 15 Feb 2026 11:09:42 +0100 Subject: [PATCH 03/13] Handle multiple itemIDs (projecting prism) & Group requirement check --- Core/Options.lua | 112 +++++++++++++---------- Core/Utils.lua | 47 ++++++---- Locales/enUS.lua | 1 + Locales/esES.lua | 1 + Locales/frFR.lua | 1 + Locales/ruRU.lua | 1 + Modules/Popups/Popups.lua | 182 ++++++++++++++++++++++++++++---------- UI/Config.lua | 29 +++++- 8 files changed, 256 insertions(+), 118 deletions(-) diff --git a/Core/Options.lua b/Core/Options.lua index 6cee368..5e89574 100644 --- a/Core/Options.lua +++ b/Core/Options.lua @@ -17,7 +17,7 @@ SIPPYCUP.Options.Type = { ---@class SIPPYCUPOption: table ---@field type string Whether this option is a consumable (0) or toy (1). ---@field auraID number The option's aura ID. ----@field itemID number The option's item ID. +---@field itemID number|table The option's item ID(s) ---@field loc string The option's localization key (auto-gen). ---@field category string The option's category (e.g., potion, food). ---@field profile table The option's associated DB profile (auto-gen). @@ -36,10 +36,15 @@ SIPPYCUP.Options.Type = { ---@param params SIPPYCUPOption A table containing parameters for the new option. ---@return SIPPYCUPOption local function NewOption(params) + local itemIDs = params.itemID; + if type(itemIDs) == "number" then + itemIDs = { itemIDs }; + end + return { type = params.type or SIPPYCUP.Options.Type.CONSUMABLE, auraID = params.auraID, - itemID = params.itemID, + itemID = itemIDs; -- always store as a table internally loc = params.loc, category = params.category, profile = params.profile, @@ -53,6 +58,7 @@ local function NewOption(params) delayedAura = params.delayedAura or false, cooldownMismatch = params.cooldownMismatch or false, buildAdded = params.buildAdded or nil, + requiresGroup = params.requiresGroup or false, }; end @@ -85,14 +91,14 @@ SIPPYCUP.Options.Data = { NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 185394, itemID = 124640, category = "EFFECT", preExpiration = true }, -- INKY_BLACK_POTION NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1218300, itemID = 235703, category = "SIZE", stacks = true, maxStacks = 10, preExpiration = true }, -- NOGGENFOGGER_SELECT_DOWN NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1218297, itemID = 235704, category = "SIZE", stacks = true, maxStacks = 10, preExpiration = true }, -- NOGGENFOGGER_SELECT_UP - NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 374957, itemID = 193029, category = "PRISM", preExpiration = true }, -- PROJECTION_PRISM + NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 374959, itemID = {193031, 193030, 193029}, category = "PRISM", preExpiration = true, requiresGroup = true }, -- PROJECTION_PRISM TO-DO: Implement selector, currently Gold > Silver > Bronze NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 368038, itemID = 190739, category = "EFFECT", preExpiration = true }, -- PROVIS_WAX NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 244015, itemID = 151256, category = "HANDHELD", preExpiration = true }, -- PURPLE_DANCE_STICK NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 53805, itemID = 40195, category = "SIZE", stacks = true, maxStacks = 10 }, -- PYGMY_OIL NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 393979, itemID = 201428, category = "EFFECT" }, -- QUICKSILVER_SANDS NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1213974, itemID = 234287, category = "EFFECT", preExpiration = true }, -- RADIANT_FOCUS NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1214287, itemID = 234527, category = "HANDHELD", preExpiration = true }, -- SACREDITES_LEDGER - NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 163267, itemID = 112384, category = "PRISM", preExpiration = true }, -- REFLECTING_PRISM + NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 163267, itemID = 112384, category = "PRISM", preExpiration = true, requiresGroup = true }, -- REFLECTING_PRISM NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 279742, itemID = 163695, category = "EFFECT" }, -- SCROLL_OF_INNER_TRUTH NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1222834, itemID = 237332, category = "PLACEMENT", spellTrackable = true }, -- SINGLE_USE_GRILL NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 58479, itemID = 43480, category = "SIZE", preExpiration = true, delayedAura = true }, -- SMALL_FEAST @@ -216,21 +222,33 @@ function SIPPYCUP.Options.Setup() -- Build lookups for _, option in ipairs(data) do - remaining[option.itemID] = true; + for _, id in ipairs(option.itemID) do + SIPPYCUP.Options.ByItemID[id] = option; + remaining[id] = true; + end SIPPYCUP.Options.ByAuraID[option.auraID] = option; - SIPPYCUP.Options.ByItemID[option.itemID] = option; end -- Fast prune: remove invalid items before async loads for i = #data, 1, -1 do local option = data[i]; + local valid = false; - if C_Item.GetItemInfoInstant(option.itemID) == nil then - SIPPYCUP.Options.ByAuraID[option.auraID] = nil; - SIPPYCUP.Options.ByItemID[option.itemID] = nil; + for _, id in ipairs(option.itemID) do + if C_Item.GetItemInfoInstant(id) ~= nil then + valid = true; + break; + end + end + if not valid then + -- Remove all itemIDs from lookups + for _, id in ipairs(option.itemID) do + SIPPYCUP.Options.ByItemID[id] = nil; + remaining[id] = nil; + end + SIPPYCUP.Options.ByAuraID[option.auraID] = nil; table.remove(data, i); - remaining[option.itemID] = nil; end end @@ -256,27 +274,40 @@ function SIPPYCUP.Options.Setup() -- Async load for all valid items for _, option in ipairs(data) do - local item = Item:CreateFromItemID(option.itemID); + -- pick first valid itemID number to feed into Item:CreateFromItemID + local firstID; + for _, id in ipairs(option.itemID) do + if type(id) == "number" then + firstID = id; + break; + end + end - item:ContinueOnItemLoad(function() - -- Skip if removed by earlier pruning - if not remaining[option.itemID] then return end; + if firstID then + local item = Item:CreateFromItemID(firstID); - local name = item:GetItemName(); + item:ContinueOnItemLoad(function() + if not remaining[firstID] then return; end; - option.name = name; - option.loc = NormalizeLocName(name); + local name = item:GetItemName(); + option.name = name; + option.loc = NormalizeLocName(name); - option.profile = string.gsub(string.lower(option.loc), "_(%a)", function(c) - return c:upper(); - end); + option.profile = string.gsub(string.lower(option.loc), "_(%a)", function(c) + return c:upper(); + end); - option.icon = item:GetItemIcon(); - SIPPYCUP.Options.ByName[name] = option; + option.icon = item:GetItemIcon(); + SIPPYCUP.Options.ByName[name] = option; - remaining[option.itemID] = nil; - Finalize(); - end); + -- mark all itemIDs as loaded + for _, id in ipairs(option.itemID) do + remaining[id] = nil; + end + + Finalize(); + end); + end end end @@ -291,39 +322,26 @@ function SIPPYCUP.Options.RefreshStackSizes(checkAll, reset, preExpireOnly) -- Helper to check cooldown startTime for item or spell trackable local function GetCooldownStartTime(option) - local trackBySpell = false; - local trackByItem = false; - - if option.type == SIPPYCUP.Options.Type.CONSUMABLE then - trackBySpell = option.spellTrackable; - trackByItem = option.itemTrackable; - elseif option.type == SIPPYCUP.Options.Type.TOY then - -- Always track by item if itemTrackable - if option.itemTrackable then - trackByItem = true; - end + local trackBySpell = option.spellTrackable or false; + local trackByItem = option.itemTrackable or false; - if option.spellTrackable then - if SIPPYCUP.global.UseToyCooldown then - trackByItem = true; - else - trackBySpell = true; + if trackByItem then + for _, id in ipairs(option.itemID) do + local startTime = C_Item.GetItemCooldown(id); + if startTime and startTime > 0 then + return startTime; end end end - if trackByItem then - local startTime = C_Item.GetItemCooldown(option.itemID); - if startTime and startTime > 0 then - return startTime; - end - elseif trackBySpell then + if trackBySpell then local spellCooldown = C_Spell.GetSpellCooldown(option.auraID); local startTime = spellCooldown and spellCooldown.startTime; if startTime and startTime > 0 then return startTime; end end + return nil; end diff --git a/Core/Utils.lua b/Core/Utils.lua index a52600c..ef5a1ab 100644 --- a/Core/Utils.lua +++ b/Core/Utils.lua @@ -125,32 +125,41 @@ function SIPPYCUP_OUTPUT.Write(output, command) Print(formattedOutput); end +local function formatValue(val, isTop) + if type(val) == "table" then + local isArray = (#val > 0); + if isArray then + local items = {}; + for _, v in ipairs(val) do + table.insert(items, formatValue(v)); + end + return "{" .. table.concat(items, ",") .. "}"; + else + local items = {} + for k, v in pairs(val) do + table.insert(items, tostring(k) .. ": " .. formatValue(v)); + end + if isTop then + return table.concat(items, ", "); + else + return "{" .. table.concat(items, ", ") .. "}"; + end + end + else + return tostring(val); + end +end + ---Debug prints formatted output, only works when IS_DEV_BUILD is true. ---Accepts any number of arguments and joins them with space. ---@param ... any Values to print (strings, numbers, tables, etc.) function SIPPYCUP_OUTPUT.Debug(...) - if not SIPPYCUP.IS_DEV_BUILD then - return; - end + if not SIPPYCUP.IS_DEV_BUILD then return; end - local args = { ... }; + local args = {...}; local outputLines = {}; - for _, arg in ipairs(args) do - if type(arg) == "table" then - local isArray = (#arg > 0); - if isArray then - for _, v in ipairs(arg) do - table.insert(outputLines, tostring(v)); - end - else - for k, v in pairs(arg) do - table.insert(outputLines, tostring(k) .. ": " .. tostring(v)); - end - end - else - table.insert(outputLines, tostring(arg)); - end + table.insert(outputLines, formatValue(arg, true)); end local finalOutput = table.concat(outputLines, " "); diff --git a/Locales/enUS.lua b/Locales/enUS.lua index 40f12f2..4a973db 100644 --- a/Locales/enUS.lua +++ b/Locales/enUS.lua @@ -12,6 +12,7 @@ L = { --/ Popup dialog /-- POPUP_ON_COOLDOWN_TEXT = "On Cooldown", POPUP_IN_FLIGHT_TEXT = "Disabled to prevent dismount during flight.", + POPUP_NOT_IN_PARTY_TEXT = "Disabled as item requires a party.", POPUP_FOOD_BUFF_TEXT = "Disappears once food buff is applied. Do not move!", POPUP_NOT_IN_INVENTORY_TEXT = "Not in Inventory", POPUP_NOT_ENOUGH_IN_INVENTORY_TEXT = "Not Enough (%d Missing)", diff --git a/Locales/esES.lua b/Locales/esES.lua index 588c258..6416ab3 100644 --- a/Locales/esES.lua +++ b/Locales/esES.lua @@ -13,6 +13,7 @@ L = { --/ Popup dialog /-- POPUP_ON_COOLDOWN_TEXT = "En tiempo de espera", POPUP_IN_FLIGHT_TEXT = "Desactivado para evitar el desmontaje durante el vuelo.", + POPUP_NOT_IN_PARTY_TEXT = "Disabled as item requires a party.", -- (NEW) POPUP_FOOD_BUFF_TEXT = "Desaparece una vez que se aplica el beneficio de comida. ¡No te muevas!", POPUP_NOT_IN_INVENTORY_TEXT = "No hay en el inventario", POPUP_NOT_ENOUGH_IN_INVENTORY_TEXT = "No es suficiente (%d Faltante)", diff --git a/Locales/frFR.lua b/Locales/frFR.lua index 6d59ceb..2f8358d 100644 --- a/Locales/frFR.lua +++ b/Locales/frFR.lua @@ -13,6 +13,7 @@ L = { --/ Popup dialog /-- POPUP_ON_COOLDOWN_TEXT = "En recharge", POPUP_IN_FLIGHT_TEXT = "Désactivé pour ne pas tomber de la monture durant le vol.", + POPUP_NOT_IN_PARTY_TEXT = "Disabled as item requires a party.", -- (NEW) POPUP_FOOD_BUFF_TEXT = "Disparaît une fois que le buff de nourriture est appliqué. Ne bougez pas !", POPUP_NOT_IN_INVENTORY_TEXT = "Pas dans l'inventaire", POPUP_NOT_ENOUGH_IN_INVENTORY_TEXT = "Insuffisant (%d manquant)", diff --git a/Locales/ruRU.lua b/Locales/ruRU.lua index 42c5e3f..babfdf8 100644 --- a/Locales/ruRU.lua +++ b/Locales/ruRU.lua @@ -13,6 +13,7 @@ L = { --/ Popup dialog /-- POPUP_ON_COOLDOWN_TEXT = "currently on cooldown.", -- (NEW) POPUP_IN_FLIGHT_TEXT = "Disabled to prevent dismount during flight.", -- (NEW) + POPUP_NOT_IN_PARTY_TEXT = "Disabled as item requires a party.", -- (NEW) POPUP_FOOD_BUFF_TEXT = "Disappears once food buff is applied. Do not move!", -- (NEW) POPUP_NOT_IN_INVENTORY_TEXT = "не найден в вашем инвентаре.", -- (TEXT CHANGE IN ENGLISH) POPUP_NOT_ENOUGH_IN_INVENTORY_TEXT = "недостаточно в вашем инвентаре.|n(%d missing)", -- (NEW) (Ending) diff --git a/Modules/Popups/Popups.lua b/Modules/Popups/Popups.lua index 2a3847b..9a7fc42 100644 --- a/Modules/Popups/Popups.lua +++ b/Modules/Popups/Popups.lua @@ -177,6 +177,11 @@ local function CreatePopup(templateType) local itemID = data and data.optionData and data.optionData.itemID; if not itemID then return; end + -- Normalize to single ID if a table + if type(itemID) == "table" then + itemID = itemID[1]; -- use the first ID for tooltip + end + local item = Item:CreateFromItemID(itemID); item:ContinueOnItemLoad(function() @@ -197,35 +202,51 @@ local function CreatePopup(templateType) local tooltipText; local currentPopup = self:GetParent(); local popupData = currentPopup and currentPopup.popupData; + local optionData = popupData.optionData; if isFlying then popupData.isFlying = true; self:Disable(); end + if optionData.requiresGroup then + if popupData.isNotinGroupButRequired and UnitInParty("player") then + popupData.isNotinGroupButRequired = false; + self:Enable(); + elseif not UnitInParty("player") then + popupData.isNotinGroupButRequired = true; + self:Disable() + end + end + if not self:IsEnabled() then if isFlying then tooltipText = "|cnWARNING_FONT_COLOR:" .. L.POPUP_IN_FLIGHT_TEXT .. "|r"; + elseif popupData.isNotinGroupButRequired then + tooltipText = "|cnWARNING_FONT_COLOR:" .. L.POPUP_NOT_IN_PARTY_TEXT .. "|r"; elseif popupData and popupData.optionData and popupData.optionData.delayedAura then tooltipText = "|cnWARNING_FONT_COLOR:" .. L.POPUP_FOOD_BUFF_TEXT .. "|r"; else tooltipText = "|cnWARNING_FONT_COLOR:" .. L.POPUP_ON_COOLDOWN_TEXT .. "|r"; end else - if popupData then - local optionData = popupData.optionData; - if optionData.type == SIPPYCUP.Options.Type.CONSUMABLE then - local profileOptionData = popupData.profileOptionData; - - local itemID = optionData.itemID; - local itemCount = C_Item.GetItemCount(itemID); - local maxCount = itemCount + profileOptionData.currentStacks; - - if itemCount == 0 then - tooltipText = "|cnWARNING_FONT_COLOR:" .. L.POPUP_NOT_IN_INVENTORY_TEXT .. "|r"; - elseif maxCount < profileOptionData.desiredStacks then - tooltipText = "|cnWARNING_FONT_COLOR:" .. L.POPUP_NOT_ENOUGH_IN_INVENTORY_TEXT:format(profileOptionData.desiredStacks - maxCount); + if optionData.type == SIPPYCUP.Options.Type.CONSUMABLE then + local profileOptionData = popupData.profileOptionData; + + local itemCount = 0 + if type(optionData.itemID) == "table" then + for _, id in ipairs(optionData.itemID) do + itemCount = itemCount + C_Item.GetItemCount(id) end + else + itemCount = C_Item.GetItemCount(optionData.itemID) + end + local maxCount = itemCount + profileOptionData.currentStacks; + + if itemCount == 0 then + tooltipText = "|cnWARNING_FONT_COLOR:" .. L.POPUP_NOT_IN_INVENTORY_TEXT .. "|r"; + elseif maxCount < profileOptionData.desiredStacks then + tooltipText = "|cnWARNING_FONT_COLOR:" .. L.POPUP_NOT_ENOUGH_IN_INVENTORY_TEXT:format(profileOptionData.desiredStacks - maxCount); end end end @@ -245,6 +266,11 @@ local function CreatePopup(templateType) popupData.isFlying = false; self:Enable(); end + + local optionData = popupData.optionData; + if optionData.requiresGroup and popupData.isNotinGroupButRequired and UnitInParty("player") then + self:Enable() + end GameTooltip:Hide(); end); @@ -253,8 +279,16 @@ local function CreatePopup(templateType) local currentPopup = self:GetParent(); local popupData = currentPopup and currentPopup.popupData; local optionData = popupData.optionData; - local itemID = optionData.itemID; - local itemCount = C_Item.GetItemCount(itemID); + + -- Sum counts if multiple item IDs + local itemCount = 0 + if type(optionData.itemID) == "table" then + for _, id in ipairs(optionData.itemID) do + itemCount = itemCount + C_Item.GetItemCount(id); + end + else + itemCount = C_Item.GetItemCount(optionData.itemID); + end -- If no more charges, don't disable as next charge doesn't exist to re-enable. if itemCount > 0 then @@ -368,7 +402,23 @@ local function UpdatePopupVisuals(popup, data) local optionData = data.optionData; local profileOptionData = data.profileOptionData; - local itemID = optionData.itemID; + -- Determine which itemID to use for tooltip/icon/cooldown + local itemID; + if type(optionData.itemID) == "table" then + itemID = nil; + for _, id in ipairs(optionData.itemID) do + if C_Item.GetItemCount(id) > 0 then + itemID = id; + break; + end + end + -- fallback to first ID if none have count > 0 + if not itemID then + itemID = optionData.itemID[1]; + end + else + itemID = optionData.itemID; + end local itemName, itemLink = C_Item.GetItemInfo(itemID); itemName = itemName or optionData.name; @@ -435,7 +485,16 @@ local function UpdatePopupVisuals(popup, data) popup.RefreshButton:Enable(); end elseif popup.templateType == "SIPPYCUP_MissingPopupTemplate" then - local itemCount = C_Item.GetItemCount(itemID) or 0; + -- Sum item counts if multiple IDs + local itemCount = 0; + if type(optionData.itemID) == "table" then + for _, id in ipairs(optionData.itemID) do + itemCount = itemCount + C_Item.GetItemCount(id); + end + else + itemCount = C_Item.GetItemCount(optionData.itemID); + end + local text = L.POPUP_INSUFFICIENT_NEXT_REFRESH_TEXT:format(itemCount, profileOptionData.desiredStacks); popup.Text:SetText(text or ""); popup.OkayButton:SetText(OKAY); @@ -735,7 +794,8 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) -- Delayed (e.g. eating x seconds) UNIT_AURA calls, mark bag as synchronized (as it was removed earlier). -- Toys UNIT_AURA calls, mark bag as synchronized (as no items are actually used). -- Reflecting Prism (spellID == 163267) uses charges, and does not require a bag sync update generally. - if reason == SIPPYCUP.Popups.Reason.REMOVAL or reason == SIPPYCUP.Popups.Reason.PRE_EXPIRATION or optionData.delayedAura or optionData.type == SIPPYCUP.Options.Type.TOY or auraID == 163267 then + if reason == SIPPYCUP.Popups.Reason.REMOVAL or reason == SIPPYCUP.Popups.Reason.PRE_EXPIRATION + or optionData.delayedAura or optionData.type == SIPPYCUP.Options.Type.TOY or auraID == 163267 or auraID == 374959 then SIPPYCUP.Items.HandleBagUpdate(); end @@ -787,7 +847,6 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) auraInfo = C_UnitAuras.GetPlayerAuraBySpellID(auraID); if auraInfo then - print("auraInfo found!"); local auraInstanceID = auraInfo.auraInstanceID; SIPPYCUP.Database.instanceToProfile[auraInstanceID] = profileOptionData; profileOptionData.currentInstanceID = auraInstanceID; @@ -803,8 +862,6 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) active = true; end - local auraInstanceID = auraInfo and auraInfo.auraInstanceID; - local trackBySpell = false; local trackByItem = false; @@ -812,11 +869,9 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) trackBySpell = optionData.spellTrackable; trackByItem = optionData.itemTrackable; elseif optionData.type == SIPPYCUP.Options.Type.TOY then - -- Always track by item if itemTrackable if optionData.itemTrackable then trackByItem = true; end - if optionData.spellTrackable then if SIPPYCUP.global.UseToyCooldown then trackByItem = true; @@ -829,17 +884,16 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) -- Extra check because toys have longer cooldowns than option tend to, so don't fire if cd is still up. if optionData.type == SIPPYCUP.Options.Type.TOY and reason == SIPPYCUP.Popups.Reason.REMOVAL then local cooldownActive = false; - - -- If item can only be tracked by the item cooldown (worst) if trackByItem then - SIPPYCUP_OUTPUT.Debug("Tracking through Item"); - local startTime = C_Item.GetItemCooldown(optionData.itemID); - if startTime and startTime > 0 then - cooldownActive = true; + local itemIDs = type(optionData.itemID) == "table" and optionData.itemID or { optionData.itemID }; + for _, id in ipairs(itemIDs) do + local startTime, duration = C_Container.GetItemCooldown(id); + if startTime and duration and duration > 0 and (startTime + duration - GetTime() > 0) then + cooldownActive = true; + break; + end end - -- If item can be tracked through the spell cooldown (fine). elseif trackBySpell then - SIPPYCUP_OUTPUT.Debug("Tracking through Spell"); local spellCooldownInfo = C_Spell.GetSpellCooldown(optionData.auraID); local startTime; if canaccessvalue == nil or canaccessvalue(spellCooldownInfo) then @@ -849,8 +903,6 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) cooldownActive = true; end end - - -- Cooldown is active when removal happened? We don't show anything. if cooldownActive then return; end @@ -858,34 +910,66 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) SIPPYCUP_OUTPUT.Debug({ caller = caller, auraID = optionData.auraID, itemID = optionData.itemID, name = optionData.name }); - -- First, let's grab the latest currentInstanceID (or have it be nil if none which is fine). - profileOptionData.currentInstanceID = (auraInfo and auraInfo.auraInstanceID) or auraInstanceID; + -- Original preference + local itemIDs = type(optionData.itemID) == "table" and optionData.itemID or { optionData.itemID }; + local usableItemID; + local itemCount = 0; - -- If the option does not support stacks, we always desire just 1. - profileOptionData.desiredStacks = optionData.stacks and profileOptionData.desiredStacks or 1; + -- Track which itemIDs are depleted + local depleted = {}; - local itemCount; - if optionData.type == SIPPYCUP.Options.Type.CONSUMABLE then - profileOptionData.currentStacks = SIPPYCUP.Auras.CalculateCurrentStacks(auraInfo, auraID, reason, active); - itemCount = C_Item.GetItemCount(optionData.itemID); - elseif optionData.type == SIPPYCUP.Options.Type.TOY then - if auraInfo then - profileOptionData.currentStacks = SIPPYCUP.Auras.CalculateCurrentStacks(auraInfo, auraID, reason, active); + for _, id in ipairs(itemIDs) do + local count = C_Item.GetItemCount(id); + SIPPYCUP_OUTPUT.Debug("Checking itemID:", id, "count:", count); + + if count > 0 then + local startTime, duration = C_Container.GetItemCooldown(id); + if not startTime or startTime + duration <= GetTime() then + usableItemID = id; + itemCount = count; + SIPPYCUP_OUTPUT.Debug("Usable found:", id, "x"..count); + end else - profileOptionData.currentStacks = active and 1 or 0; + SIPPYCUP_OUTPUT.Debug("No items for:", id); + depleted[id] = true; + end + end + + -- Remove depleted items from optionData.itemID, but always keep at least the last one + local newItemIDs = {}; + for i, id in ipairs(itemIDs) do + if not depleted[id] or i == #itemIDs then + newItemIDs[#newItemIDs + 1] = id; end - itemCount = PlayerHasToy(optionData.itemID) and 1 or 0; end + optionData.itemID = newItemIDs; - local requiredStacks = profileOptionData.desiredStacks - profileOptionData.currentStacks; + -- Only update the *current usable item* + if usableItemID then + profileOptionData.currentItemID = usableItemID; + else + -- No usable item available, popup can report zero count + profileOptionData.currentItemID = itemIDs[#itemIDs]; + SIPPYCUP_OUTPUT.Debug("No usable item, reporting last itemID:", profileOptionData.currentItemID); + --[[ TO-DO: Consider if we require this here + RemoveDeferredActionsByLoc(optionData.loc); + local existingPopup = SIPPYCUP.Popups.activeByLoc[optionData.loc]; + if existingPopup and existingPopup:IsShown() then + existingPopup:Hide(); + end + return; + ]] + end SIPPYCUP.Popups.HandleReminderPopup({ + active = auraInfo and true or active, + auraID = auraID, + auraInfo = auraInfo, optionData = optionData, profileOptionData = profileOptionData, - requiredStacks = requiredStacks, reason = reason, + requiredStacks = profileOptionData.desiredStacks - profileOptionData.currentStacks, itemCount = itemCount, - active = active, }); end diff --git a/UI/Config.lua b/UI/Config.lua index 10a03ba..7ab754f 100644 --- a/UI/Config.lua +++ b/UI/Config.lua @@ -37,6 +37,26 @@ function SIPPYCUP.Config.TryCreateConfigFrame() end end +-- Utility to get the first valid itemID from number or table +local function GetFirstItemID(itemID) + if type(itemID) == "table" then + for _, id in ipairs(itemID) do + if id and id > 0 then + return id; + end + end + return nil; + else + return itemID; + end +end + +-- Example: toy checkbox disabled function +local function IsToyDisabled(toyID) + local firstID = GetFirstItemID(toyID); + return not firstID or not PlayerHasToy(firstID); +end + ---AddTab creates a new tab button under the given parent frame and adds it to the parent's Tabs list. ---It positions the new tab relative to existing tabs, sets up its scripts for show and click events, and registers it for ElvUI skinning. ---@param parent table Frame containing the Tabs table and the SetTab function. @@ -207,17 +227,20 @@ end ---AttachItemTooltip adds mouseover tooltips showing item info by itemID to one or multiple frames. ---It sets up OnEnter and OnLeave scripts to show and hide the item tooltip anchored as specified. ---@param frames table|table[] The single frame or list of frames to attach the tooltip to. ----@param itemID number The item ID to create the tooltip for. +---@param itemID number|number[] The item ID(s) to create the tooltip for. ---@param anchor string? Optional anchor point for tooltip, defaults to "ANCHOR_TOP". local function AttachItemTooltip(frames, itemID, anchor) if not itemID then return; end anchor = anchor or "ANCHOR_TOP"; + local firstID = GetFirstItemID(itemID); + if not firstID then return; end + local isList = type(frames) == "table" and not frames.GetObjectType; local firstFrame = isList and frames[1] or frames; if not firstFrame then return; end - local item = Item:CreateFromItemID(itemID); + local item = Item:CreateFromItemID(firstID); item:ContinueOnItemLoad(function() local itemLink = item:GetItemLink(); @@ -1634,7 +1657,7 @@ function SIPPYCUP_ConfigMixin:OnLoad() stacks = optionData.stacks, buildAdded = optionData.buildAdded, disabled = function() - return not PlayerHasToy(toyID); + return IsToyDisabled(toyID); end, get = function() return SIPPYCUP.Database.GetSetting("profile", checkboxProfileKey, "enable"); From eac25dcf93b8e3b482a39816ae051f0f4d56e6fa Mon Sep 17 00:00:00 2001 From: Raenore <172234435+Raenore@users.noreply.github.com> Date: Sun, 15 Feb 2026 11:12:47 +0100 Subject: [PATCH 04/13] Tabs reign superior --- Modules/Popups/Popups.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Popups/Popups.lua b/Modules/Popups/Popups.lua index 9a7fc42..fe3239a 100644 --- a/Modules/Popups/Popups.lua +++ b/Modules/Popups/Popups.lua @@ -795,7 +795,7 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) -- Toys UNIT_AURA calls, mark bag as synchronized (as no items are actually used). -- Reflecting Prism (spellID == 163267) uses charges, and does not require a bag sync update generally. if reason == SIPPYCUP.Popups.Reason.REMOVAL or reason == SIPPYCUP.Popups.Reason.PRE_EXPIRATION - or optionData.delayedAura or optionData.type == SIPPYCUP.Options.Type.TOY or auraID == 163267 or auraID == 374959 then + or optionData.delayedAura or optionData.type == SIPPYCUP.Options.Type.TOY or auraID == 163267 or auraID == 374959 then SIPPYCUP.Items.HandleBagUpdate(); end From 12a435ef01815575b4522525dd3f6b2c48f3ccc0 Mon Sep 17 00:00:00 2001 From: Raenore <172234435+Raenore@users.noreply.github.com> Date: Sun, 15 Feb 2026 18:50:52 +0100 Subject: [PATCH 05/13] Return proper stack calculation & toy cooldown support --- Modules/Popups/Popups.lua | 62 +++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/Modules/Popups/Popups.lua b/Modules/Popups/Popups.lua index fe3239a..b3245a0 100644 --- a/Modules/Popups/Popups.lua +++ b/Modules/Popups/Popups.lua @@ -789,13 +789,20 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) local auraInfo = data.auraInfo; local currentInstanceID = profileOptionData.currentInstanceID; + local optionType = optionData.type; + local isToy = optionType == SIPPYCUP.Options.Type.TOY; + local isConsumable = optionType == SIPPYCUP.Options.Type.CONSUMABLE; + -- Removal of a spell/aura count generally is not due to an item's action, mark bag as synchronized. -- Pre-expiration also does not do any bag changes, so mark as synchronised in case. -- Delayed (e.g. eating x seconds) UNIT_AURA calls, mark bag as synchronized (as it was removed earlier). -- Toys UNIT_AURA calls, mark bag as synchronized (as no items are actually used). -- Reflecting Prism (spellID == 163267) uses charges, and does not require a bag sync update generally. + -- Projecting Prism (spellID == 374959) is special, and does not require a bag sync update generally. if reason == SIPPYCUP.Popups.Reason.REMOVAL or reason == SIPPYCUP.Popups.Reason.PRE_EXPIRATION - or optionData.delayedAura or optionData.type == SIPPYCUP.Options.Type.TOY or auraID == 163267 or auraID == 374959 then + or optionData.delayedAura or isToy + or auraID == 163267 or auraID == 374959 + then SIPPYCUP.Items.HandleBagUpdate(); end @@ -865,10 +872,10 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) local trackBySpell = false; local trackByItem = false; - if optionData.type == SIPPYCUP.Options.Type.CONSUMABLE then + if isConsumable then trackBySpell = optionData.spellTrackable; trackByItem = optionData.itemTrackable; - elseif optionData.type == SIPPYCUP.Options.Type.TOY then + elseif isToy then if optionData.itemTrackable then trackByItem = true; end @@ -881,14 +888,18 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) end end + local itemIDs = type(optionData.itemID) == "table" and optionData.itemID or { optionData.itemID }; + local usableItemID; + local itemCount = 0; + local now = GetTime(); + -- Extra check because toys have longer cooldowns than option tend to, so don't fire if cd is still up. - if optionData.type == SIPPYCUP.Options.Type.TOY and reason == SIPPYCUP.Popups.Reason.REMOVAL then + if isToy and reason == SIPPYCUP.Popups.Reason.REMOVAL then local cooldownActive = false; if trackByItem then - local itemIDs = type(optionData.itemID) == "table" and optionData.itemID or { optionData.itemID }; for _, id in ipairs(itemIDs) do local startTime, duration = C_Container.GetItemCooldown(id); - if startTime and duration and duration > 0 and (startTime + duration - GetTime() > 0) then + if startTime and duration and duration > 0 and (startTime + duration - now > 0) then cooldownActive = true; break; end @@ -910,21 +921,23 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) SIPPYCUP_OUTPUT.Debug({ caller = caller, auraID = optionData.auraID, itemID = optionData.itemID, name = optionData.name }); - -- Original preference - local itemIDs = type(optionData.itemID) == "table" and optionData.itemID or { optionData.itemID }; - local usableItemID; - local itemCount = 0; - -- Track which itemIDs are depleted local depleted = {}; for _, id in ipairs(itemIDs) do - local count = C_Item.GetItemCount(id); + local count; + + if isToy then + count = PlayerHasToy(id) and 1 or 0; + else + count = C_Item.GetItemCount(id); + end + SIPPYCUP_OUTPUT.Debug("Checking itemID:", id, "count:", count); if count > 0 then local startTime, duration = C_Container.GetItemCooldown(id); - if not startTime or startTime + duration <= GetTime() then + if not startTime or startTime + duration <= now then usableItemID = id; itemCount = count; SIPPYCUP_OUTPUT.Debug("Usable found:", id, "x"..count); @@ -937,8 +950,9 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) -- Remove depleted items from optionData.itemID, but always keep at least the last one local newItemIDs = {}; + local lastIndex = #itemIDs; for i, id in ipairs(itemIDs) do - if not depleted[id] or i == #itemIDs then + if not depleted[id] or i == lastIndex then newItemIDs[#newItemIDs + 1] = id; end end @@ -961,14 +975,30 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) ]] end + local auraInstanceID = auraInfo and auraInfo.auraInstanceID; + -- First, let's grab the latest currentInstanceID (or have it be nil if none which is fine). + profileOptionData.currentInstanceID = (auraInfo and auraInfo.auraInstanceID) or auraInstanceID; + + if isConsumable then + profileOptionData.currentStacks = SIPPYCUP.Auras.CalculateCurrentStacks(auraInfo, auraID, reason, active); + elseif isToy then + if auraInfo then + profileOptionData.currentStacks = SIPPYCUP.Auras.CalculateCurrentStacks(auraInfo, auraID, reason, active); + else + profileOptionData.currentStacks = active and 1 or 0; + end + end + + local requiredStacks = profileOptionData.desiredStacks - profileOptionData.currentStacks; + SIPPYCUP.Popups.HandleReminderPopup({ - active = auraInfo and true or active, + active = active, auraID = auraID, auraInfo = auraInfo, optionData = optionData, profileOptionData = profileOptionData, reason = reason, - requiredStacks = profileOptionData.desiredStacks - profileOptionData.currentStacks, + requiredStacks = requiredStacks, itemCount = itemCount, }); end From 7e905dadbb141a42bef22f0bf4e278cc3ee1c5f4 Mon Sep 17 00:00:00 2001 From: Raenore <172234435+Raenore@users.noreply.github.com> Date: Mon, 16 Feb 2026 03:20:54 +0100 Subject: [PATCH 06/13] Improve party check on refresh button --- .luacheckrc | 1 + Modules/Popups/Popups.lua | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 35065d3..9cd16e8 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -503,6 +503,7 @@ stds.wow = { "GetMouseFocus", "GetNormalizedRealmName", "GetNumLanguages", + "GetNumSubgroupMembers", "GetPlayerInfoByGUID", "GetRealmName", "GetSpellBaseCooldown", diff --git a/Modules/Popups/Popups.lua b/Modules/Popups/Popups.lua index b3245a0..a37e913 100644 --- a/Modules/Popups/Popups.lua +++ b/Modules/Popups/Popups.lua @@ -210,10 +210,10 @@ local function CreatePopup(templateType) end if optionData.requiresGroup then - if popupData.isNotinGroupButRequired and UnitInParty("player") then + if popupData.isNotinGroupButRequired and UnitInParty("player") and GetNumSubgroupMembers() > 0 then popupData.isNotinGroupButRequired = false; self:Enable(); - elseif not UnitInParty("player") then + elseif not UnitInParty("player") or GetNumSubgroupMembers() == 0 then popupData.isNotinGroupButRequired = true; self:Disable() end @@ -268,7 +268,7 @@ local function CreatePopup(templateType) end local optionData = popupData.optionData; - if optionData.requiresGroup and popupData.isNotinGroupButRequired and UnitInParty("player") then + if optionData.requiresGroup and popupData.isNotinGroupButRequired and UnitInParty("player") and GetNumSubgroupMembers() > 0 then self:Enable() end GameTooltip:Hide(); From fcea4a39da944e474ec46bd39119da449a3cce40 Mon Sep 17 00:00:00 2001 From: Raenore <172234435+Raenore@users.noreply.github.com> Date: Mon, 16 Feb 2026 03:38:45 +0100 Subject: [PATCH 07/13] Reset Refresh button on interrupts --- Core/Database.lua | 9 +++++++++ Core/Options.lua | 7 +++++-- SippyCup.lua | 30 ++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/Core/Database.lua b/Core/Database.lua index 62333eb..5f0354f 100644 --- a/Core/Database.lua +++ b/Core/Database.lua @@ -128,6 +128,7 @@ local defaults = SIPPYCUP.Database.defaults; ---@field currentInstanceID number? Aura instance ID currently being tracked (if any). ---@field currentStacks number Current number of detected stacks. ---@field aura number The associated aura ID for this option. +---@field castAura number The associated cast aura ID, if none is set then use aura ID. ---@field untrackableByAura boolean Whether this option can be tracked via its aura or not. ---Populate the default option's table keyed by aura ID, with all known entries from SIPPYCUP.Options.Data. @@ -138,6 +139,7 @@ local function PopulateDefaultOptions() for i = 1, #optionsData do local option = optionsData[i]; local spellID = option.auraID; + local castSpellID = option.castAuraID; local untrackableByAura = option.itemTrackable or option.spellTrackable; if spellID then @@ -148,6 +150,7 @@ local function PopulateDefaultOptions() currentInstanceID = nil, currentStacks = 0, aura = spellID, + castAura = castSpellID, untrackableByAura = untrackableByAura, }; end @@ -162,6 +165,9 @@ SIPPYCUP.Database.auraToProfile = {}; -- auraID --> profile data SIPPYCUP.Database.instanceToProfile = {}; -- instanceID --> profile data ---@type table SIPPYCUP.Database.untrackableByAuraProfile = {}; -- itemID --> profile data (only if no aura) +---@type table +SIPPYCUP.Database.castAuraToProfile = {}; -- castAuraID (if different) / auraID --> profile data + ---RebuildAuraMap rebuilds internal lookup tables for aura and instance-based option tracking. ---@return nil @@ -171,11 +177,14 @@ function SIPPYCUP.Database.RebuildAuraMap() wipe(db.auraToProfile); wipe(db.instanceToProfile); wipe(db.untrackableByAuraProfile); + wipe(db.castAuraToProfile); for _, profileOptionData in pairs(SIPPYCUP.Profile) do if profileOptionData.enable and profileOptionData.aura then local auraID = profileOptionData.aura; db.auraToProfile[auraID] = profileOptionData; + local castAuraID = profileOptionData.castAura; + db.castAuraToProfile[castAuraID] = profileOptionData; -- Update instance ID if aura is currently active local auraInfo; diff --git a/Core/Options.lua b/Core/Options.lua index 5e89574..1d819af 100644 --- a/Core/Options.lua +++ b/Core/Options.lua @@ -17,6 +17,7 @@ SIPPYCUP.Options.Type = { ---@class SIPPYCUPOption: table ---@field type string Whether this option is a consumable (0) or toy (1). ---@field auraID number The option's aura ID. +---@field castAuraID number The option's cast aura ID, if none is set then use auraID. ---@field itemID number|table The option's item ID(s) ---@field loc string The option's localization key (auto-gen). ---@field category string The option's category (e.g., potion, food). @@ -30,7 +31,7 @@ SIPPYCUP.Options.Type = { ---@field spellTrackable boolean Whether the option can only be tracked through the spell itself (cooldowns, etc.). ---@field delayedAura boolean Whether the option is applied after a delay (e.g. food buff), on false a buff is applied instantly. ---@field cooldownMismatch boolean Whether the option has a mismatch in cooldowns (cd longer than buff lasts), on false there is no mismatch. - +---@field charges boolean Whether the option uses charges. ---NewOption creates a new object with the specified parameters. ---@param params SIPPYCUPOption A table containing parameters for the new option. @@ -44,6 +45,7 @@ local function NewOption(params) return { type = params.type or SIPPYCUP.Options.Type.CONSUMABLE, auraID = params.auraID, + castAuraID = params.castAuraID or params.auraID, itemID = itemIDs; -- always store as a table internally loc = params.loc, category = params.category, @@ -59,6 +61,7 @@ local function NewOption(params) cooldownMismatch = params.cooldownMismatch or false, buildAdded = params.buildAdded or nil, requiresGroup = params.requiresGroup or false, + charges = params.charges or false, }; end @@ -98,7 +101,7 @@ SIPPYCUP.Options.Data = { NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 393979, itemID = 201428, category = "EFFECT" }, -- QUICKSILVER_SANDS NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1213974, itemID = 234287, category = "EFFECT", preExpiration = true }, -- RADIANT_FOCUS NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1214287, itemID = 234527, category = "HANDHELD", preExpiration = true }, -- SACREDITES_LEDGER - NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 163267, itemID = 112384, category = "PRISM", preExpiration = true, requiresGroup = true }, -- REFLECTING_PRISM + NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 163267, castAuraID = 163219, itemID = 112384, category = "PRISM", preExpiration = true, requiresGroup = true, charges = true }, -- REFLECTING_PRISM NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 279742, itemID = 163695, category = "EFFECT" }, -- SCROLL_OF_INNER_TRUTH NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1222834, itemID = 237332, category = "PLACEMENT", spellTrackable = true }, -- SINGLE_USE_GRILL NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 58479, itemID = 43480, category = "SIZE", preExpiration = true, delayedAura = true }, -- SMALL_FEAST diff --git a/SippyCup.lua b/SippyCup.lua index d801070..abb9b45 100644 --- a/SippyCup.lua +++ b/SippyCup.lua @@ -92,6 +92,8 @@ function SIPPYCUP_Addon:OnPlayerLogin() -- Register game events related to aura and spell tracking for Sippy Cup. SIPPYCUP_Addon:RegisterUnitEvent("UNIT_AURA", "player"); SIPPYCUP_Addon:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player"); + SIPPYCUP_Addon:RegisterUnitEvent("UNIT_SPELLCAST_RETICLE_CLEAR", "player"); + SIPPYCUP_Addon:RegisterUnitEvent("UNIT_SPELLCAST_INTERRUPTED", "player"); SIPPYCUP.Config.TryCreateConfigFrame(); end @@ -193,6 +195,34 @@ function SIPPYCUP_Addon:UNIT_SPELLCAST_SUCCEEDED(_, unitTarget, _, spellID) -- l SIPPYCUP.Items.CheckNoAuraSingleOption(nil, spellID); end +function SIPPYCUP_Addon:UNIT_SPELLCAST_RETICLE_CLEAR(_, unitTarget, _, spellID) -- luacheck: no unused (unitTarget) + if InCombatLockdown() or not canaccessvalue(spellID) then + return; + end + + if SIPPYCUP.Database.castAuraToProfile[spellID] then + SIPPYCUP.Popups.ForEachActivePopup(function(popup) + if popup.templateType == "SIPPYCUP_RefreshPopupTemplate" and popup.RefreshButton and not popup.RefreshButton:IsEnabled() then + popup.RefreshButton:Enable(); + end + end); + end +end + +function SIPPYCUP_Addon:UNIT_SPELLCAST_INTERRUPTED(_, unitTarget, _, spellID) -- luacheck: no unused (unitTarget) + if InCombatLockdown() or not canaccessvalue(spellID) then + return; + end + + if SIPPYCUP.Database.castAuraToProfile[spellID] then + SIPPYCUP.Popups.ForEachActivePopup(function(popup) + if popup.templateType == "SIPPYCUP_RefreshPopupTemplate" and popup.RefreshButton and not popup.RefreshButton:IsEnabled() then + popup.RefreshButton:Enable(); + end + end); + end +end + ---ZONE_CHANGED_NEW_AREA Handles zone changes and triggers loading screen end logic if needed. function SIPPYCUP_Addon:ZONE_CHANGED_NEW_AREA() SIPPYCUP.Callbacks:TriggerEvent(SIPPYCUP.Events.LOADING_SCREEN_ENDED); From f616a4d59ed16d9fdcc18ce8044d47bf01091b56 Mon Sep 17 00:00:00 2001 From: Raenore <172234435+Raenore@users.noreply.github.com> Date: Mon, 16 Feb 2026 03:51:48 +0100 Subject: [PATCH 08/13] Rewrite system that tracks dirty bag state + cleanup --- Core/Auras.lua | 79 +++++++--- Core/Bags.lua | 24 +++ Core/Core.xml | 1 + Core/Database.lua | 12 +- Core/Items.lua | 11 -- Core/Options.lua | 2 + Core/Utils.lua | 2 +- Modules/LinkDialog/LinkDialog.lua | 2 +- Modules/Popups/Popups.lua | 248 ++++++++++++++++-------------- Modules/TRP/TRP.lua | 2 +- SippyCup.lua | 7 +- UI/Config.lua | 18 +-- 12 files changed, 244 insertions(+), 164 deletions(-) create mode 100644 Core/Bags.lua diff --git a/Core/Auras.lua b/Core/Auras.lua index 5952ed0..3e2788c 100644 --- a/Core/Auras.lua +++ b/Core/Auras.lua @@ -25,6 +25,27 @@ function SIPPYCUP.Auras.DebugEnabledAuras() end end +---SkipDuplicatePrismUnitAura determines if a prism-type aura update should be ignored +---@param profileOptionData table The option profile data. +---@return boolean skip True if this aura update should be skipped due to duplicate UNIT_AURA events. +local function SkipDuplicatePrismUnitAura(profileOptionData) + local skip = false; + + -- Prism duplicate handling + if profileOptionData.isPrism then + if profileOptionData.instantUpdate then + -- Second UNIT_AURA in pair: skip + skip = true; + profileOptionData.instantUpdate = false; -- reset for next usage + else + -- First UNIT_AURA: allow and mark + profileOptionData.instantUpdate = true; + end + end + + return skip; +end + ---QueueAuraAction enqueues a popup action for aura changes. ---@param profileOptionData table The option profile data. ---@param auraInfo table? The aura information, or nil if removed. @@ -32,14 +53,26 @@ end ---@param source string The source description of the action. ---@return nil local function QueueAuraAction(profileOptionData, auraInfo, reason, source) + local optionData = SIPPYCUP.Options.ByAuraID[profileOptionData.aura]; + -- Consumables that actually consume bag space/items need a bag check + local needsBagCheck = (profileOptionData.type == 0 and not profileOptionData.usesCharges); + local shouldIncrement = needsBagCheck and reason == SIPPYCUP.Popups.Reason.ADDITION; + local data = { active = auraInfo ~= nil, auraID = profileOptionData.aura, auraInfo = auraInfo, - optionData = SIPPYCUP.Options.ByAuraID[profileOptionData.aura], + optionData = optionData, profileOptionData = profileOptionData, reason = reason, - } + needsBagCheck = needsBagCheck, + auraGeneration = shouldIncrement and (SIPPYCUP.Bags.auraGeneration + 1) or 0, + }; + + if shouldIncrement then + SIPPYCUP.Bags.auraGeneration = SIPPYCUP.Bags.auraGeneration + 1; + end + SIPPYCUP.Popups.QueuePopupAction(data, source); end @@ -54,7 +87,6 @@ end ---@return nil local function ParseAura(updateInfo) local GetAuraDataByAuraInstanceID = C_UnitAuras.GetAuraDataByAuraInstanceID; - local auraAdded = false; -- isFullUpdate true means nil values, invalidate the state. if updateInfo.isFullUpdate then @@ -72,13 +104,16 @@ local function ParseAura(updateInfo) for _, auraInfo in ipairs(updateInfo.addedAuras) do local profileOptionData = SIPPYCUP.Database.FindMatchingProfile(auraInfo.spellId); if profileOptionData and profileOptionData.enable then - auraAdded = true; - profileOptionData.currentInstanceID = auraInfo.auraInstanceID; - SIPPYCUP.Database.instanceToProfile[auraInfo.auraInstanceID] = profileOptionData; + local skip = SkipDuplicatePrismUnitAura(profileOptionData); - SIPPYCUP.Auras.CheckPreExpirationForSingleOption(profileOptionData); + if not skip then + profileOptionData.currentInstanceID = auraInfo.auraInstanceID; + SIPPYCUP.Database.instanceToProfile[auraInfo.auraInstanceID] = profileOptionData; + + SIPPYCUP.Auras.CheckPreExpirationForSingleOption(profileOptionData); - QueueAuraAction(profileOptionData, auraInfo, SIPPYCUP.Popups.Reason.ADDITION, "ParseAura - addition"); + QueueAuraAction(profileOptionData, auraInfo, SIPPYCUP.Popups.Reason.ADDITION, "ParseAura - addition"); + end end end end @@ -91,16 +126,17 @@ local function ParseAura(updateInfo) if profileOptionData and profileOptionData.enable then local auraInfo = GetAuraDataByAuraInstanceID("player", auraInstanceID); if auraInfo then - auraAdded = true; - -- This is not necessary, but a safety update just in case. - profileOptionData.currentInstanceID = auraInfo.auraInstanceID; - SIPPYCUP.Database.instanceToProfile[auraInfo.auraInstanceID] = profileOptionData; + local skip = SkipDuplicatePrismUnitAura(profileOptionData); - -- On aura update, we remove all pre-expiration timers as that's obvious no longer relevant. - SIPPYCUP.Auras.CancelPreExpirationTimer(nil, profileOptionData.aura, auraInstanceID); - SIPPYCUP.Auras.CheckPreExpirationForSingleOption(profileOptionData); + if not skip then + profileOptionData.currentInstanceID = auraInfo.auraInstanceID; + SIPPYCUP.Database.instanceToProfile[auraInfo.auraInstanceID] = profileOptionData; + + SIPPYCUP.Auras.CancelPreExpirationTimer(nil, profileOptionData.aura, auraInstanceID); + SIPPYCUP.Auras.CheckPreExpirationForSingleOption(profileOptionData); - QueueAuraAction(profileOptionData, auraInfo, SIPPYCUP.Popups.Reason.ADDITION, "ParseAura - updated"); + QueueAuraAction(profileOptionData, auraInfo, SIPPYCUP.Popups.Reason.ADDITION, "ParseAura - updated"); + end end end end @@ -122,11 +158,6 @@ local function ParseAura(updateInfo) end end end - - -- If no auras were added, there was no bag update so we mark bag as synchronized. - if not auraAdded then - SIPPYCUP.Items.HandleBagUpdate(); - end end SIPPYCUP.Auras.auraQueue = {}; @@ -148,7 +179,7 @@ local function flushAuraQueue() -- Deduplication arrays local seenAdd, seenUpdate, seenRemoval = {}, {}, {}; - local isFullUpdate = false + local isFullUpdate = false; -- 1) harvest everything into seenAdd / seenUpdate / seenRemoval for _, updateInfo in ipairs(queue) do @@ -294,7 +325,7 @@ function SIPPYCUP.Auras.Convert(source, data) end -- buffer it instead of parsing immediately - SIPPYCUP.Auras.auraQueue[#SIPPYCUP.Auras.auraQueue + 1] = updateInfo + SIPPYCUP.Auras.auraQueue[#SIPPYCUP.Auras.auraQueue + 1] = updateInfo; -- flush on the next frame (which will run the batched UNIT_AURAs) if not SIPPYCUP.Auras.auraQueueScheduled then @@ -319,7 +350,7 @@ function SIPPYCUP.Auras.CheckAllActiveOptions() -- auraToProfile holds only enabled options (whether they are active or not). for _, profileOptionData in pairs(auraToProfile) do - local currentInstanceID = profileOptionData.currentInstanceID + local currentInstanceID = profileOptionData.currentInstanceID; -- True if it's active (or was), false if it's not been active. local canBeActive = (currentInstanceID or 0) ~= 0; local auraInfo = GetPlayerAuraBySpellID(profileOptionData.aura); diff --git a/Core/Bags.lua b/Core/Bags.lua new file mode 100644 index 0000000..b99217d --- /dev/null +++ b/Core/Bags.lua @@ -0,0 +1,24 @@ +-- Copyright The Sippy Cup Authors +-- SPDX-License-Identifier: Apache-2.0 + +SIPPYCUP.Bags = {}; + +SIPPYCUP.Bags.auraGeneration = 0; +SIPPYCUP.Bags.bagGeneration = 0; + +function SIPPYCUP.Bags.BagUpdateDelayed() + SIPPYCUP_OUTPUT.Debug("BAG_UPDATE_DELAYED"); + + SIPPYCUP.Bags.lastBagUpdate = GetTime(); + SIPPYCUP.Bags.bagGeneration = SIPPYCUP.Bags.bagGeneration + 1; + + SIPPYCUP_OUTPUT.Debug("Bag generation:", SIPPYCUP.Bags.bagGeneration); + SIPPYCUP.Bags.ClearBagQueue(); +end + +---HandleBagUpdate marks bag data as synchronized and processes deferred popups. +-- Fires on BAG_UPDATE_DELAYED, which batches all bag changes after UNIT_AURA. +function SIPPYCUP.Bags.ClearBagQueue() + -- Flush deferred popups that were blocked by bag desync + SIPPYCUP.Popups.HandleDeferredActions("bag", SIPPYCUP.Items.bagSyncedGeneration); +end diff --git a/Core/Core.xml b/Core/Core.xml index a546b17..8548abd 100644 --- a/Core/Core.xml +++ b/Core/Core.xml @@ -7,6 +7,7 @@ + diff --git a/Core/Database.lua b/Core/Database.lua index 5f0354f..d6567e6 100644 --- a/Core/Database.lua +++ b/Core/Database.lua @@ -117,7 +117,7 @@ SIPPYCUP.Database.defaults = { profiles = { Default = {}, -- will hold all options per profile }, -} +}; local defaults = SIPPYCUP.Database.defaults; @@ -130,6 +130,8 @@ local defaults = SIPPYCUP.Database.defaults; ---@field aura number The associated aura ID for this option. ---@field castAura number The associated cast aura ID, if none is set then use aura ID. ---@field untrackableByAura boolean Whether this option can be tracked via its aura or not. +---@field type string Whether this option is a consumable (0) or toy (1). +---@field instantUpdate boolean Whether the instant UNIT_AURA update has already happened right after the addition (prisms). ---Populate the default option's table keyed by aura ID, with all known entries from SIPPYCUP.Options.Data. ---This defines initial tracking settings for each option by its aura ID key. @@ -141,6 +143,10 @@ local function PopulateDefaultOptions() local spellID = option.auraID; local castSpellID = option.castAuraID; local untrackableByAura = option.itemTrackable or option.spellTrackable; + local type = option.type; + local isPrism = (option.category == "PRISM") or false; + local instantUpdate = not isPrism; + local usesCharges = option.charges; if spellID then -- Use auraID as the key, not profileKey @@ -152,6 +158,10 @@ local function PopulateDefaultOptions() aura = spellID, castAura = castSpellID, untrackableByAura = untrackableByAura, + type = type, + isPrism = isPrism, + instantUpdate = instantUpdate, + usesCharges = usesCharges, }; end end diff --git a/Core/Items.lua b/Core/Items.lua index 9d5e7e5..a51134a 100644 --- a/Core/Items.lua +++ b/Core/Items.lua @@ -279,14 +279,3 @@ function SIPPYCUP.Items.CheckNoAuraSingleOption(profileOptionData, spellID, minS return preExpireFired; end - -SIPPYCUP.Items.bagUpdateUnhandled = false; - ----HandleBagUpdate Marks bag data as synced and processes deferred popups. --- Fires on BAG_UPDATE_DELAYED, which batches all bag changes after UNIT_AURA. -function SIPPYCUP.Items.HandleBagUpdate() - SIPPYCUP.Items.bagUpdateUnhandled = false; - - -- Now that bag data is synced, process deferred actions using accurate data. - SIPPYCUP.Popups.HandleDeferredActions("bag"); -end diff --git a/Core/Options.lua b/Core/Options.lua index 1d819af..2942b78 100644 --- a/Core/Options.lua +++ b/Core/Options.lua @@ -31,6 +31,8 @@ SIPPYCUP.Options.Type = { ---@field spellTrackable boolean Whether the option can only be tracked through the spell itself (cooldowns, etc.). ---@field delayedAura boolean Whether the option is applied after a delay (e.g. food buff), on false a buff is applied instantly. ---@field cooldownMismatch boolean Whether the option has a mismatch in cooldowns (cd longer than buff lasts), on false there is no mismatch. +---@field buildAdded string The Sippy Cup and WoW build this option was added (for the new feature indicator). +---@field requiresGroup boolean Whether the option requires a group to be sued (e.g. prisms). ---@field charges boolean Whether the option uses charges. ---NewOption creates a new object with the specified parameters. diff --git a/Core/Utils.lua b/Core/Utils.lua index ef5a1ab..6af38cf 100644 --- a/Core/Utils.lua +++ b/Core/Utils.lua @@ -135,7 +135,7 @@ local function formatValue(val, isTop) end return "{" .. table.concat(items, ",") .. "}"; else - local items = {} + local items = {}; for k, v in pairs(val) do table.insert(items, tostring(k) .. ": " .. formatValue(v)); end diff --git a/Modules/LinkDialog/LinkDialog.lua b/Modules/LinkDialog/LinkDialog.lua index ae4e101..a12f7cf 100644 --- a/Modules/LinkDialog/LinkDialog.lua +++ b/Modules/LinkDialog/LinkDialog.lua @@ -54,7 +54,7 @@ StaticPopupDialogs["SIPPYCUP_LINK_DIALOG"] = { whileDead = true, hideOnEscape = true, preferredIndex = 3, -} +}; ---CreateExternalLinkDialog displays a dialog with an external link. ---@param url string The URL to be displayed in the dialog. diff --git a/Modules/Popups/Popups.lua b/Modules/Popups/Popups.lua index a37e913..37a493c 100644 --- a/Modules/Popups/Popups.lua +++ b/Modules/Popups/Popups.lua @@ -233,7 +233,7 @@ local function CreatePopup(templateType) if optionData.type == SIPPYCUP.Options.Type.CONSUMABLE then local profileOptionData = popupData.profileOptionData; - local itemCount = 0 + local itemCount = 0; if type(optionData.itemID) == "table" then for _, id in ipairs(optionData.itemID) do itemCount = itemCount + C_Item.GetItemCount(id) @@ -281,7 +281,7 @@ local function CreatePopup(templateType) local optionData = popupData.optionData; -- Sum counts if multiple item IDs - local itemCount = 0 + local itemCount = 0; if type(optionData.itemID) == "table" then for _, id in ipairs(optionData.itemID) do itemCount = itemCount + C_Item.GetItemCount(id); @@ -735,7 +735,7 @@ local pendingCalls = {}; ---@param caller string What function called the popup action. ---@return nil function SIPPYCUP.Popups.QueuePopupAction(data, caller) - SIPPYCUP_OUTPUT.Debug("QueuePopupAction"); + SIPPYCUP_OUTPUT.Debug("QueuePopupAction -", caller); -- If MSP status checks are on and the character is currently OOC, we skip everything. if SIPPYCUP.MSP.IsEnabled() and SIPPYCUP.global.MSPStatusCheck then local _, _, isIC = SIPPYCUP.MSP.CheckRPStatus(); @@ -751,11 +751,11 @@ function SIPPYCUP.Popups.QueuePopupAction(data, caller) if pendingCalls[key] then -- Update the existing entry with the latest arguments for this auraID+reason pendingCalls[key].args = args; - return + return; end -- first call for this auraID+reason: store args and schedule - pendingCalls[key] = { args = args } + pendingCalls[key] = { args = args }; C_Timer.After(DEBOUNCE_DELAY, function() local entry = pendingCalls[key]; pendingCalls[key] = nil; @@ -774,7 +774,8 @@ end ---@param caller string What function called the popup action. ---@return nil function SIPPYCUP.Popups.HandlePopupAction(data, caller) - SIPPYCUP_OUTPUT.Debug("HandlePopupAction"); + SIPPYCUP_OUTPUT.Debug("HandlePopupAction -", caller); + local optionData = data.optionData or SIPPYCUP.Options.ByAuraID[data.auraID]; local profileOptionData = data.profileOptionData or SIPPYCUP.Profile[data.auraID]; @@ -782,6 +783,72 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) return; end + local optionType = optionData.type; + local isToy = optionType == SIPPYCUP.Options.Type.TOY; + local isConsumable = optionType == SIPPYCUP.Options.Type.CONSUMABLE; + + SIPPYCUP_OUTPUT.Debug("SIPPYCUP.Bags.updateUnhandled:", SIPPYCUP.Bags.updateUnhandled); + SIPPYCUP_OUTPUT.Debug("name:", optionData.name); + if isToy then + SIPPYCUP_OUTPUT.Debug("Toy"); + else + SIPPYCUP_OUTPUT.Debug("Consumable"); + end + + -- Check for a dirty bag state, if so then defer until it is no longer. + if isConsumable and data.needsBagCheck then + if SIPPYCUP.Bags.bagGeneration < data.auraGeneration then + SIPPYCUP_OUTPUT.Debug("We reached item count in HandlePopupAction but bag state is dirty"); + + deferredActions[#deferredActions + 1] = { + data = data, + caller = caller, + blockedBy = { + bag = true, + }, + }; + return; + end + SIPPYCUP_OUTPUT.Debug("Bag state is not dirty, we can continue this time!"); + end + + local itemIDs = type(optionData.itemID) == "table" and optionData.itemID or { optionData.itemID }; + local usableItemID; + local itemCount = 0; + local now = GetTime(); + + -- Track which itemIDs are depleted + local depleted = {}; + + for _, id in ipairs(itemIDs) do + local count = isToy and (PlayerHasToy(id) and 1 or 0) or C_Item.GetItemCount(id); + if count > 0 then + local startTime, duration = C_Container.GetItemCooldown(id); + if not startTime or startTime + duration <= now then + usableItemID = usableItemID or id; -- pick the first usable + itemCount = itemCount + count; -- sum counts across all items + end + else + depleted[id] = true; + end + end + + -- Only update the *current usable item* + if usableItemID then + profileOptionData.currentItemID = usableItemID; + else + -- No usable item available, popup can report zero count + profileOptionData.currentItemID = itemIDs[#itemIDs]; + SIPPYCUP_OUTPUT.Debug("No usable item, reporting last itemID:", profileOptionData.currentItemID); + end + + -- Always update last known count + profileOptionData.lastItemCount = itemCount; + + -- Unnecessary for now, keep it here in case it might become so. + -- local currentItemID = profileOptionData.currentItemID or optionData.itemID; + -- local lastCount = profileOptionData.lastItemCount or itemCount; + local reason = data.reason; local active = data.active; @@ -789,54 +856,31 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) local auraInfo = data.auraInfo; local currentInstanceID = profileOptionData.currentInstanceID; - local optionType = optionData.type; - local isToy = optionType == SIPPYCUP.Options.Type.TOY; - local isConsumable = optionType == SIPPYCUP.Options.Type.CONSUMABLE; - - -- Removal of a spell/aura count generally is not due to an item's action, mark bag as synchronized. - -- Pre-expiration also does not do any bag changes, so mark as synchronised in case. - -- Delayed (e.g. eating x seconds) UNIT_AURA calls, mark bag as synchronized (as it was removed earlier). - -- Toys UNIT_AURA calls, mark bag as synchronized (as no items are actually used). - -- Reflecting Prism (spellID == 163267) uses charges, and does not require a bag sync update generally. - -- Projecting Prism (spellID == 374959) is special, and does not require a bag sync update generally. - if reason == SIPPYCUP.Popups.Reason.REMOVAL or reason == SIPPYCUP.Popups.Reason.PRE_EXPIRATION - or optionData.delayedAura or isToy - or auraID == 163267 or auraID == 374959 - then - SIPPYCUP.Items.HandleBagUpdate(); - end - -- We defer popups in three situations: - -- > Bag data is desynch'd from UNIT_AURA. - -- > We're in combat (experimental). + -- > Bag data is desynch'd from UNIT_AURA. (Already done). + -- > We're in combat. -- > We're in a loading screen. -- This should be handled before any other logic, as there's no point to calculate deferred logic. - if SIPPYCUP.Items.bagUpdateUnhandled or InCombatLockdown() or SIPPYCUP.InLoadingScreen then - local blockedBy; - if SIPPYCUP.Items.bagUpdateUnhandled then - blockedBy = "bag"; - elseif InCombatLockdown() then - blockedBy = "combat"; -- Won't ever happen anymore as UNIT_AURA does not run in combat, legacy code. - elseif SIPPYCUP.States.loadingScreen then - blockedBy = "loading"; - end - - local deferredData = { - active = active, - auraID = auraID, - auraInfo = auraInfo, - optionData = optionData, - profileOptionData = profileOptionData, - reason = reason, + if InCombatLockdown() then + deferredActions[#deferredActions + 1] = { + data = data, + caller = caller, + blockedBy = { + combat = true, + }, }; - + return; + elseif SIPPYCUP.InLoadingScreen then deferredActions[#deferredActions + 1] = { - data = deferredData, + data = data, caller = caller, - blockedBy = blockedBy, + blockedBy = { + loading = true, + }, }; return; end + -- At this point, we're certain that we're safe to execute further! -- Recover auraInfo if possible (perhaps triggered through combat or other means) @@ -888,11 +932,6 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) end end - local itemIDs = type(optionData.itemID) == "table" and optionData.itemID or { optionData.itemID }; - local usableItemID; - local itemCount = 0; - local now = GetTime(); - -- Extra check because toys have longer cooldowns than option tend to, so don't fire if cd is still up. if isToy and reason == SIPPYCUP.Popups.Reason.REMOVAL then local cooldownActive = false; @@ -921,33 +960,6 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) SIPPYCUP_OUTPUT.Debug({ caller = caller, auraID = optionData.auraID, itemID = optionData.itemID, name = optionData.name }); - -- Track which itemIDs are depleted - local depleted = {}; - - for _, id in ipairs(itemIDs) do - local count; - - if isToy then - count = PlayerHasToy(id) and 1 or 0; - else - count = C_Item.GetItemCount(id); - end - - SIPPYCUP_OUTPUT.Debug("Checking itemID:", id, "count:", count); - - if count > 0 then - local startTime, duration = C_Container.GetItemCooldown(id); - if not startTime or startTime + duration <= now then - usableItemID = id; - itemCount = count; - SIPPYCUP_OUTPUT.Debug("Usable found:", id, "x"..count); - end - else - SIPPYCUP_OUTPUT.Debug("No items for:", id); - depleted[id] = true; - end - end - -- Remove depleted items from optionData.itemID, but always keep at least the last one local newItemIDs = {}; local lastIndex = #itemIDs; @@ -958,23 +970,6 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) end optionData.itemID = newItemIDs; - -- Only update the *current usable item* - if usableItemID then - profileOptionData.currentItemID = usableItemID; - else - -- No usable item available, popup can report zero count - profileOptionData.currentItemID = itemIDs[#itemIDs]; - SIPPYCUP_OUTPUT.Debug("No usable item, reporting last itemID:", profileOptionData.currentItemID); - --[[ TO-DO: Consider if we require this here - RemoveDeferredActionsByLoc(optionData.loc); - local existingPopup = SIPPYCUP.Popups.activeByLoc[optionData.loc]; - if existingPopup and existingPopup:IsShown() then - existingPopup:Hide(); - end - return; - ]] - end - local auraInstanceID = auraInfo and auraInfo.auraInstanceID; -- First, let's grab the latest currentInstanceID (or have it be nil if none which is fine). profileOptionData.currentInstanceID = (auraInfo and auraInfo.auraInstanceID) or auraInstanceID; @@ -1031,18 +1026,16 @@ end ---DeferAllRefreshPopups defers all active and queued popups to be processed later. -- Typically called when popups cannot be shown due to bags, combat, or loading screen. ----@param reason number Why a deferred action is being handled (0 - bag, 1 - combat, 2 - loading) +---@param reasonKey "bag"|"combat"|"loading" The gate reason being deferred. ---@return nil function SIPPYCUP.Popups.DeferAllRefreshPopups(reasonKey) - local blockedBy; - if reasonKey == 0 or SIPPYCUP.Items.bagUpdateUnhandled then - blockedBy = "bag"; - elseif reasonKey == 1 or InCombatLockdown() then - blockedBy = "combat"; -- Only way combat is ever used, by deferring before combat. - elseif reasonKey == 2 or SIPPYCUP.States.loadingScreen then - blockedBy = "loading"; + if not reasonKey then + return; end + local blockedBy = {}; + blockedBy[reasonKey] = true; + local function MakeDeferredData(popup) local d = popup.popupData; return { @@ -1079,9 +1072,24 @@ function SIPPYCUP.Popups.DeferAllRefreshPopups(reasonKey) end end ----HandleDeferredActions Processes and flushes all queued popup actions. --- Called after bag data is synced (BAG_UPDATE_DELAYED) to ensure accurate context. ----@param reason number Why a deferred action is being handled (0 - bag, 1 - combat, 2 - loading) +---ForEachActivePopup iterates over all currently shown popups and calls a handler function. +---@param handler fun(popup: Frame) Callback to execute on each active popup. +---@return nil +function SIPPYCUP.Popups.ForEachActivePopup(handler) + if not handler then return; end + + for _, popup in ipairs(activePopups) do + if popup and popup:IsShown() then + handler(popup); + end + end +end + +---HandleDeferredActions Resolves and processes deferred popup actions for a specific gate. +---Removes the given reasonKey blocker from deferred entries and executes those +---that are no longer blocked and whose bag generation requirement is satisfied. +---@param reasonKey "bag"|"combat"|"loading" The gate reason being cleared. +---@return nil function SIPPYCUP.Popups.HandleDeferredActions(reasonKey) if not deferredActions or #deferredActions == 0 then return; @@ -1090,14 +1098,30 @@ function SIPPYCUP.Popups.HandleDeferredActions(reasonKey) local i = 1; while i <= #deferredActions do local action = deferredActions[i]; - if action.blockedBy == reasonKey then - SIPPYCUP.Popups.QueuePopupAction( - action.data, - action.caller - ); - tremove(deferredActions, i); -- remove handled item, don't increment + + -- Remove the specified blocker from this action + if action.blockedBy and action.blockedBy[reasonKey] then + action.blockedBy[reasonKey] = nil; + end + + -- Check if any blockers remain + local stillBlocked = action.blockedBy and next(action.blockedBy) ~= nil; + + if not stillBlocked then + -- Final bag ordering safety check + local gen = action.data.auraGeneration or 0; + + if SIPPYCUP.Bags.bagGeneration >= gen then + SIPPYCUP.Popups.QueuePopupAction( + action.data, + action.caller + ); + tremove(deferredActions, i); + else + i = i + 1; + end else - i = i + 1; -- skip non-matching item + i = i + 1; end end end diff --git a/Modules/TRP/TRP.lua b/Modules/TRP/TRP.lua index 3b566bd..ddc14c5 100644 --- a/Modules/TRP/TRP.lua +++ b/Modules/TRP/TRP.lua @@ -26,7 +26,7 @@ local function onStart() SIPPYCUP_Addon:OpenSettings(8); end end, - } + }; end) end diff --git a/SippyCup.lua b/SippyCup.lua index abb9b45..364afd4 100644 --- a/SippyCup.lua +++ b/SippyCup.lua @@ -136,7 +136,7 @@ end ---BAG_UPDATE_DELAYED Handles delayed bag updates and triggers item update processing. function SIPPYCUP_Addon:BAG_UPDATE_DELAYED() - SIPPYCUP.Items.HandleBagUpdate(); + SIPPYCUP.Bags.BagUpdateDelayed(); end ---PLAYER_ENTERING_WORLD Handles player entering world or UI reload; triggers loading screen end logic if reloading. @@ -160,7 +160,7 @@ end function SIPPYCUP_Addon:PLAYER_REGEN_DISABLED() -- Combat is entered when regen is disabled. self:StopContinuousCheck(); - SIPPYCUP.Popups.DeferAllRefreshPopups(1); + SIPPYCUP.Popups.DeferAllRefreshPopups("combat"); end ---PLAYER_REGEN_ENABLED Restarts continuous checks and handles deferred combat actions after leaving combat. @@ -180,8 +180,7 @@ function SIPPYCUP_Addon:UNIT_AURA(_, unitTarget, updateInfo) -- luacheck: no unu if InCombatLockdown() or C_Secrets and C_Secrets.ShouldAurasBeSecret() then return; end - -- Bag data is not synched immediately when UNIT_AURA fires, signal desync to the addon. - SIPPYCUP.Items.bagUpdateUnhandled = true; + SIPPYCUP.Auras.Convert(SIPPYCUP.Auras.Sources.UNIT_AURA, updateInfo); end diff --git a/UI/Config.lua b/UI/Config.lua index 7ab754f..68bd15f 100644 --- a/UI/Config.lua +++ b/UI/Config.lua @@ -18,7 +18,7 @@ local defaultSounds = { { key = "fx_ship_bell_chime_02", fid = 1129274 }, { key = "fx_ship_bell_chime_03", fid = 1129275 }, { key = "raidwarning", fid = 567397 }, -} +}; -- Register default sounds for _, sound in ipairs(defaultSounds) do @@ -26,9 +26,9 @@ for _, sound in ipairs(defaultSounds) do end -- Build soundList with keys = values for quick lookup/use -local soundList = {} +local soundList = {}; for _, soundName in ipairs(SharedMedia:List("sound")) do - soundList[soundName] = soundName + soundList[soundName] = soundName; end function SIPPYCUP.Config.TryCreateConfigFrame() @@ -463,7 +463,7 @@ local function CreateInset(parent, insetData) ElvUI.RegisterSkinnableElement(infoInset, "inset"); -- Distance from bottom - local bottomOffset = 20 + local bottomOffset = 20; infoInset:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 20, bottomOffset); infoInset:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -20, bottomOffset); @@ -1410,7 +1410,7 @@ function SIPPYCUP_ConfigMixin:OnLoad() func = function() SIPPYCUP.Popups.ResetIgnored(); end, - } + }, }; self.allWidgets[#self.allWidgets + 1] = CreateWidgetRowContainer(generalPanel, reminderCheckboxData); @@ -1437,7 +1437,7 @@ function SIPPYCUP_ConfigMixin:OnLoad() SIPPYCUP.Database.UpdateSetting("global", "PopupPosition", nil, val); end, }, - } + }; self.allWidgets[#self.allWidgets + 1] = CreateWidgetRowContainer(generalPanel, positionWidgetData); @@ -1484,7 +1484,7 @@ function SIPPYCUP_ConfigMixin:OnLoad() SIPPYCUP.Database.UpdateSetting("global", "FlashTaskbar", nil, val); end, }, - } + }; self.allWidgets[#self.allWidgets + 1] = CreateWidgetRowContainer(generalPanel, alertWidgetData); @@ -1511,7 +1511,7 @@ function SIPPYCUP_ConfigMixin:OnLoad() end end, }, - } + }; self.allWidgets[#self.allWidgets + 1] = CreateWidgetRowContainer(generalPanel, integrationsWidgetData); @@ -1547,7 +1547,7 @@ function SIPPYCUP_ConfigMixin:OnLoad() text = "Bluesky", tooltip = L.OPTIONS_GENERAL_BLUESKY_SHILL_DESC, }, - } + }; self.allWidgets[#self.allWidgets + 1] = CreateInset(generalPanel, insetData); From d00783482b87826b2109351b158ba8badc47e9a3 Mon Sep 17 00:00:00 2001 From: Raenore <172234435+Raenore@users.noreply.github.com> Date: Mon, 16 Feb 2026 05:09:54 +0100 Subject: [PATCH 09/13] Add prism-specific timers (projection & reflecting) --- .luacheckrc | 1 + Core/Auras.lua | 16 +++++++++--- Core/Database.lua | 6 +++++ Core/Options.lua | 4 +-- Locales/enUS.lua | 2 ++ Locales/esES.lua | 2 ++ Locales/frFR.lua | 2 ++ Locales/ruRU.lua | 2 ++ UI/Config.lua | 65 ++++++++++++++++++++++++++++++++++++++++++++++- 9 files changed, 94 insertions(+), 6 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 9cd16e8..1312b2a 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -817,6 +817,7 @@ stds.wow = { "RUNES", "RUNIC_POWER", "SAVE", + "SETTINGS", "SOUL_SHARDS", "SOUND", "SOUNDKIT", diff --git a/Core/Auras.lua b/Core/Auras.lua index 3e2788c..108b256 100644 --- a/Core/Auras.lua +++ b/Core/Auras.lua @@ -26,7 +26,7 @@ function SIPPYCUP.Auras.DebugEnabledAuras() end ---SkipDuplicatePrismUnitAura determines if a prism-type aura update should be ignored ----@param profileOptionData table The option profile data. +---@param profileOptionData SIPPYCUPProfileOption The option profile data. ---@return boolean skip True if this aura update should be skipped due to duplicate UNIT_AURA events. local function SkipDuplicatePrismUnitAura(profileOptionData) local skip = false; @@ -540,7 +540,7 @@ function SIPPYCUP.Auras.CheckPreExpirationForAllActiveOptions(minSeconds) end ---CheckPreExpirationForSingleOption sets up pre-expiration warnings for aura-based options. ----@param profileOptionData table Profile data for the option. +---@param profileOptionData SIPPYCUPProfileOption Profile data for the option. ---@param minSeconds number? Time window to check ahead, defaults to 180. ---@return boolean preExpireFired True if a pre-expiration popup was fired. function SIPPYCUP.Auras.CheckPreExpirationForSingleOption(profileOptionData, minSeconds) @@ -552,7 +552,6 @@ function SIPPYCUP.Auras.CheckPreExpirationForSingleOption(profileOptionData, min end minSeconds = minSeconds or 180.0; - local preOffset = SIPPYCUP.global.PreExpirationLeadTimer * 60; local auraID = profileOptionData.aura; local auraInfo = C_UnitAuras.GetPlayerAuraBySpellID(auraID); @@ -579,6 +578,17 @@ function SIPPYCUP.Auras.CheckPreExpirationForSingleOption(profileOptionData, min local now = GetTime(); local remaining = auraInfo.expirationTime - now; local duration = auraInfo.duration; + local preOffset; + + if profileOptionData.isPrism then + if profileOptionData.usesCharges then -- reflecting prism + preOffset = SIPPYCUP.global.ReflectingPrismPreExpirationLeadTimer * 60; + else -- projection prism + preOffset = SIPPYCUP.global.ProjectionPrismPreExpirationLeadTimer * 60; + end + else -- Global + preOffset = SIPPYCUP.global.PreExpirationLeadTimer * 60; + end -- If the option only lasts for less than user set, we need to change it. if duration <= preOffset then diff --git a/Core/Database.lua b/Core/Database.lua index d6567e6..d327071 100644 --- a/Core/Database.lua +++ b/Core/Database.lua @@ -78,6 +78,8 @@ end ---@field PopupPosition string Position of the popup ("TOP", "BOTTOM", etc.). ---@field PreExpirationChecks boolean Whether to perform checks shortly before aura expiration. ---@field PreExpirationLeadTimer number Time (in minutes) before a pre-expiration reminder should fire. +---@field ProjectionPrismPreExpirationLeadTimer number Time (in minutes) before a projection prism pre-expiration reminder should fire. +---@field ReflectingPrismPreExpirationLeadTimer number Time (in minutes) before a reflecting prism pre-expiration reminder should fire. ---@field UseToyCooldown boolean Whether to use toy cooldowns for popups instead. ---@field WelcomeMessage boolean Whether to display a welcome message on login. @@ -111,6 +113,8 @@ SIPPYCUP.Database.defaults = { PopupPosition = "TOP", PreExpirationChecks = true, PreExpirationLeadTimer = 1, + ProjectionPrismPreExpirationLeadTimer = 5, + ReflectingPrismPreExpirationLeadTimer = 3, UseToyCooldown = true, WelcomeMessage = true, }, @@ -131,7 +135,9 @@ local defaults = SIPPYCUP.Database.defaults; ---@field castAura number The associated cast aura ID, if none is set then use aura ID. ---@field untrackableByAura boolean Whether this option can be tracked via its aura or not. ---@field type string Whether this option is a consumable (0) or toy (1). +---@field isPrism boolean Whether this option is considered a prism. ---@field instantUpdate boolean Whether the instant UNIT_AURA update has already happened right after the addition (prisms). +---@field usesCharges boolean Whether this option uses charges (generally reflecting prism). ---Populate the default option's table keyed by aura ID, with all known entries from SIPPYCUP.Options.Data. ---This defines initial tracking settings for each option by its aura ID key. diff --git a/Core/Options.lua b/Core/Options.lua index 2942b78..e2aee5f 100644 --- a/Core/Options.lua +++ b/Core/Options.lua @@ -96,14 +96,14 @@ SIPPYCUP.Options.Data = { NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 185394, itemID = 124640, category = "EFFECT", preExpiration = true }, -- INKY_BLACK_POTION NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1218300, itemID = 235703, category = "SIZE", stacks = true, maxStacks = 10, preExpiration = true }, -- NOGGENFOGGER_SELECT_DOWN NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1218297, itemID = 235704, category = "SIZE", stacks = true, maxStacks = 10, preExpiration = true }, -- NOGGENFOGGER_SELECT_UP - NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 374959, itemID = {193031, 193030, 193029}, category = "PRISM", preExpiration = true, requiresGroup = true }, -- PROJECTION_PRISM TO-DO: Implement selector, currently Gold > Silver > Bronze + NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 374959, itemID = {193031, 193030, 193029}, category = "PRISM", preExpiration = true, requiresGroup = true, buildAdded = "0.7.0|120001" }, -- PROJECTION_PRISM NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 368038, itemID = 190739, category = "EFFECT", preExpiration = true }, -- PROVIS_WAX NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 244015, itemID = 151256, category = "HANDHELD", preExpiration = true }, -- PURPLE_DANCE_STICK NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 53805, itemID = 40195, category = "SIZE", stacks = true, maxStacks = 10 }, -- PYGMY_OIL NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 393979, itemID = 201428, category = "EFFECT" }, -- QUICKSILVER_SANDS NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1213974, itemID = 234287, category = "EFFECT", preExpiration = true }, -- RADIANT_FOCUS NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1214287, itemID = 234527, category = "HANDHELD", preExpiration = true }, -- SACREDITES_LEDGER - NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 163267, castAuraID = 163219, itemID = 112384, category = "PRISM", preExpiration = true, requiresGroup = true, charges = true }, -- REFLECTING_PRISM + NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 163267, castAuraID = 163219, itemID = 112384, category = "PRISM", preExpiration = true, requiresGroup = true, charges = true, buildAdded = "0.7.0|120001" }, -- REFLECTING_PRISM NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 279742, itemID = 163695, category = "EFFECT" }, -- SCROLL_OF_INNER_TRUTH NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 1222834, itemID = 237332, category = "PLACEMENT", spellTrackable = true }, -- SINGLE_USE_GRILL NewOption{ type = SIPPYCUP.Options.Type.CONSUMABLE, auraID = 58479, itemID = 43480, category = "SIZE", preExpiration = true, delayedAura = true }, -- SMALL_FEAST diff --git a/Locales/enUS.lua b/Locales/enUS.lua index 4a973db..22ad7f0 100644 --- a/Locales/enUS.lua +++ b/Locales/enUS.lua @@ -104,6 +104,8 @@ L = { -- Prism OPTIONS_TAB_PRISM_TITLE = "Prism", OPTIONS_TAB_PRISM_INSTRUCTION = "These options control all reminders for prism consumables/toys that change your appearance.", + OPTIONS_TAB_PRISM_TIMER = "%s - Timer", + OPTIONS_TAB_PRISM_TIMER_TEXT = "Set the desired time, in minutes, before the prism pre-expiration reminder popup should be shown (default: %d minutes).|n|n|cnWARNING_FONT_COLOR:If an option does not support the chosen time, it will default to %d minutes.|r", -- Size OPTIONS_TAB_SIZE_TITLE = "Size", diff --git a/Locales/esES.lua b/Locales/esES.lua index 6416ab3..7dbe4c3 100644 --- a/Locales/esES.lua +++ b/Locales/esES.lua @@ -100,6 +100,8 @@ L = { -- Prism OPTIONS_TAB_PRISM_TITLE = "Prismas", OPTIONS_TAB_PRISM_INSTRUCTION = "Estas opciones controlan todos los recordatorios para los consumibles/juguetes prismáticos que cambian tu apariencia.", + OPTIONS_TAB_PRISM_TIMER = "%s - Timer", -- (NEW) + OPTIONS_TAB_PRISM_TIMER_TEXT = "Set the desired time, in minutes, before the prism pre-expiration reminder popup should be shown (default: %d minutes).|n|n|cnWARNING_FONT_COLOR:If an option does not support the chosen time, it will default to %d minutes.|r", -- (NEW) -- Size OPTIONS_TAB_SIZE_TITLE = "Tamaño", diff --git a/Locales/frFR.lua b/Locales/frFR.lua index 2f8358d..56687d9 100644 --- a/Locales/frFR.lua +++ b/Locales/frFR.lua @@ -107,6 +107,8 @@ L = { -- Prism OPTIONS_TAB_PRISM_TITLE = "Prisme", OPTIONS_TAB_PRISM_INSTRUCTION = "Ces options contrôlent tous les rappels pour les prismes consommables/jouets qui altèrent votre apparence.", + OPTIONS_TAB_PRISM_TIMER = "%s - Timer", -- (NEW) + OPTIONS_TAB_PRISM_TIMER_TEXT = "Set the desired time, in minutes, before the prism pre-expiration reminder popup should be shown (default: %d minutes).|n|n|cnWARNING_FONT_COLOR:If an option does not support the chosen time, it will default to %d minutes.|r", -- (NEW) -- Size OPTIONS_TAB_SIZE_TITLE = "Taille", diff --git a/Locales/ruRU.lua b/Locales/ruRU.lua index babfdf8..65366ec 100644 --- a/Locales/ruRU.lua +++ b/Locales/ruRU.lua @@ -108,6 +108,8 @@ L = { -- Prism OPTIONS_TAB_PRISM_TITLE = "Prism", -- (NEW) OPTIONS_TAB_PRISM_INSTRUCTION = "These options control all reminders for prism consumables/toys to alter your appearance.", -- (NEW) + OPTIONS_TAB_PRISM_TIMER = "%s - Timer", -- (NEW) + OPTIONS_TAB_PRISM_TIMER_TEXT = "Set the desired time, in minutes, before the prism pre-expiration reminder popup should be shown (default: %d minutes).|n|n|cnWARNING_FONT_COLOR:If an option does not support the chosen time, it will default to %d minutes.|r", -- (NEW) -- Size OPTIONS_TAB_SIZE_TITLE = "Размер", diff --git a/UI/Config.lua b/UI/Config.lua index 68bd15f..cd84c44 100644 --- a/UI/Config.lua +++ b/UI/Config.lua @@ -883,6 +883,8 @@ local function CreateConfigSlider(elementContainer, data) local r, g, b = WHITE_FONT_COLOR:GetRGB(); + widget.TopText:SetText(data.label); + widget.MinText:SetText(minVal); widget.MinText:SetVertexColor(r, g, b); @@ -891,13 +893,14 @@ local function CreateConfigSlider(elementContainer, data) widget.RightText:SetVertexColor(r, g, b); + widget.TopText:Show(); widget.MinText:Show(); widget.MaxText:Show(); widget.RightText:Show(); widget:SetPoint("LEFT", 0, 0); widget:SetWidth(elementContainer:GetWidth() * 0.8); - widget:SetHeight(41); + widget:SetHeight(data.height or 41); if data.buildAdded and SIPPYCUP_BUILDINFO.CheckNewlyAdded(data.buildAdded) then local newPip = widget:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); @@ -1622,6 +1625,66 @@ function SIPPYCUP_ConfigMixin:OnLoad() end end + if categoryName == "PRISM" then + CreateCategoryHeader(categoryPanel, SETTINGS); + + local prismWidgetData = { + { + type = "slider", + label = L.OPTIONS_TAB_PRISM_TIMER:format(SIPPYCUP.Options.ByItemID[193031].name), + tooltip = L.OPTIONS_TAB_PRISM_TIMER_TEXT:format(5, 5), + buildAdded = "0.7.0|120001", + min = 1, + max = 7, + step = 1, + height = 35, + disabled = function() + return not SIPPYCUP.Database.GetSetting("global", "ProjectionPrismPreExpirationLeadTimer", nil); + end, + get = function() + return SIPPYCUP.Database.GetSetting("global", "ProjectionPrismPreExpirationLeadTimer", nil); + end, + set = function(val) + SIPPYCUP.Database.UpdateSetting("global", "ProjectionPrismPreExpirationLeadTimer", nil, val); + local reason = SIPPYCUP.Popups.Reason.PRE_EXPIRATION; + SIPPYCUP.Auras.CancelAllPreExpirationTimers(); + SIPPYCUP.Items.CancelAllItemTimers(reason); + SIPPYCUP.Popups.HideAllRefreshPopups(reason); + SIPPYCUP.Options.RefreshStackSizes(SIPPYCUP.MSP.IsEnabled() and SIPPYCUP.global.MSPStatusCheck, false, true); + end, + }, + { + type = "slider", + label = L.OPTIONS_TAB_PRISM_TIMER:format(SIPPYCUP.Options.ByItemID[112384].name), + tooltip = L.OPTIONS_TAB_PRISM_TIMER_TEXT:format(3, 3), + buildAdded = "0.7.0|120001", + min = 1, + max = 5, + step = 1, + height = 35, + disabled = function() + return not SIPPYCUP.Database.GetSetting("global", "ReflectingPrismPreExpirationLeadTimer", nil); + end, + get = function() + return SIPPYCUP.Database.GetSetting("global", "ReflectingPrismPreExpirationLeadTimer", nil); + end, + set = function(val) + SIPPYCUP.Database.UpdateSetting("global", "ReflectingPrismPreExpirationLeadTimer", nil, val); + local reason = SIPPYCUP.Popups.Reason.PRE_EXPIRATION; + SIPPYCUP.Auras.CancelAllPreExpirationTimers(); + SIPPYCUP.Items.CancelAllItemTimers(reason); + SIPPYCUP.Popups.HideAllRefreshPopups(reason); + SIPPYCUP.Options.RefreshStackSizes(SIPPYCUP.MSP.IsEnabled() and SIPPYCUP.global.MSPStatusCheck, false, true); + end, + }, + } + + local widgets = CreateWidgetRowContainer(categoryPanel, prismWidgetData, 2, 40, 20, true); + + self.profileWidgets[#self.profileWidgets + 1] = widgets; + self.allWidgets[#self.allWidgets + 1] = widgets; + end + if #categoryConsumablesData > 0 then CreateCategoryHeader(categoryPanel, BAG_FILTER_CONSUMABLES); From c69d2f16987320f8db78452b82fa35d5b1d2b636 Mon Sep 17 00:00:00 2001 From: Raenore <172234435+Raenore@users.noreply.github.com> Date: Mon, 16 Feb 2026 05:48:48 +0100 Subject: [PATCH 10/13] Update frFR (thanks Daen) --- Locales/frFR.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Locales/frFR.lua b/Locales/frFR.lua index 56687d9..4433050 100644 --- a/Locales/frFR.lua +++ b/Locales/frFR.lua @@ -13,7 +13,7 @@ L = { --/ Popup dialog /-- POPUP_ON_COOLDOWN_TEXT = "En recharge", POPUP_IN_FLIGHT_TEXT = "Désactivé pour ne pas tomber de la monture durant le vol.", - POPUP_NOT_IN_PARTY_TEXT = "Disabled as item requires a party.", -- (NEW) + POPUP_NOT_IN_PARTY_TEXT = "Désactivé : l'objet nécessite d'être en groupe.", POPUP_FOOD_BUFF_TEXT = "Disparaît une fois que le buff de nourriture est appliqué. Ne bougez pas !", POPUP_NOT_IN_INVENTORY_TEXT = "Pas dans l'inventaire", POPUP_NOT_ENOUGH_IN_INVENTORY_TEXT = "Insuffisant (%d manquant)", @@ -42,14 +42,14 @@ L = { OPTIONS_GENERAL_POPUPS_HEADER = "Fenêtre de rappel", OPTIONS_GENERAL_POPUPS_POSITION_NAME = "Position", - OPTIONS_GENERAL_POPUPS_POSITION_DESC = "Sélectionne où vous souhaitez que les fenêtres de rappel s'affichent sur votre écran.", - OPTIONS_GENERAL_POPUPS_POSITION_TOP = "Top (Default)", -- (NEW) - OPTIONS_GENERAL_POPUPS_POSITION_CENTER = "Center", -- (NEW) - OPTIONS_GENERAL_POPUPS_POSITION_BOTTOM = "Bottom", -- (NEW) + OPTIONS_GENERAL_POPUPS_POSITION_DESC = "Sélectionne où vous souhaitez que les notifications de rappel s'affichent sur votre écran.", + OPTIONS_GENERAL_POPUPS_POSITION_TOP = "Haut(Default)", + OPTIONS_GENERAL_POPUPS_POSITION_CENTER = "Centre", + OPTIONS_GENERAL_POPUPS_POSITION_BOTTOM = "Bas", OPTIONS_GENERAL_POPUPS_PRE_EXPIRATION_CHECKS_ENABLE = "Rappel pré-expiration", OPTIONS_GENERAL_POPUPS_PRE_EXPIRATION_CHECKS_ENABLE_DESC = "Active l'affichage des rappels pré-expiration juste avant que le consommable/jouet expire.|n|n|cnWARNING_FONT_COLOR:Tous les objets ne supportent pas cette option; voir l'infobulle quand activé.|r", OPTIONS_GENERAL_POPUPS_PRE_EXPIRATION_LEAD_TIMER = "Minuteur du Rappel pré-expiration", - OPTIONS_GENERAL_POPUPS_PRE_EXPIRATION_LEAD_TIMER_TEXT = "Définit, en minutes, le délai souhaité avant l'affichage d'une notification de Rappel pré-expiration (par défaut: 1 minute).|n|n|cnWARNING_FONT_COLOR:Si une option ne prend pas en charge le délai choisi, la valeur sera par défaut de 1 minute, ou 15 secondes si 1 minute n'est pas disponible.|r", + OPTIONS_GENERAL_POPUPS_PRE_EXPIRATION_LEAD_TIMER_TEXT = "Définit, en minutes, le délai souhaité avant l'affichage d'une notification de Rappel pré-expiration (par défaut: 1 minute).|n|n|cnWARNING_FONT_COLOR:Si une option ne prend pas en charge le délai choisi, la valeur par défaut sera de 1 minute, ou 15 secondes si 1 minute n'est pas disponible.|r", OPTIONS_GENERAL_POPUPS_INSUFFICIENT_REMINDER_ENABLE = "Rappel insuffisant", OPTIONS_GENERAL_POPUPS_INSUFFICIENT_REMINDER_ENABLE_DESC = "Active l'affichage d'un rappel lorsque la quantité de consommables est insuffisante pour le prochain rafraîchissement.", OPTIONS_GENERAL_POPUPS_TRACK_TOY_ITEM_CD_ENABLE = "Utiliser la recharge du jouet", @@ -107,8 +107,8 @@ L = { -- Prism OPTIONS_TAB_PRISM_TITLE = "Prisme", OPTIONS_TAB_PRISM_INSTRUCTION = "Ces options contrôlent tous les rappels pour les prismes consommables/jouets qui altèrent votre apparence.", - OPTIONS_TAB_PRISM_TIMER = "%s - Timer", -- (NEW) - OPTIONS_TAB_PRISM_TIMER_TEXT = "Set the desired time, in minutes, before the prism pre-expiration reminder popup should be shown (default: %d minutes).|n|n|cnWARNING_FONT_COLOR:If an option does not support the chosen time, it will default to %d minutes.|r", -- (NEW) + OPTIONS_TAB_PRISM_TIMER = "%s - Minuteur", + OPTIONS_TAB_PRISM_TIMER_TEXT = "Définit, en minutes, le délai souhaité avant l'affichage d'une notification de Rappel de pré-expiration du prisme (défaut : %d minutes).|n|n|cnWARNING_FONT_COLOR:Si une option ne prend pas en charge le délai choisi, la valeur par défaut sera de %d minutes.|r", -- Size OPTIONS_TAB_SIZE_TITLE = "Taille", From c9bb8651db9ab4e03af42c0cba2a5001c3bcf7df Mon Sep 17 00:00:00 2001 From: Raenore <172234435+Raenore@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:20:59 +0100 Subject: [PATCH 11/13] Fix whitespace added by merge --- Modules/Popups/Popups.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Popups/Popups.lua b/Modules/Popups/Popups.lua index a1b6e5c..773433b 100644 --- a/Modules/Popups/Popups.lua +++ b/Modules/Popups/Popups.lua @@ -793,7 +793,7 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) if not optionData or not profileOptionData or SIPPYCUP.Popups.IsIgnored(optionData.auraID) then return; end - + -- If user has disabled an option before it's shown (due to deferring or something else), remove it. if not profileOptionData.enable then RemoveDeferredActionsByLoc(optionData.loc); From faea0bee32468e16f446e626424fc7b9f9de67b5 Mon Sep 17 00:00:00 2001 From: Raenore <172234435+Raenore@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:22:15 +0100 Subject: [PATCH 12/13] Another one (thanks merge) --- Modules/Popups/Popups.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Popups/Popups.lua b/Modules/Popups/Popups.lua index 773433b..2c982af 100644 --- a/Modules/Popups/Popups.lua +++ b/Modules/Popups/Popups.lua @@ -794,7 +794,7 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) return; end - -- If user has disabled an option before it's shown (due to deferring or something else), remove it. + -- If user has disabled an option before it's shown (due to deferring or something else), remove it. if not profileOptionData.enable then RemoveDeferredActionsByLoc(optionData.loc); local existingPopup = SIPPYCUP.Popups.activeByLoc[optionData.loc]; From 63316339d30caf986c21dde8bce0a6595d172809 Mon Sep 17 00:00:00 2001 From: Raenore <172234435+Raenore@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:31:41 +0100 Subject: [PATCH 13/13] Remove unnecessary prints & add debug option to disable prints --- Core/Bags.lua | 2 -- Core/Database.lua | 1 + Core/Utils.lua | 2 +- Modules/Popups/Popups.lua | 13 ++----------- UI/Config.lua | 15 +++++++++++++++ 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Core/Bags.lua b/Core/Bags.lua index b99217d..5ae4702 100644 --- a/Core/Bags.lua +++ b/Core/Bags.lua @@ -7,8 +7,6 @@ SIPPYCUP.Bags.auraGeneration = 0; SIPPYCUP.Bags.bagGeneration = 0; function SIPPYCUP.Bags.BagUpdateDelayed() - SIPPYCUP_OUTPUT.Debug("BAG_UPDATE_DELAYED"); - SIPPYCUP.Bags.lastBagUpdate = GetTime(); SIPPYCUP.Bags.bagGeneration = SIPPYCUP.Bags.bagGeneration + 1; diff --git a/Core/Database.lua b/Core/Database.lua index d327071..4a6fb13 100644 --- a/Core/Database.lua +++ b/Core/Database.lua @@ -98,6 +98,7 @@ SIPPYCUP.Database.defaults = { global = { AlertSound = true, AlertSoundID = "fx_ship_bell_chime_02", + DebugOutput = false, FlashTaskbar = true, Flyway = { CurrentBuild = 0, diff --git a/Core/Utils.lua b/Core/Utils.lua index 6af38cf..788950e 100644 --- a/Core/Utils.lua +++ b/Core/Utils.lua @@ -154,7 +154,7 @@ end ---Accepts any number of arguments and joins them with space. ---@param ... any Values to print (strings, numbers, tables, etc.) function SIPPYCUP_OUTPUT.Debug(...) - if not SIPPYCUP.IS_DEV_BUILD then return; end + if not SIPPYCUP.IS_DEV_BUILD or not SIPPYCUP.Database.GetSetting("global", "DebugOutput", nil) then return; end local args = {...}; local outputLines = {}; diff --git a/Modules/Popups/Popups.lua b/Modules/Popups/Popups.lua index 2c982af..16b234c 100644 --- a/Modules/Popups/Popups.lua +++ b/Modules/Popups/Popups.lua @@ -684,14 +684,12 @@ function SIPPYCUP.Popups.Toggle(itemName, auraID, enabled) -- If item can only be tracked by the item cooldown (worst) if trackByItem then - SIPPYCUP_OUTPUT.Debug("Tracking through Item"); startTime = C_Item.GetItemCooldown(optionData.itemID); if startTime and startTime > 0 then active = true; end -- If item can be tracked through the spell cooldown (fine). elseif trackBySpell then - SIPPYCUP_OUTPUT.Debug("Tracking through Spell"); local spellCooldownInfo = C_Spell.GetSpellCooldown(optionData.auraID); if canaccessvalue == nil or canaccessvalue(spellCooldownInfo) then startTime = spellCooldownInfo and spellCooldownInfo.startTime; @@ -816,18 +814,11 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) local isToy = optionType == SIPPYCUP.Options.Type.TOY; local isConsumable = optionType == SIPPYCUP.Options.Type.CONSUMABLE; - SIPPYCUP_OUTPUT.Debug("SIPPYCUP.Bags.updateUnhandled:", SIPPYCUP.Bags.updateUnhandled); SIPPYCUP_OUTPUT.Debug("name:", optionData.name); - if isToy then - SIPPYCUP_OUTPUT.Debug("Toy"); - else - SIPPYCUP_OUTPUT.Debug("Consumable"); - end - -- Check for a dirty bag state, if so then defer until it is no longer. if isConsumable and data.needsBagCheck then if SIPPYCUP.Bags.bagGeneration < data.auraGeneration then - SIPPYCUP_OUTPUT.Debug("We reached item count in HandlePopupAction but bag state is dirty"); + SIPPYCUP_OUTPUT.Debug("Reached HandlePopupAction, but bag state is dirty"); deferredActions[#deferredActions + 1] = { data = data, @@ -838,7 +829,7 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller) }; return; end - SIPPYCUP_OUTPUT.Debug("Bag state is not dirty, we can continue this time!"); + SIPPYCUP_OUTPUT.Debug("Reached HandlePopupAction, bag state is fine so continue."); end local itemIDs = type(optionData.itemID) == "table" and optionData.itemID or { optionData.itemID }; diff --git a/UI/Config.lua b/UI/Config.lua index cd84c44..eb9d7cb 100644 --- a/UI/Config.lua +++ b/UI/Config.lua @@ -519,6 +519,21 @@ local function CreateInset(parent, insetData) bsky:SetScript("OnClick", function() SIPPYCUP.LinkDialog.CreateExternalLinkDialog("https://bsky.app/profile/dawnsong.me"); end); + + if SIPPYCUP.IS_DEV_BUILD then + local printCheckbox = CreateFrame("CheckButton", nil, infoInset, "SettingsCheckBoxTemplate"); + printCheckbox:SetPoint("TOPRIGHT", bsky, "TOPLEFT", -8, 0); + printCheckbox:SetSize(22, 22); + ElvUI.RegisterSkinnableElement(printCheckbox, "checkbox"); + + printCheckbox:SetChecked(SIPPYCUP.Database.GetSetting("global", "DebugOutput", nil)); + + AttachTooltip(printCheckbox, "Enable Debug Output", "Click this to enable the debug prints.|n|nIf you see this without knowing what debug is, you might've done something wrong!"); + + printCheckbox:SetScript("OnClick", function(self) + SIPPYCUP.Database.UpdateSetting("global", "DebugOutput", nil, self:GetChecked()) + end); + end end end