Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to this project will be documented in this file.

## [0.7.4] - 2026-03-25
Minor patch adding select Midnight consumables.

### Added
- Added [Pango Plating](https://www.wowhead.com/item=268717) (Appearance) ([#96](https://github.com/Raenore/Sippy-Cup/pull/96)).
- Added [Hexed Potatoad](https://www.wowhead.com/item=252265) Mucus (Appearance) and [Researcher's Shadowgraft](https://www.wowhead.com/item=250319) (Effect) ([#97](https://github.com/Raenore/Sippy-Cup/pull/97)).

## [0.7.3] - 2026-03-05
Minor patch adding select Ruby Feast (Dragonflight) consumables and resolving user-submitted bugs.

Expand Down Expand Up @@ -79,7 +86,8 @@ Given that Sippy Cup never officially supported combat situations, these restric
## Full Changelog
The complete changelog, including older versions, can always be found on [Sippy Cup's GitHub Wiki](https://github.com/Raenore/Sippy-Cup/wiki/Full-Changelog).

[unreleased]: https://github.com/Raenore/Sippy-Cup/compare/0.7.3...HEAD
[unreleased]: https://github.com/Raenore/Sippy-Cup/compare/0.7.4...HEAD
[0.7.4]: https://github.com/Raenore/Sippy-Cup/compare/0.7.3...0.7.4
[0.7.3]: https://github.com/Raenore/Sippy-Cup/compare/0.7.2...0.7.3
[0.7.2]: https://github.com/Raenore/Sippy-Cup/compare/0.7.1...0.7.2
[0.7.1]: https://github.com/Raenore/Sippy-Cup/compare/0.7.0...0.7.1
Expand Down
42 changes: 23 additions & 19 deletions Core/Auras.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

SIPPYCUP.Auras = {};

---CheckEnabledAuras displays data on the currently tracking enabled options (even if they are zero).
---DebugEnabledAuras displays data on the currently tracking enabled options (even if they are zero).
---@return nil
function SIPPYCUP.Auras.DebugEnabledAuras()
for _, profileOptionData in pairs(SIPPYCUP.Database.auraToProfile) do
Expand Down Expand Up @@ -107,7 +107,7 @@ local function ParseAura(updateInfo)
-- On aura application (spellId secret in combat).
local added = updateInfo.addedAuras;
if added then
for _, auraInfo in ipairs(updateInfo.addedAuras) do
for _, auraInfo in ipairs(added) do
local profileOptionData = SIPPYCUP.Database.FindMatchingProfile(auraInfo.spellId);
if profileOptionData and profileOptionData.enable then
local skip = SkipDuplicatePrismUnitAura(profileOptionData, auraInfo.auraInstanceID);
Expand All @@ -127,7 +127,7 @@ local function ParseAura(updateInfo)
-- On aura update (auraInstanceID is not secret).
local updated = updateInfo.updatedAuraInstanceIDs;
if updated then
for _, auraInstanceID in ipairs(updateInfo.updatedAuraInstanceIDs) do
for _, auraInstanceID in ipairs(updated) do
local profileOptionData = SIPPYCUP.Database.FindMatchingProfile(nil, auraInstanceID);
if profileOptionData and profileOptionData.enable then
local auraInfo = GetAuraDataByAuraInstanceID("player", auraInstanceID);
Expand All @@ -151,7 +151,7 @@ local function ParseAura(updateInfo)
-- On aura removal (auraInstanceID is not secret).
local removed = updateInfo.removedAuraInstanceIDs;
if removed then
for _, auraInstanceID in ipairs(updateInfo.removedAuraInstanceIDs) do
for _, auraInstanceID in ipairs(removed) do
local profileOptionData = SIPPYCUP.Database.FindMatchingProfile(nil, auraInstanceID);
if profileOptionData and profileOptionData.enable and profileOptionData.currentInstanceID then
SIPPYCUP.Database.instanceToProfile[auraInstanceID] = nil;
Expand All @@ -166,12 +166,20 @@ local function ParseAura(updateInfo)
end
end

---BuildAuraKey creates a key from an auraID and instanceID.
---@param auraID number The aura ID.
---@param instanceID number The aura instance ID.
---@return string key The key.
local function BuildAuraKey(auraID, instanceID)
return tostring(auraID) .. "-" .. tostring(instanceID);
end

SIPPYCUP.Auras.auraQueue = {};
SIPPYCUP.Auras.auraQueueScheduled = false;

---flushAuraQueue combines all the UNIT_AURA events in the same frame together, filtering them for weird exceptions.
---FlushAuraQueue combines all the UNIT_AURA events in the same frame together, filtering them for weird exceptions.
---@return nil
local function flushAuraQueue()
local function FlushAuraQueue()
local queue = SIPPYCUP.Auras.auraQueue;
SIPPYCUP.Auras.auraQueue = {};
SIPPYCUP.Auras.auraQueueScheduled = false;
Expand Down Expand Up @@ -226,7 +234,7 @@ local function flushAuraQueue()
for addID, auraInfo in pairs(seenAdd) do
if canaccessvalue(auraInfo.spellId) and auraInfo.spellId == auraID then
-- cancel any pre-expiration timer keyed by this spell+instance
local key = tostring(auraID) .. "-" .. tostring(removedID);
local key = BuildAuraKey(auraID, removedID);
SIPPYCUP.Auras.CancelPreExpirationTimer(key);

seenRemoval[removedID] = nil;
Expand All @@ -247,7 +255,7 @@ local function flushAuraQueue()
if seenAdd[updatedAuraInstanceIDs] then
if canaccessvalue(seenAdd[updatedAuraInstanceIDs].spellId) then
-- cancel any pre-expiration timer keyed by this spell+instance
local key = tostring(seenAdd[updatedAuraInstanceIDs].spellId) .. "-" .. tostring(updatedAuraInstanceIDs);
local key = BuildAuraKey(seenAdd[updatedAuraInstanceIDs].spellId, updatedAuraInstanceIDs);
SIPPYCUP.Auras.CancelPreExpirationTimer(key);
seenAdd[updatedAuraInstanceIDs] = nil;
end
Expand Down Expand Up @@ -338,7 +346,7 @@ function SIPPYCUP.Auras.Convert(source, data)
-- flush on the next frame (which will run the batched UNIT_AURAs)
if not SIPPYCUP.Auras.auraQueueScheduled then
SIPPYCUP.Auras.auraQueueScheduled = true;
RunNextFrame(flushAuraQueue);
RunNextFrame(FlushAuraQueue);
end
end

Expand All @@ -360,7 +368,7 @@ function SIPPYCUP.Auras.CheckAllActiveOptions()
for _, profileOptionData in pairs(auraToProfile) do
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 canBeActive = currentInstanceID ~= nil;
local auraInfo = GetPlayerAuraBySpellID(profileOptionData.aura);

if not auraInfo then
Expand All @@ -386,7 +394,7 @@ function SIPPYCUP.Auras.CheckAllActiveOptions()
end
else
-- There is auraInfo data but the spell was not marked as active, this means it was added but we did not catch it.
Convert(SIPPYCUP.Auras.Sources.ADD_AURA, auraInfo);
Convert(SIPPYCUP.Auras.Sources.ADD_AURA, { auraInfo });
end
end
end
Expand Down Expand Up @@ -433,10 +441,6 @@ end

local scheduledPreExpirationAuraTimers = {};

local function BuildAuraKey(auraID, instanceID)
return tostring(auraID) .. "-" .. tostring(instanceID)
end

---CreatePreExpirationTimer schedules a pre-expiration popup for a specific aura.
---Either pass in `key` directly, or provide `auraID` and `auraInstanceID` to build it.
---@param fireIn number Seconds until the timer fires.
Expand Down Expand Up @@ -616,8 +620,8 @@ function SIPPYCUP.Auras.CheckPreExpirationForSingleOption(profileOptionData, min
-- Schedule for "preOffset" seconds before expiration
local fireIn = remaining - preOffset;

if fireIn <= 0 and SIPPYCUP.global.PreExpirationChecks then
-- Less than 60s left and we want pre-expiration popup: fire immediately
if fireIn <= 0 then
-- Less than preOffset left: fire immediately
preExpireFired = true;

local data = {
Expand All @@ -629,8 +633,8 @@ function SIPPYCUP.Auras.CheckPreExpirationForSingleOption(profileOptionData, min
reason = SIPPYCUP.Popups.Reason.PRE_EXPIRATION,
};
SIPPYCUP.Popups.QueuePopupAction(data, "CheckPreExpirationForSingleOption - pre-expiration");
elseif SIPPYCUP.global.PreExpirationChecks then
-- Schedule our 1m before expiration reminder.
else
-- Schedule our pre-expiration reminder.
SIPPYCUP.Auras.CreatePreExpirationTimer(fireIn, auraInfo, key, auraID);
end
end
Expand Down
50 changes: 23 additions & 27 deletions Core/Database.lua
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@ SIPPYCUP.Database.defaults = {

local defaults = SIPPYCUP.Database.defaults;

---PersistCurrentProfile saves the current runtime profile as minimal differences to saved variables.
---@return nil
local function PersistCurrentProfile()
local currentProfileName = SIPPYCUP.Database.GetCurrentProfileName();
if not currentProfileName or not SIPPYCUP.Profile then return; end

local defaultProfile = defaults.profiles.Default or {};
local minimal = GetMinimalTable(SIPPYCUP.Profile, defaultProfile);
SIPPYCUP.db.profiles[currentProfileName] = minimal;
end

---Represents a single option's tracking settings within a user profile.
---@class SIPPYCUPProfileOption: table
---@field enable boolean Whether the option is enabled for tracking.
Expand All @@ -159,7 +170,7 @@ local function PopulateDefaultOptions()
local spellID = option.auraID;
local castSpellID = option.castAuraID;
local untrackableByAura = option.itemTrackable or option.spellTrackable;
local type = option.type;
local optionType = option.type;
local isPrism = (option.category == "PRISM") or false;
local instantUpdate = not isPrism;
local usesCharges = option.charges;
Expand All @@ -174,7 +185,7 @@ local function PopulateDefaultOptions()
aura = spellID,
castAura = castSpellID,
untrackableByAura = untrackableByAura,
type = type,
type = optionType,
isPrism = isPrism,
instantUpdate = instantUpdate,
usesCharges = usesCharges,
Expand Down Expand Up @@ -379,7 +390,7 @@ function SIPPYCUP.Database.GetGlobalSetting(key)

if global[key] == nil then
if type(def) == "table" then
global[key] = global[key] or {};
global[key] = {};
local t = global[key];
for k, v in pairs(def) do
t[k] = t[k] or v;
Expand All @@ -397,7 +408,6 @@ end
---@param value any
function SIPPYCUP.Database.SetGlobalSetting(key, value)
local global = SIPPYCUP.global;
-- local def = SIPPYCUP.Database.defaults.global[key];

if type(value) == "table" then
global[key] = global[key] or {};
Expand Down Expand Up @@ -550,11 +560,7 @@ function SIPPYCUP.Database.SetProfile(profileName)
local defaultProfile = defaults.profiles.Default or {};

-- Persist current runtime profile before switching
local currentProfileName = SIPPYCUP.Database.GetCurrentProfileName();
if currentProfileName and SIPPYCUP.Profile then
local minimal = GetMinimalTable(SIPPYCUP.Profile, defaultProfile);
db.profiles[currentProfileName] = minimal;
end
PersistCurrentProfile();

-- Create target profile if it does not exist
if not db.profiles[profileName] then
Expand Down Expand Up @@ -599,11 +605,7 @@ function SIPPYCUP.Database.CreateProfile(profileName)
local defaultProfile = defaults.profiles.Default or {};

-- Persist current runtime profile before switching
local currentProfileName = SIPPYCUP.Database.GetCurrentProfileName();
if currentProfileName and SIPPYCUP.Profile then
local minimal = GetMinimalTable(SIPPYCUP.Profile, defaultProfile);
db.profiles[currentProfileName] = minimal;
end
PersistCurrentProfile();

-- Create an empty minimal profile (no overrides)
db.profiles[profileName] = {};
Expand Down Expand Up @@ -679,9 +681,6 @@ function SIPPYCUP.Database.CopyProfile(sourceProfileName)
DeepCopyDefaults(defaultProfile, sourceFull);
DeepMerge(SIPPYCUP.db.profiles[sourceProfileName], sourceFull);

-- Clear current profile minimal data table
SIPPYCUP.db.profiles[currentProfileName] = {};

-- Save only minimal differences from defaults into current profile
local minimalCopy = GetMinimalTable(sourceFull, defaultProfile);
SIPPYCUP.db.profiles[currentProfileName] = minimalCopy;
Expand Down Expand Up @@ -714,29 +713,26 @@ function SIPPYCUP.Database.DeleteProfile(profileName)
-- Delete the profile minimal data
SIPPYCUP.db.profiles[profileName] = nil;

-- Check current profile via profileKeys mapping for current character
local charKey = SIPPYCUP.Database.GetUnitName();
local currentProfile = SIPPYCUP.db.profileKeys[charKey] or "Default";

-- Remove any profileKeys that point to this profile
if SIPPYCUP.db.profileKeys then
for charKey, profName in pairs(SIPPYCUP.db.profileKeys) do
for key, profName in pairs(SIPPYCUP.db.profileKeys) do
if profName == profileName then
SIPPYCUP.db.profileKeys[charKey] = "Default";
SIPPYCUP.db.profileKeys[key] = "Default";
end
end
else
SIPPYCUP.db.profileKeys = {};
end

-- Check current profile via profileKeys mapping for current character
local charKey = SIPPYCUP.Database.GetUnitName();
local currentProfile = SIPPYCUP.db.profileKeys[charKey] or "Default";

if profileName == currentProfile then
-- Switch character's profile to Default
SIPPYCUP.db.profileKeys[charKey] = "Default";

-- Ensure Default profile exists minimally
-- Ensure Default profile exists minimally (no overrides = empty table)
if not SIPPYCUP.db.profiles["Default"] then
SIPPYCUP.db.profiles["Default"] = {};
DeepCopyDefaults(defaults.profiles.Default, SIPPYCUP.db.profiles["Default"]);
end

-- Update runtime shortcut with full Default profile data
Expand Down
3 changes: 1 addition & 2 deletions Core/Items.lua
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,7 @@ function SIPPYCUP.Items.CheckNoAuraSingleOption(profileOptionData, spellID, minS
-- Unfortunately duration can be 5 seconds (GCD), so we pull from the spell's base cooldown associated with the item.
local duration;

local trackBySpell = false;
local trackByItem = false;
local trackBySpell, trackByItem = SIPPYCUP.Options.ResolveTrackingMethod(optionData);

-- Determine tracking method
if optionData.type == SIPPYCUP.Options.Type.CONSUMABLE then
Expand Down
33 changes: 27 additions & 6 deletions Core/Options.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ SIPPYCUP.Options.Type = {
};

---@class SIPPYCUPOption: table
---@field type string Whether this option is a consumable (0) or toy (1).
---@field type number 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)
Expand Down Expand Up @@ -45,10 +45,10 @@ local function NewOption(params)
end

return {
type = params.type or SIPPYCUP.Options.Type.CONSUMABLE,
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
itemID = itemIDs, -- always store as a table internally
loc = params.loc,
category = params.category,
profile = params.profile,
Expand All @@ -61,7 +61,7 @@ local function NewOption(params)
spellTrackable = params.spellTrackable or false,
delayedAura = params.delayedAura or false,
cooldownMismatch = params.cooldownMismatch or false,
buildAdded = params.buildAdded or nil,
buildAdded = params.buildAdded,
requiresGroup = params.requiresGroup or false,
charges = params.charges or false,
};
Expand Down Expand Up @@ -260,6 +260,28 @@ SIPPYCUP.Options.Data = {
NewOption{ type = SIPPYCUP.Options.Type.TOY, auraID = 1254376, itemID = 252265, category = "APPEARANCE", cooldownMismatch = true, buildAdded = "0.7.4|120001" }, -- Hexed Potatoad Mucus
};

---ResolveTrackingMethod returns whether to track a given option by spell or item cooldown.
---@param optionData SIPPYCUPOption
---@return boolean trackBySpell
---@return boolean trackByItem
function SIPPYCUP.Options.ResolveTrackingMethod(optionData)
if optionData.type == SIPPYCUP.Options.Type.CONSUMABLE then
return optionData.spellTrackable, optionData.itemTrackable;
elseif optionData.type == SIPPYCUP.Options.Type.TOY then
if optionData.itemTrackable then
return false, true;
end
if optionData.spellTrackable then
if SIPPYCUP.global.UseToyCooldown then
return false, true;
else
return true, false;
end
end
end
return false, false;
end

local function NormalizeLocName(name)
return name:upper():gsub("[^%w]+", "_");
end
Expand Down Expand Up @@ -370,8 +392,7 @@ function SIPPYCUP.Options.RefreshStackSizes(checkAll, reset, preExpireOnly)

-- Helper to check cooldown startTime for item or spell trackable
local function GetCooldownStartTime(option)
local trackBySpell = option.spellTrackable or false;
local trackByItem = option.itemTrackable or false;
local trackBySpell, trackByItem = SIPPYCUP.Options.ResolveTrackingMethod(option);

if trackByItem then
for _, id in ipairs(option.itemID) do
Expand Down
Loading
Loading