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
2 changes: 2 additions & 0 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ stds.wow = {
"GetMouseFocus",
"GetNormalizedRealmName",
"GetNumLanguages",
"GetNumSubgroupMembers",
"GetPlayerInfoByGUID",
"GetRealmName",
"GetSpellBaseCooldown",
Expand Down Expand Up @@ -829,6 +830,7 @@ stds.wow = {
"RUNES",
"RUNIC_POWER",
"SAVE",
"SETTINGS",
"SOUL_SHARDS",
"SOUND",
"SOUNDKIT",
Expand Down
93 changes: 67 additions & 26 deletions Core/Auras.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,54 @@ 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.
---@param reason number The reason for the popup (ADDITION, REMOVAL, etc).
---@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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 = {};
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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)
Expand All @@ -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);

Expand All @@ -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
Expand Down
22 changes: 22 additions & 0 deletions Core/Bags.lua
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions Core/Core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<Include file="Utils.lua"/>

<Include file="Auras.lua"/>
<Include file="Bags.lua"/>
<Include file="Items.lua"/>

<Include file="Timers.lua"/>
Expand Down
28 changes: 27 additions & 1 deletion Core/Database.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -96,6 +98,7 @@ SIPPYCUP.Database.defaults = {
global = {
AlertSound = true,
AlertSoundID = "fx_ship_bell_chime_02",
DebugOutput = false,
FlashTaskbar = true,
Flyway = {
CurrentBuild = 0,
Expand All @@ -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;

Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -162,6 +182,9 @@ SIPPYCUP.Database.auraToProfile = {}; -- auraID --> profile data
SIPPYCUP.Database.instanceToProfile = {}; -- instanceID --> profile data
---@type table<number, SIPPYCUPProfileOption>
SIPPYCUP.Database.untrackableByAuraProfile = {}; -- itemID --> profile data (only if no aura)
---@type table<number, SIPPYCUPProfileOption>
SIPPYCUP.Database.castAuraToProfile = {}; -- castAuraID (if different) / auraID --> profile data


---RebuildAuraMap rebuilds internal lookup tables for aura and instance-based option tracking.
---@return nil
Expand All @@ -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;
Expand Down
11 changes: 0 additions & 11 deletions Core/Items.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading