diff --git a/.luacheckrc b/.luacheckrc
index 6565a19..d59c1c8 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -516,6 +516,7 @@ stds.wow = {
"GetMouseFocus",
"GetNormalizedRealmName",
"GetNumLanguages",
+ "GetNumSubgroupMembers",
"GetPlayerInfoByGUID",
"GetRealmName",
"GetSpellBaseCooldown",
@@ -829,6 +830,7 @@ stds.wow = {
"RUNES",
"RUNIC_POWER",
"SAVE",
+ "SETTINGS",
"SOUL_SHARDS",
"SOUND",
"SOUNDKIT",
diff --git a/Core/Auras.lua b/Core/Auras.lua
index a8ab4e1..b62eb5e 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 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;
+
+ -- 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;
- QueueAuraAction(profileOptionData, auraInfo, SIPPYCUP.Popups.Reason.ADDITION, "ParseAura - addition");
+ SIPPYCUP.Auras.CheckPreExpirationForSingleOption(profileOptionData);
+
+ 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;
- QueueAuraAction(profileOptionData, auraInfo, SIPPYCUP.Popups.Reason.ADDITION, "ParseAura - updated");
+ SIPPYCUP.Auras.CancelPreExpirationTimer(nil, profileOptionData.aura, auraInstanceID);
+ SIPPYCUP.Auras.CheckPreExpirationForSingleOption(profileOptionData);
+
+ 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
@@ -296,7 +327,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
@@ -321,7 +352,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);
@@ -511,7 +542,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)
@@ -523,7 +554,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);
@@ -550,6 +580,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/Bags.lua b/Core/Bags.lua
new file mode 100644
index 0000000..5ae4702
--- /dev/null
+++ b/Core/Bags.lua
@@ -0,0 +1,22 @@
+-- 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.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 62333eb..4a6fb13 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.
@@ -96,6 +98,7 @@ SIPPYCUP.Database.defaults = {
global = {
AlertSound = true,
AlertSoundID = "fx_ship_bell_chime_02",
+ DebugOutput = false,
FlashTaskbar = true,
Flyway = {
CurrentBuild = 0,
@@ -111,13 +114,15 @@ SIPPYCUP.Database.defaults = {
PopupPosition = "TOP",
PreExpirationChecks = true,
PreExpirationLeadTimer = 1,
+ ProjectionPrismPreExpirationLeadTimer = 5,
+ ReflectingPrismPreExpirationLeadTimer = 3,
UseToyCooldown = true,
WelcomeMessage = true,
},
profiles = {
Default = {}, -- will hold all options per profile
},
-}
+};
local defaults = SIPPYCUP.Database.defaults;
@@ -128,7 +133,12 @@ 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.
+---@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.
@@ -138,7 +148,12 @@ 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;
+ 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
@@ -148,7 +163,12 @@ local function PopulateDefaultOptions()
currentInstanceID = nil,
currentStacks = 0,
aura = spellID,
+ castAura = castSpellID,
untrackableByAura = untrackableByAura,
+ type = type,
+ isPrism = isPrism,
+ instantUpdate = instantUpdate,
+ usesCharges = usesCharges,
};
end
end
@@ -162,6 +182,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 +194,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/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 5764724..e2aee5f 100644
--- a/Core/Options.lua
+++ b/Core/Options.lua
@@ -17,7 +17,8 @@ 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 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).
---@field profile table The option's associated DB profile (auto-gen).
@@ -30,16 +31,24 @@ 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.
---@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,
+ castAuraID = params.castAuraID or params.auraID,
+ itemID = itemIDs; -- always store as a table internally
loc = params.loc,
category = params.category,
profile = params.profile,
@@ -53,6 +62,8 @@ local function NewOption(params)
delayedAura = params.delayedAura or false,
cooldownMismatch = params.cooldownMismatch or false,
buildAdded = params.buildAdded or nil,
+ requiresGroup = params.requiresGroup or false,
+ charges = params.charges or false,
};
end
@@ -85,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 = 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, 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 = 163219, itemID = 112384, category = "PRISM", preExpiration = 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
@@ -216,21 +227,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 +279,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 +327,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..788950e 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 or not SIPPYCUP.Database.GetSetting("global", "DebugOutput", nil) 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..22ad7f0 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)",
@@ -103,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 4d6c7fb..c0e83c9 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)",
@@ -104,6 +105,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 6d59ceb..4433050 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 = "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)",
@@ -41,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",
@@ -106,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 - 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",
diff --git a/Locales/ruRU.lua b/Locales/ruRU.lua
index 42c5e3f..65366ec 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)
@@ -107,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/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 f3c41f6..16b234c 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") and GetNumSubgroupMembers() > 0 then
+ popupData.isNotinGroupButRequired = false;
+ self:Enable();
+ elseif not UnitInParty("player") or GetNumSubgroupMembers() == 0 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") and GetNumSubgroupMembers() > 0 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);
@@ -625,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;
@@ -681,7 +738,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);
-- Bail out entirely when in PvP Matches, we do not show popups.
if SIPPYCUP.States.pvpMatch then
@@ -703,11 +760,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;
@@ -726,7 +783,7 @@ 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];
@@ -753,6 +810,65 @@ 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("name:", optionData.name);
+ -- 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("Reached HandlePopupAction, but bag state is dirty");
+
+ deferredActions[#deferredActions + 1] = {
+ data = data,
+ caller = caller,
+ blockedBy = {
+ bag = true,
+ },
+ };
+ return;
+ end
+ SIPPYCUP_OUTPUT.Debug("Reached HandlePopupAction, bag state is fine so continue.");
+ 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;
@@ -760,45 +876,31 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller)
local auraInfo = data.auraInfo;
local currentInstanceID = profileOptionData.currentInstanceID;
- -- 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).
- if reason == SIPPYCUP.Popups.Reason.REMOVAL or reason == SIPPYCUP.Popups.Reason.PRE_EXPIRATION or optionData.delayedAura or optionData.type == SIPPYCUP.Options.Type.TOY 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 or SIPPYCUP.States.pvpMatch then
- local blockedBy;
- if SIPPYCUP.Items.bagUpdateUnhandled then
- blockedBy = "bag";
- elseif InCombatLockdown() or SIPPYCUP.States.pvpMatch 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)
@@ -831,20 +933,16 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller)
active = true;
end
- local auraInstanceID = auraInfo and auraInfo.auraInstanceID;
-
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
- -- Always track by item if itemTrackable
+ elseif isToy then
if optionData.itemTrackable then
trackByItem = true;
end
-
if optionData.spellTrackable then
if SIPPYCUP.global.UseToyCooldown then
trackByItem = true;
@@ -855,19 +953,17 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller)
end
-- 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 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;
+ for _, id in ipairs(itemIDs) do
+ local startTime, duration = C_Container.GetItemCooldown(id);
+ if startTime and duration and duration > 0 and (startTime + duration - now > 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
@@ -877,8 +973,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
@@ -886,34 +980,41 @@ function SIPPYCUP.Popups.HandlePopupAction(data, caller)
SIPPYCUP_OUTPUT.Debug({ caller = caller, auraID = optionData.auraID, itemID = optionData.itemID, name = optionData.name });
+ -- 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 == lastIndex then
+ newItemIDs[#newItemIDs + 1] = id;
+ end
+ end
+ optionData.itemID = newItemIDs;
+
+ 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 the option does not support stacks, we always desire just 1.
- profileOptionData.desiredStacks = optionData.stacks and profileOptionData.desiredStacks or 1;
-
- local itemCount;
- if optionData.type == SIPPYCUP.Options.Type.CONSUMABLE then
+ if isConsumable then
profileOptionData.currentStacks = SIPPYCUP.Auras.CalculateCurrentStacks(auraInfo, auraID, reason, active);
- itemCount = C_Item.GetItemCount(optionData.itemID);
- elseif optionData.type == SIPPYCUP.Options.Type.TOY then
+ elseif isToy then
if auraInfo then
profileOptionData.currentStacks = SIPPYCUP.Auras.CalculateCurrentStacks(auraInfo, auraID, reason, active);
else
profileOptionData.currentStacks = active and 1 or 0;
end
- itemCount = PlayerHasToy(optionData.itemID) and 1 or 0;
end
local requiredStacks = profileOptionData.desiredStacks - profileOptionData.currentStacks;
SIPPYCUP.Popups.HandleReminderPopup({
+ active = active,
+ auraID = auraID,
+ auraInfo = auraInfo,
optionData = optionData,
profileOptionData = profileOptionData,
- requiredStacks = requiredStacks,
reason = reason,
+ requiredStacks = requiredStacks,
itemCount = itemCount,
- active = active,
});
end
@@ -945,18 +1046,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() or SIPPYCUP.States.pvpMatch then
- blockedBy = "combat"; -- Only way combat is ever used, by deferring before combat or PvP matches.
- 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 {
@@ -993,9 +1092,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;
@@ -1004,14 +1118,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 b19dd76..1be9d31 100644
--- a/SippyCup.lua
+++ b/SippyCup.lua
@@ -93,6 +93,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
@@ -142,7 +144,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
function SIPPYCUP_Addon:ADDON_RESTRICTION_STATE_CHANGED(_, type, state) -- luacheck: no unused (type)
@@ -189,7 +191,7 @@ 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.
@@ -214,8 +216,7 @@ function SIPPYCUP_Addon:UNIT_AURA(_, unitTarget, updateInfo) -- luacheck: no unu
if InCombatLockdown() or C_Secrets and C_Secrets.ShouldAurasBeSecret() or SIPPYCUP.States.pvpMatch 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
@@ -229,6 +230,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);
diff --git a/UI/Config.lua b/UI/Config.lua
index 10a03ba..eb9d7cb 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()
@@ -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();
@@ -440,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);
@@ -496,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
@@ -860,6 +898,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);
@@ -868,13 +908,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");
@@ -1387,7 +1428,7 @@ function SIPPYCUP_ConfigMixin:OnLoad()
func = function()
SIPPYCUP.Popups.ResetIgnored();
end,
- }
+ },
};
self.allWidgets[#self.allWidgets + 1] = CreateWidgetRowContainer(generalPanel, reminderCheckboxData);
@@ -1414,7 +1455,7 @@ function SIPPYCUP_ConfigMixin:OnLoad()
SIPPYCUP.Database.UpdateSetting("global", "PopupPosition", nil, val);
end,
},
- }
+ };
self.allWidgets[#self.allWidgets + 1] = CreateWidgetRowContainer(generalPanel, positionWidgetData);
@@ -1461,7 +1502,7 @@ function SIPPYCUP_ConfigMixin:OnLoad()
SIPPYCUP.Database.UpdateSetting("global", "FlashTaskbar", nil, val);
end,
},
- }
+ };
self.allWidgets[#self.allWidgets + 1] = CreateWidgetRowContainer(generalPanel, alertWidgetData);
@@ -1488,7 +1529,7 @@ function SIPPYCUP_ConfigMixin:OnLoad()
end
end,
},
- }
+ };
self.allWidgets[#self.allWidgets + 1] = CreateWidgetRowContainer(generalPanel, integrationsWidgetData);
@@ -1524,7 +1565,7 @@ function SIPPYCUP_ConfigMixin:OnLoad()
text = "Bluesky",
tooltip = L.OPTIONS_GENERAL_BLUESKY_SHILL_DESC,
},
- }
+ };
self.allWidgets[#self.allWidgets + 1] = CreateInset(generalPanel, insetData);
@@ -1599,6 +1640,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);
@@ -1634,7 +1735,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");