From 035914f0d368135c8a23089955382062bbbd5299 Mon Sep 17 00:00:00 2001 From: ImUnicke Date: Tue, 24 May 2022 22:35:15 -0600 Subject: [PATCH 1/7] Removed whitespace that notepad++ likes to add --- AllTheThings.lua | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/AllTheThings.lua b/AllTheThings.lua index 63e37ba4781..14bf15a36b1 100644 --- a/AllTheThings.lua +++ b/AllTheThings.lua @@ -5390,9 +5390,9 @@ local function ProcessIncomingChunk(sender, uid, index, chunk) if index % 25 == 0 then app.print("Syncing " .. index .. " / " .. incomingForUID.total); end return true; end - + incomingFromSender[uid] = nil; - + local msg = ""; for i=1,incomingForUID.total,1 do msg = msg .. incomingForUID.chunks[i]; @@ -5431,7 +5431,7 @@ local function ProcessIncomingChunk(sender, uid, index, chunk) end app.print("Update complete for " .. character.text .. "."); end - + app:RecalculateAccountWideData(); app.Settings:Refresh(); active = nil; @@ -5493,10 +5493,10 @@ function app:ReceiveSyncRequest(sender, battleTag) return false; end end - + -- Whitelist the character name, if not already. (This is needed for future sync methods) AllTheThingsAD.LinkedAccounts[sender] = true; - + -- Generate the sync string (there may be several depending on how many alts there are) local msgs = {}; local msg = "?\tsyncsum"; @@ -5534,7 +5534,7 @@ function app:ReceiveSyncSummaryResponse(sender, summary) if character then -- Put easy character data into a raw data string local rawData = character.guid .. ":" .. character.name .. ":" .. character.lvl .. ":" .. character.text .. ":" .. (character.realm or " ") .. ":" .. (character.factionID or " ") .. ":" .. (character.classID or " ") .. ":" .. (character.raceID or " ") .. ":" .. character.lastPlayed .. ":" .. character.Deaths; - + for i,field in ipairs(whiteListedFields) do if character[field] then rawData = rawData .. ":" .. field; @@ -5545,7 +5545,7 @@ function app:ReceiveSyncSummaryResponse(sender, summary) end end end - + if not rawMsg then rawMsg = rawData; else @@ -5553,7 +5553,7 @@ function app:ReceiveSyncSummaryResponse(sender, summary) end end end - + if rawMsg then -- Send Addon Message Back local length = string.len(rawMsg); @@ -5569,7 +5569,7 @@ function app:ReceiveSyncSummaryResponse(sender, summary) local uid = outgoingForSender.total + 1; outgoingForSender.uids[uid] = chunks; outgoingForSender.total = uid; - + -- Send Addon Message Back C_ChatInfo.SendAddonMessage("ATT", "chks\t" .. uid .. "\t" .. #chunks, "WHISPER", sender); end @@ -6901,7 +6901,7 @@ local function RefreshCollections() end end coroutine.yield(); - + app:RecalculateAccountWideData(); -- Refresh Sources from Cache if tracking Transmog From ed0403142a311e29a4ca70a142faedff96bd3e38 Mon Sep 17 00:00:00 2001 From: ImUnicke Date: Tue, 24 May 2022 22:43:35 -0600 Subject: [PATCH 2/7] Some simplification and differentiation for tooltip completion indicators from row summaries --- AllTheThings.lua | 46 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/AllTheThings.lua b/AllTheThings.lua index 14bf15a36b1..03f914e7669 100644 --- a/AllTheThings.lua +++ b/AllTheThings.lua @@ -1517,6 +1517,13 @@ end local function GetCompletionText(state) return L[(state == 2 and "COMPLETE_OTHER") or (state and "COMPLETE") or "INCOMPLETE"]; end +local function GetStateIcon(data, iconOnly) + if data.collectible then + return iconOnly and GetCollectionIcon(data.collected) or GetCollectionText(data.collected); + elseif data.trackable then + return iconOnly and GetCompletionIcon(data.saved) or GetCompletionText(data.saved); + end +end local function GetProgressTextForRow(data) local total = data.total; local isCollectible = data.collectible; @@ -1525,16 +1532,19 @@ local function GetProgressTextForRow(data) if isContainer then local costTotal = data.costTotal; local isCost = costTotal and costTotal > 0; - local isFilledCost = data.collectibleAsCost == false; + local isFilledCost = data.filledCost; -- Cost & Progress if isFilledCost then return L["COST_ICON"].." "..GetProgressColorText(data.progress or 0, total); end + -- Cost if isCost then return L["COST_ICON"]; end + + -- Progress Only return GetProgressColorText(data.progress or 0, total); elseif isCollectible then return GetCollectionIcon(data.collected); @@ -1550,30 +1560,43 @@ local function GetProgressTextForRow(data) return "---"; end end -local function GetProgressTextForTooltip(data) +local function GetProgressTextForTooltip(data, iconOnly) local total = data.total; local isCollectible = data.collectible; local isContainer = total and (total > 1 or (total > 0 and not isCollectible)); + local stateText = GetStateIcon(data, iconOnly); if isContainer then local costTotal = data.costTotal; local isCost = costTotal and costTotal > 0; - local isFilledCost = data.collectibleAsCost == false; + local isFilledCost = data.filledCost; -- Cost & Progress if isFilledCost then - return L["COST_TEXT"].." "..GetProgressColorText(data.progress or 0, total); + if stateText then + return L["COST_TEXT"].." "..GetProgressColorText(data.progress or 0, total).." "..stateText; + else + return L["COST_TEXT"].." "..GetProgressColorText(data.progress or 0, total); + end end + -- Cost if isCost then - return L["COST_TEXT"]; + if stateText then + return L["COST_TEXT"].." "..stateText; + else + return L["COST_TEXT"]; + end + end + + -- Progress Only + if stateText then + return GetProgressColorText(data.progress or 0, total).." "..stateText; + else + return GetProgressColorText(data.progress or 0, total); end - return GetProgressColorText(data.progress or 0, total); - elseif isCollectible then - return GetCollectionText(data.collected); - elseif data.trackable then - return GetCompletionText(data.saved); end + return stateText; end local function GetRemovedWithPatchString(rwp) if rwp then @@ -4780,7 +4803,7 @@ local function GetCachedSearchResults(search, method, paramA, paramB, ...) -- If the user wants to show the progress of this search result, do so if app.Settings:GetTooltipSetting("Progress") and (group.key ~= "spellID" or group.collectible) then - group.collectionText = (app.Settings:GetTooltipSetting("ShowIconOnly") and GetProgressTextForRow or GetProgressTextForTooltip)(group); + group.collectionText = GetProgressTextForTooltip(group, app.Settings:GetTooltipSetting("ShowIconOnly")); -- add the progress as a new line for encounter tooltips instead of using right text since it can overlap the NPC name if group.encounterID then tinsert(info, 1, { left = "Progress", right = group.collectionText }); end @@ -4861,6 +4884,7 @@ local function DeterminePurchaseGroups(group) -- app.PrintDebug("DeterminePurchaseGroups",group.hash,"-final",groups and #groups); -- mark this group as no-longer collectible as a cost since its cost collectibles have been determined group.collectibleAsCost = false; + group.filledCost = true; return groups; end end From 68ed05d7020719a238669eff3ab059171ad3ce38 Mon Sep 17 00:00:00 2001 From: ImUnicke Date: Tue, 24 May 2022 23:04:09 -0600 Subject: [PATCH 3/7] Some partial cost logic improvements --- AllTheThings.lua | 54 ++++++++++++------------------------------------ 1 file changed, 13 insertions(+), 41 deletions(-) diff --git a/AllTheThings.lua b/AllTheThings.lua index 03f914e7669..7359eddd547 100644 --- a/AllTheThings.lua +++ b/AllTheThings.lua @@ -7656,7 +7656,10 @@ app.CheckCollectible = CheckCollectible; app.CollectibleAsCost = function(t) local collectibles = t.costCollectibles; -- literally nothing to collect with 't' as a cost, so don't process the logic anymore - if not collectibles or #collectibles == 0 then return; end + if not collectibles or #collectibles == 0 then + t.collectibleAsCost = false; + return; + end -- This instance of the Thing 't' is not actually collectible for this character if it is under a saved quest parent if not app.MODE_DEBUG_OR_ACCOUNT then local parent = t.parent; @@ -7664,11 +7667,6 @@ app.CollectibleAsCost = function(t) -- app.PrintDebug("CollectibleAsCost:t.parent.saved",t.hash) return; end - -- Make sure this thing can actually be collectible via hierarchy - if GetRelativeValue(t, "altcollected") then - -- app.PrintDebug("CollectibleAsCost:altcollected",t.hash) - return; - end end -- mark this group as not collectible by cost while it is processing, in case it has sub-content which can be used to obtain this 't' t.collectibleAsCost = false; @@ -7678,48 +7676,22 @@ app.CollectibleAsCost = function(t) -- Use the common collectibility check logic collectible, collected = CheckCollectible(ref); if collectible and not collected then - -- app.PrintDebug("CollectibleAsCost:true",t.hash,"from",ref.hash) t.collectibleAsCost = nil; + -- app.PrintDebug("CollectibleAsCost:true",t.hash,"from",ref.hash) + -- Found something collectible for t, make sure t is actually obtainable as well + -- Make sure this thing can actually be collectible via hierarchy + if GetRelativeValue(t, "altcollected") then + app.PrintDebug("CollectibleAsCost:altcollected",t.hash) + return; + end + -- Costs are only collectible until all purchases are collected, thus to be collectible means being not collected as well + t.collectedAsCost = false; return true; end end -- app.PrintDebug("CollectibleAsCost:false",t.hash) t.collectibleAsCost = nil; end -app.CollectedAsCost = function(t) - local collectibles = t.costCollectibles; - -- literally nothing to collect with 't' as a cost, so don't process the logic anymore - if not collectibles or #collectibles == 0 then return; end - -- This instance of the Thing 't' is not actually collectible for this character if it is under a saved quest parent - if not app.MODE_DEBUG_OR_ACCOUNT then - local parent = t.parent; - if parent and parent.questID and parent.saved then - -- app.PrintDebug("CollectedAsCost:t.parent.saved",t.hash) - return; - end - -- Make sure this thing can actually be collectible via hierarchy - if GetRelativeValue(t, "altcollected") then - -- app.PrintDebug("CollectedAsCost:altcollected",t.hash) - return; - end - end - -- mark this group as not collectible by cost while it is processing, in case it has sub-content which can be used to obtain this 't' - t.collectedAsCost = false; - -- check the collectibles if any are considered collectible currently - local collectible, collected; - for _,ref in ipairs(collectibles) do - -- Use the common collectibility check logic - collectible, collected = CheckCollectible(ref); - if collectible and not collected then - -- app.PrintDebug("CollectedAsCost:false",t.hash,"from",ref.hash) - t.collectedAsCost = nil; - return; - end - end - -- app.PrintDebug("CollectedAsCost:true",t.hash) - t.collectedAsCost = nil; - return true; -end end)(); -- Achievement Lib From 88f3830dbaa524647716417f017cedf4225e41c7 Mon Sep 17 00:00:00 2001 From: ImUnicke Date: Wed, 25 May 2022 01:29:20 -0600 Subject: [PATCH 4/7] Huge realignment of Type libraries to improve localization/consolidation of functionality Localized a bunch of functions which are only used by other local functionality Couple additional minor local usages instead of repeated table accesses --- AllTheThings.lua | 9305 +++++++++++++++++++++++----------------------- 1 file changed, 4647 insertions(+), 4658 deletions(-) diff --git a/AllTheThings.lua b/AllTheThings.lua index 7359eddd547..87cd6f8a8be 100644 --- a/AllTheThings.lua +++ b/AllTheThings.lua @@ -30,7 +30,6 @@ local C_TransmogCollection_GetIllusionSourceInfo = C_TransmogCollection.GetIllus local C_TransmogCollection_GetItemInfo = C_TransmogCollection.GetItemInfo; local C_TransmogCollection_PlayerHasTransmogItemModifiedAppearance = C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance; local C_TransmogCollection_GetSourceInfo = C_TransmogCollection.GetSourceInfo; -local C_QuestLog_GetAllCompletedQuestIDs = C_QuestLog.GetAllCompletedQuestIDs; local C_Map_GetMapInfo = C_Map.GetMapInfo; local SetPortraitTexture = _G["SetPortraitTexture"]; local SetPortraitTextureFromDisplayID = _G["SetPortraitTextureFromCreatureDisplayID"]; @@ -2595,124 +2594,6 @@ local IsQuestFlaggedCompletedForObject = function(t) end end --- Quest Harvesting Lib (http://www.wowinterface.com/forums/showthread.php?t=46934) --- This is pretty heavily utilized, so will keep it local -local QuestHarvester = CreateFrame("GameTooltip", "AllTheThingsQuestHarvester", UIParent, "GameTooltipTemplate"); -(function() -local questRetries = {}; -local QuestUtils_GetQuestName, C_QuestLog_RequestLoadQuestByID, C_QuestLog_IsOnQuest = QuestUtils_GetQuestName, C_QuestLog.RequestLoadQuestByID, C_QuestLog.IsOnQuest; -local QuestTitleFromID = setmetatable({}, { __index = function(t, id) - if id then - local title = QuestUtils_GetQuestName(id); - if title and title ~= "" then - -- print("QuestUtils_GetQuestName",id,title) - rawset(questRetries, id, nil); - rawset(t, id, title); - return title - end - - C_QuestLog_RequestLoadQuestByID(id); - QuestHarvester:SetOwner(UIParent, "ANCHOR_NONE"); - QuestHarvester:SetHyperlink("quest:"..id); - title = AllTheThingsQuestHarvesterTextLeft1:GetText(); - -- QuestHarvester:SetHyperlink("\124cffaaaaaa\124Hquest:".. id.."\124h[QUEST:".. id .. "]\124h\124r"); - -- local title = AllTheThingsQuestHarvesterTextLeft1:GetText() or C_QuestLog.GetQuestInfo(id); - QuestHarvester:Hide() - if title and title ~= RETRIEVING_DATA then - -- working Quest Link Example from Wowhead - -- /script DEFAULT_CHAT_FRAME:AddMessage("\124cffffff00\124Hquest:48615:110\124h[War Never Changes]\124h\124r"); - -- /script DEFAULT_CHAT_FRAME:AddMessage("\124cffff0000\124Hquest:48615\124h[VisibleText]\124h\124r"); - -- QuestHarvester:SetHyperlink("\124cffffff00\124Hquest:".. id .."\124h[".. title .. "]\124h\124r"); - rawset(questRetries, id, nil); - rawset(t, id, title); - return title - else - local retries = rawget(questRetries, id); - if retries and retries > 120 then - title = "Quest #" .. id .. "*"; - rawset(questRetries, id, nil); - rawset(t, id, title); - return title; - else - rawset(questRetries, id, (retries or 0) + 1); - end - return RETRIEVING_DATA; - end - end -end}); -app.QuestTitleFromID = QuestTitleFromID; -app.events.QUEST_DATA_LOAD_RESULT = function(questID, success) - if success then - local title = QuestUtils_GetQuestName(questID); - if title and title ~= "" then - -- app.PrintDebug("Available QuestData",questID,title) - rawset(questRetries, questID, nil); - rawset(QuestTitleFromID, questID, title); - end - -- trigger a slight delayed refresh to visible ATT windows since a quest name was now populated - app:RefreshWindows(); - else - -- this quest name cannot be populated by the server - -- app.PrintDebug("No Server QuestData",questID) - rawset(questRetries, questID, nil); - rawset(QuestTitleFromID, questID, "Quest #"..questID.."*"); - end -end --- consolidated representation of whether a Thing can be collectible via QuestID -app.CollectibleAsQuest = function(t) - local questID = t.questID; - return - -- must have a questID associated - questID - -- must not be repeatable, unless considering repeatable quests as collectible - and (not t.repeatable or app.Settings:GetTooltipSetting("Repeatable")) - and - ( - ( -- Regular Quests - app.CollectibleQuests - and - ( - ( - ( - -- debug/account mode - app.MODE_DEBUG_OR_ACCOUNT - -- or able to access quest on current character - or not t.locked - ) - -- account-wide quests (special case since quests are only available once per account, so can only consider them collectible if they've never been completed otherwise) - and - ( - app.AccountWideQuests - -- otherwise must not be a once-per-account quest which has already been flagged as completed on a different character - or (not ATTAccountWideData.OneTimeQuests[questID] or ATTAccountWideData.OneTimeQuests[questID] == app.GUID) - ) - ) - -- If it is an item/cost and associated to an active quest. - -- TODO: add t.requiredForQuestID - or (not t.isWorldQuest and (t.cost or t.itemID) and C_QuestLog_IsOnQuest(questID)) - ) - ) - or - ( -- Locked Quests - app.CollectibleQuestsLocked - and - ( - -- not able to access quest on current character - t.locked - and - ( - -- debug/account mode - app.MODE_DEBUG_OR_ACCOUNT - or - -- available in party sync - not t.DisablePartySync - ) - ) - ) - ) -end -end)(); - -- NPC & Title Name Harvesting Lib (https://us.battle.net/forums/en/wow/topic/20758497390?page=1#post-4, Thanks Gello!) (function() local NPCTitlesFromID = {}; @@ -7694,2421 +7575,2879 @@ app.CollectibleAsCost = function(t) end end)(); --- Achievement Lib +-- Quest Lib +-- Quests first because a lot of other Thing libs use Quest logic (function() -local GetAchievementCategory, GetAchievementNumCriteria, GetCategoryInfo, GetStatistic = GetAchievementCategory, GetAchievementNumCriteria, GetCategoryInfo, GetStatistic; -local cache = app.CreateCache("achievementID"); -local function CacheInfo(t, field) - local _t, id = cache.GetCached(t); - --local IDNumber, Name, Points, Completed, Month, Day, Year, Description, Flags, Image, RewardText, isGuildAch = GetAchievementInfo(t.achievementID); - local _, name, _, _, _, _, _, _, _, icon = GetAchievementInfo(id); - _t.link = GetAchievementLink(id); - _t.name = name or ("Achievement #"..id); - _t.icon = icon or QUESTION_MARK_ICON; - if field then return _t[field]; end -end -app.AchievementFilter = 4; -local fields = { - ["key"] = function(t) - return "achievementID"; - end, - ["achievementID"] = function(t) - local achievementID = t.altAchID and app.FactionID == Enum.FlightPathFaction.Horde and t.altAchID or t.achID; - if achievementID then - rawset(t, "achievementID", achievementID); - return achievementID; - end - end, - ["text"] = function(t) - return t.link or t.name; - end, - ["link"] = function(t) - return cache.GetCachedField(t, "link", CacheInfo); - end, - ["name"] = function(t) - return cache.GetCachedField(t, "name", CacheInfo); - end, - ["icon"] = function(t) - return cache.GetCachedField(t, "icon", CacheInfo); - end, - ["collectible"] = function(t) - return app.CollectibleAchievements; - end, - ["collected"] = function(t) - if app.CurrentCharacter.Achievements[t.achievementID] then return 1; end - if select(13, GetAchievementInfo(t.achievementID)) then - app.CurrentCharacter.Achievements[t.achievementID] = 1; - ATTAccountWideData.Achievements[t.achievementID] = 1; - return 1; +local C_QuestLog_GetQuestObjectives,C_QuestLog_IsOnQuest,C_QuestLog_IsQuestReplayable,C_QuestLog_IsQuestReplayedRecently,C_QuestLog_ReadyForTurnIn,C_QuestLog_GetAllCompletedQuestIDs,C_QuestLog_RequestLoadQuestByID,QuestUtils_GetQuestName,GetNumQuestLogRewards,GetQuestLogRewardInfo,GetNumQuestLogRewardCurrencies,GetQuestLogRewardCurrencyInfo = + C_QuestLog.GetQuestObjectives,C_QuestLog.IsOnQuest,C_QuestLog.IsQuestReplayable,C_QuestLog.IsQuestReplayedRecently,C_QuestLog.ReadyForTurnIn,C_QuestLog.GetAllCompletedQuestIDs,C_QuestLog.RequestLoadQuestByID,QuestUtils_GetQuestName,GetNumQuestLogRewards,GetQuestLogRewardInfo,GetNumQuestLogRewardCurrencies,GetQuestLogRewardCurrencyInfo; +local IsSpellKnown,GetSpellInfo,math_floor = + IsSpellKnown,GetSpellInfo,math.floor; + +-- Quest Harvesting Lib (http://www.wowinterface.com/forums/showthread.php?t=46934) +local QuestHarvester = CreateFrame("GameTooltip", "AllTheThingsQuestHarvester", UIParent, "GameTooltipTemplate"); +local questRetries = {}; +local QuestTitleFromID = setmetatable({}, { __index = function(t, id) + if id then + local title = QuestUtils_GetQuestName(id); + if title and title ~= "" then + -- print("QuestUtils_GetQuestName",id,title) + rawset(questRetries, id, nil); + rawset(t, id, title); + return title end - if app.AccountWideAchievements and ATTAccountWideData.Achievements[t.achievementID] then return 2; end - end, - ["parentCategoryID"] = function(t) - return GetAchievementCategory(t.achievementID) or -1; - end, - ["statistic"] = function(t) - if GetAchievementNumCriteria(t.achievementID) == 1 then - local quantity, reqQuantity = select(4, GetAchievementCriteriaInfo(t.achievementID, 1)); - if quantity and reqQuantity and reqQuantity > 1 then - return tostring(quantity) .. " / " .. tostring(reqQuantity); + + C_QuestLog_RequestLoadQuestByID(id); + QuestHarvester:SetOwner(UIParent, "ANCHOR_NONE"); + QuestHarvester:SetHyperlink("quest:"..id); + title = AllTheThingsQuestHarvesterTextLeft1:GetText(); + -- QuestHarvester:SetHyperlink("\124cffaaaaaa\124Hquest:".. id.."\124h[QUEST:".. id .. "]\124h\124r"); + -- local title = AllTheThingsQuestHarvesterTextLeft1:GetText() or C_QuestLog.GetQuestInfo(id); + QuestHarvester:Hide() + if title and title ~= RETRIEVING_DATA then + -- working Quest Link Example from Wowhead + -- /script DEFAULT_CHAT_FRAME:AddMessage("\124cffffff00\124Hquest:48615:110\124h[War Never Changes]\124h\124r"); + -- /script DEFAULT_CHAT_FRAME:AddMessage("\124cffff0000\124Hquest:48615\124h[VisibleText]\124h\124r"); + -- QuestHarvester:SetHyperlink("\124cffffff00\124Hquest:".. id .."\124h[".. title .. "]\124h\124r"); + rawset(questRetries, id, nil); + rawset(t, id, title); + return title + else + local retries = rawget(questRetries, id); + if retries and retries > 120 then + title = "Quest #" .. id .. "*"; + rawset(questRetries, id, nil); + rawset(t, id, title); + return title; + else + rawset(questRetries, id, (retries or 0) + 1); end + return RETRIEVING_DATA; end - local statistic = GetStatistic(t.achievementID); - if statistic and statistic ~= '0' then - return statistic; - end - end, - ["sortProgress"] = function(t) - if t.collected then - return 1; - end - -- only calculate achievement progress using achievements where the single criteria is the 'progress bar' - if GetAchievementNumCriteria(t.achievementID) == 1 then - local quantity, reqQuantity = select(4, GetAchievementCriteriaInfo(t.achievementID, 1)); - if quantity and reqQuantity and reqQuantity > 1 then - -- print("ach-prog",t.achievementID,quantity,reqQuantity); - return (quantity / reqQuantity); - end + end +end}); +app.events.QUEST_DATA_LOAD_RESULT = function(questID, success) + if success then + local title = QuestUtils_GetQuestName(questID); + if title and title ~= "" then + -- app.PrintDebug("Available QuestData",questID,title) + rawset(questRetries, questID, nil); + rawset(QuestTitleFromID, questID, title); end - return 0; - end, - ["OnUpdate"] = function(t) ResolveSymbolicLink(t); end, -}; -app.BaseAchievement = app.BaseObjectFields(fields, "BaseAchievement"); -app.CreateAchievement = function(id, t) - return setmetatable(constructor(id, t, "achID"), app.BaseAchievement); + -- trigger a slight delayed refresh to visible ATT windows since a quest name was now populated + app:RefreshWindows(); + else + -- this quest name cannot be populated by the server + -- app.PrintDebug("No Server QuestData",questID) + rawset(questRetries, questID, nil); + rawset(QuestTitleFromID, questID, "Quest #"..questID.."*"); + end end - -local categoryFields = { - ["key"] = function(t) - return "achievementCategoryID"; - end, - ["name"] = function(t) - return GetCategoryInfo(t.achievementCategoryID); - end, - ["icon"] = function(t) - return app.asset("Category_Achievements"); - end, - ["parentCategoryID"] = function(t) - return select(2, GetCategoryInfo(t.achievementCategoryID)) or -1; - end, -}; -app.BaseAchievementCategory = app.BaseObjectFields(categoryFields, "BaseAchievementCategory"); -app.CreateAchievementCategory = function(id, t) - return setmetatable(constructor(id, t, "achievementCategoryID"), app.BaseAchievementCategory); +-- consolidated representation of whether a Thing can be collectible via QuestID +app.CollectibleAsQuest = function(t) + local questID = t.questID; + return + -- must have a questID associated + questID + -- must not be repeatable, unless considering repeatable quests as collectible + and (not t.repeatable or app.Settings:GetTooltipSetting("Repeatable")) + and + ( + ( -- Regular Quests + app.CollectibleQuests + and + ( + ( + ( + -- debug/account mode + app.MODE_DEBUG_OR_ACCOUNT + -- or able to access quest on current character + or not t.locked + ) + -- account-wide quests (special case since quests are only available once per account, so can only consider them collectible if they've never been completed otherwise) + and + ( + app.AccountWideQuests + -- otherwise must not be a once-per-account quest which has already been flagged as completed on a different character + or (not ATTAccountWideData.OneTimeQuests[questID] or ATTAccountWideData.OneTimeQuests[questID] == app.GUID) + ) + ) + -- If it is an item/cost and associated to an active quest. + -- TODO: add t.requiredForQuestID + or (not t.isWorldQuest and (t.cost or t.itemID) and C_QuestLog_IsOnQuest(questID)) + ) + ) + or + ( -- Locked Quests + app.CollectibleQuestsLocked + and + ( + -- not able to access quest on current character + t.locked + and + ( + -- debug/account mode + app.MODE_DEBUG_OR_ACCOUNT + or + -- available in party sync + not t.DisablePartySync + ) + ) + ) + ) end --- Achievement Criteria Lib -local function GetParentAchievementInfo(t, key) - local sourceAch = t.sourceParent or t.parent; - if sourceAch and sourceAch.achievementID == t.achievementID then - rawset(t, key, sourceAch[key]); - return rawget(t, key); +local function QuestConsideredSaved(questID) + if app.IsInPartySync then + return C_QuestLog_IsQuestReplayedRecently(questID) or (not C_QuestLog_IsQuestReplayable(questID) and IsQuestFlaggedCompleted(questID)); end + return IsQuestFlaggedCompleted(questID); end -local criteriaFields = { - ["key"] = function(t) - return "criteriaID"; - end, - ["achievementID"] = function(t) - local sourceAch = t.sourceParent or t.parent; - local achievementID = t.altAchID and app.FactionID == Enum.FlightPathFaction.Horde and t.altAchID or t.achID or (sourceAch and (sourceAch.achievementID or (sourceAch.parent and sourceAch.parent.achievementID))); - if achievementID then - rawset(t, "achievementID", achievementID); - return achievementID; +-- Given an Object, will return the indicator (asset name) if this Object should show one based on it being tied to a QuestID +app.GetQuestIndicator = function(t) + if t.questID then + if C_QuestLog_IsOnQuest(t.questID) then + return (C_QuestLog_ReadyForTurnIn(t.questID) and "Interface_Questin") + or "Interface_Questin_grey"; + elseif ATTAccountWideData.OneTimeQuests[t.questID] == false then + return "Interface_Quest_Arrow"; + end + end +end + +local criteriaFuncs = { + ["lvl"] = function(v) + return app.Level >= v; + end, + ["label_lvl"] = L["LOCK_CRITERIA_LEVEL_LABEL"], + ["text_lvl"] = function(v) + return v; + end, + + ["questID"] = QuestConsideredSaved, + ["label_questID"] = L["LOCK_CRITERIA_QUEST_LABEL"], + ["text_questID"] = function(v) + local questObj = app.SearchForObject("questID", v); + return sformat("[%d] %s", v, questObj and questObj.text or "???"); + end, + + ["spellID"] = function(v) + return IsSpellKnown(v) or app.CurrentCharacter.Spells[v]; + end, + ["label_spellID"] = L["LOCK_CRITERIA_SPELL_LABEL"], + ["text_spellID"] = function(v) + return select(1, GetSpellInfo(v)); + end, + + ["factionID"] = function(v) + -- v = factionID.standingRequiredToLock + local factionID = math_floor(v + 0.00001); + local lockStanding = math_floor((v - factionID) * 10 + 0.00001); + local standing = select(3, GetFactionInfoByID(factionID)) or 1; + -- app.PrintDebug(sformat("Check Faction %s Standing (%d) is locked @ (%d)", factionID, standing, lockStanding)) + return standing >= lockStanding; + end, + ["label_factionID"] = L["LOCK_CRITERIA_FACTION_LABEL"], + ["text_factionID"] = function(v) + -- v = factionID.standingRequiredToLock + local factionID = math_floor(v + 0.00001); + local lockStanding = math_floor((v - factionID) * 10 + 0.00001); + local name, _, standing = GetFactionInfoByID(factionID); + return sformat(L["LOCK_CRITERIA_FACTION_FORMAT"], app.GetFactionStandingText(lockStanding), name, app.GetFactionStandingText(standing)); + end, +}; +app.QuestLockCriteriaFunctions = criteriaFuncs; +local function LockedAsQuest(t) + local questID = t.questID; + if not IsQuestFlaggedCompleted(questID) then + local lockCriteria = t.lc; + if lockCriteria then + local criteriaRequired = lockCriteria[1]; + local critKey, critFunc, nonQuestLock; + local i, limit = 2, #lockCriteria; + while i < limit do + critKey = lockCriteria[i]; + critFunc = criteriaFuncs[critKey]; + i = i + 1; + if critFunc then + if critFunc(lockCriteria[i]) then + criteriaRequired = criteriaRequired - 1; + if not nonQuestLock and critKey ~= "questID" then + nonQuestLock = true; + end + end + else + app.print("Unknown 'lockCriteria' key:",critKey,lockCriteria[i]); + end + -- enough criteria met to consider this quest locked + if criteriaRequired <= 0 then + -- we can rawset this since there's no real way for a player to 'remove' this lock during a session + -- and this does not come into play during party sync + rawset(t, "locked", true); + -- if this was locked due to something other than a Quest specifically, indicate it cannot be done in Party Sync + if nonQuestLock then + -- app.PrintDebug("Automatic DisablePartySync", app:Linkify(questID, app.Colors.ChatLink, "search:questID:" .. questID)) + rawset(t, "DisablePartySync", true); + end + return true; + end + i = i + 1; + end + end + -- if an alt-quest is completed, then this quest is locked + if t.altcollected then + rawset(t, "locked", t.altcollected); + return true; + end + -- determine if a 'nextQuest' exists and is completed specifically by this character, to remove availability of the breadcrumb + if t.isBreadcrumb and t.nextQuests then + local nq; + for _,questID in ipairs(t.nextQuests) do + if IsQuestFlaggedCompleted(questID) then + rawset(t, "locked", questID); + return questID; + else + -- this questID may not even be available to pick up, so try to find an object with this questID to determine if the object is complete + nq = app.SearchForObject("questID", questID); + if nq and (IsQuestFlaggedCompleted(nq.questID) or nq.altcollected or nq.locked) then + rawset(t, "locked", questID); + return questID; + end + end + end end + end + -- rawset means that this will persist as a non-locked quest until reload, so quests that become locked while playing will not immediately update + -- maybe can revise that somehow without also having this entire logic be calculated billions of times when nothing changes.... + rawset(t, "locked", false); +end +app.LockedAsQuest = LockedAsQuest; + +local questFields = { + ["key"] = function(t) + return "questID"; end, ["name"] = function(t) - if t.link then return t.link; end - if t.encounterID then - return select(1, EJ_GetEncounterInfo(t.encounterID)); - end - if t.achievementID then - local m = GetAchievementNumCriteria(t.achievementID); - if m and t.criteriaID <= m then - return GetAchievementCriteriaInfo(t.achievementID, t.criteriaID, true); + return QuestTitleFromID[t.questID]; + end, + ["objectiveInfo"] = function(t) + local questID = t.questID; + if questID then + local objectives = C_QuestLog_GetQuestObjectives(questID); + if objectives then + rawset(t, "objectiveInfo", objectives); + return objectives; end end - return L["WRONG_FACTION"]; + rawset(t, "objectiveInfo", app.EmptyTable) end, ["description"] = function(t) - if t.encounterID then - return select(2, EJ_GetEncounterInfo(t.encounterID)); + -- Provide a fall-back description as to collectibility of a Quest due to granting reputation + if app.CollectibleReputations and t.maxReputation and t.collectibleAsReputation then + local factionID = t.maxReputation[1]; + return L["ITEM_GIVES_REP"] .. (select(1, GetFactionInfoByID(factionID)) or ("Faction #" .. tostring(factionID))) .. "'"; end end, - ["link"] = function(t) - if t.itemID then - local _, link, _, _, _, _, _, _, _, icon = GetItemInfo(t.itemID); - if link then - t.text = link; - t.link = link; - t.icon = icon; - return link; + ["icon"] = function(t) + if t.providers then + for k,v in ipairs(t.providers) do + if v[2] > 0 then + if v[1] == "o" then + return app.ObjectIcons[v[2]] or "Interface\\Icons\\INV_Misc_Bag_10"; + elseif v[1] == "i" then + return select(5, GetItemInfoInstant(v[2])) or "Interface\\Icons\\INV_Misc_Book_09"; + end + end end end + if t.isWorldQuest then + return "Interface\\AddOns\\AllTheThings\\assets\\Interface_Questind"; + elseif t.repeatable then + return "Interface\\AddOns\\AllTheThings\\assets\\Interface_Questd"; + elseif t._missing then + return "Interface\\Icons\\INV_Misc_QuestionMark"; + else + return "Interface\\AddOns\\AllTheThings\\assets\\Interface_Quest"; + end end, - ["displayID"] = function(t) - if t.encounterID then - -- local id, name, description, displayInfo, iconImage = EJ_GetCreatureInfo(1, t.encounterID); - return select(4, EJ_GetCreatureInfo(t.index, t.encounterID)); - end - end, - ["displayInfo"] = function(t) - if t.encounterID then - local displayInfos, displayInfo = {}; - for i=1,MAX_CREATURES_PER_ENCOUNTER do - displayInfo = select(4, EJ_GetCreatureInfo(i, t.encounterID)); - if displayInfo then - tinsert(displayInfos, displayInfo); - else - break; + ["model"] = function(t) + if t.providers then + for k,v in ipairs(t.providers) do + if v[2] > 0 then + if v[1] == "o" then + return app.ObjectModels[v[2]]; + end end end - return displayInfos; end end, + ["link"] = function(t) + return GetQuestLink(t.questID) or "quest:" .. t.questID; + end, + ["repeatable"] = function(t) + return rawget(t, "isDaily") or rawget(t, "isWeekly") or rawget(t, "isMonthly") or rawget(t, "isYearly") or rawget(t, "isWorldQuest"); + end, + ["collectible"] = app.CollectibleAsQuest, + ["collected"] = IsQuestFlaggedCompletedForObject, ["trackable"] = app.ReturnTrue, - ["collected"] = function(t) - local achievementID = t.achievementID; - if achievementID then - if app.CurrentCharacter.Achievements[achievementID] then return 1; end - if app.AccountWideAchievements and ATTAccountWideData.Achievements[achievementID] then return 2; end - if t.criteriaID and t.criteriaID <= (GetAchievementNumCriteria(achievementID) or -1) then - return select(3, GetAchievementCriteriaInfo(achievementID, t.criteriaID, true)); + ["saved"] = function(t) + return QuestConsideredSaved(t.questID); + end, + ["indicatorIcon"] = app.GetQuestIndicator, + ["collectibleAsReputation"] = function(t) + local factionID = t.maxReputation[1]; + -- If Collectible by providing reputation towards a Faction with which the character is below the rep-granting Standing + -- and the Faction itself is Collectible & Not Collected + -- and the Quest is not locked from being completed + if app.CollectibleReputations and not t.locked then + local factionRef = app.SearchForObject("factionID", factionID); + if factionRef and not factionRef.collected and (select(6, GetFactionInfoByID(factionID)) or 0) < t.maxReputation[2] then + return true; end end + -- If Collectible by being a Quest + if app.CollectibleQuests or app.CollectibleQuestsLocked then + return app.CollectibleAsQuest(t); + end end, - ["saved"] = function(t) - local achievementID = t.achievementID; - if achievementID then - if app.CurrentCharacter.Achievements[achievementID] then return true; end - if t.criteriaID and t.criteriaID <= (GetAchievementNumCriteria(achievementID) or -1) then - return select(3, GetAchievementCriteriaInfo(achievementID, t.criteriaID, true)); + ["collectedAsReputation"] = function(t) + -- If the Quest is completed on this character, then it doesn't matter about the faction + if IsQuestFlaggedCompleted(t.questID) then + return 1; + end + -- Check whether this Quest can provide Rep towards an incomplete Faction + if app.CollectibleReputations and t.maxReputation then + local factionID = t.maxReputation[1]; + local factionRef = app.SearchForObject("factionID", factionID); + -- Completing the quest will increase the Faction, so it is incomplete + if factionRef and not factionRef.collected and (select(6, GetFactionInfoByID(factionID)) or 0) < t.maxReputation[2] then + return false; + elseif not app.CollectibleQuests and not app.CollectibleQuestsLocked then + -- Completing the quest will not increase the Faction, but User doesn't care about Quests, then consider it 'collected' + return 2; end end + -- Finally, check if the quest is otherwise considered 'collected' by normal logic + return IsQuestFlaggedCompletedForObject(t); end, - ["index"] = function(t) - return 1; - end, - -- Use parent achievement if info not listed directly in the criteria - ["c"] = function(t) - return GetParentAchievementInfo(t, "c"); - end, - ["classID"] = function(t) - return GetParentAchievementInfo(t, "classID"); - end, - ["races"] = function(t) - return GetParentAchievementInfo(t, "races"); + ["altcollected"] = function(t) + -- determine if an altQuest is considered completed for this quest for this character + if t.altQuests then + for _,questID in ipairs(t.altQuests) do + if IsQuestFlaggedCompleted(questID) then + -- if LOG then print(LOG,"altCollected by",questID) end + rawset(t, "altcollected", questID); + return questID; + end + end + end end, - ["r"] = function(t) - return GetParentAchievementInfo(t, "r"); + ["missingSourceQuests"] = function(t) + if t.sourceQuests and #t.sourceQuests > 0 then + local includeBreadcrumbs = app.Settings:Get("Thing:QuestsLocked"); + local sq; + for _,sourceQuestID in ipairs(t.sourceQuests) do + if not IsQuestFlaggedCompleted(sourceQuestID) then + -- consider the breadcrumb as an actual sq since the user is tracking them + if includeBreadcrumbs then + return true; + -- otherwise incomplete breadcrumbs will not prevent picking up a quest if they are ignored + else + sq = app.SearchForObject("questID", sourceQuestID); + if sq and not sq.isBreadcrumb and not (sq.locked or sq.altcollected) then + return true; + end + end + end + end + end end, + ["locked"] = LockedAsQuest, }; -criteriaFields.collectible = fields.collectible; -criteriaFields.icon = fields.icon; -app.BaseAchievementCriteria = app.BaseObjectFields(criteriaFields, "BaseAchievementCriteria"); -app.CreateAchievementCriteria = function(id, t) - return setmetatable(constructor(id, t, "criteriaID"), app.BaseAchievementCriteria); -end +app.BaseQuest = app.BaseObjectFields(questFields, "BaseQuest"); + +-- These are Items rewarded by WQs which are treated as currency +-- other Items which are 'costs' will not be excluded by the "WorldQuestsList:Currencies" setting +local WorldQuestCurrencyItems = { + [163036] = true, -- Polished Pet Charms + [116415] = true, -- Shiny Pet Charms +}; +-- Will attempt to populate the rewards of the quest object into itself (will become the object's OnUpdate until populated or 15 rendered frames) +app.TryPopulateQuestRewards = function(questObject) + if not questObject or not questObject.questID then return; end + if not questObject.OnUpdate then questObject.OnUpdate = app.TryPopulateQuestRewards; end + -- track how many attempts for retrieving reward data for both types (15 frames) + questObject.missingItem = questObject.missingItem and (questObject.missingItem - 1) or 15; + questObject.missingCurr = questObject.missingCurr and (questObject.missingCurr - 1) or 15; -local HarvestedAchievementDatabase = {}; -local harvesterFields = RawCloneData(fields); -harvesterFields.visible = app.ReturnTrue; -harvesterFields.collectible = app.ReturnTrue; -harvesterFields.collected = app.ReturnFalse; -harvesterFields.text = function(t) - local achievementID = t.achievementID; - if achievementID then - local IDNumber, Name, Points, Completed, Month, Day, Year, Description, Flags, Image, RewardText, isGuildAch = GetAchievementInfo(achievementID); - if Name then - local info = { - ["name"] = Name, - ["achievementID"] = IDNumber, - ["parentCategoryID"] = GetAchievementCategory(achievementID) or -1, - ["icon"] = Image, - }; - if Description ~= nil and Description ~= "" then - info.description = Description; - end - local totalCriteria = GetAchievementNumCriteria(achievementID); - if totalCriteria > 0 then - local criteria = {}; - for criteriaID=totalCriteria,1,-1 do - local criteriaString, criteriaType, completed, quantity, reqQuantity, charName, flags, assetID, quantityString = GetAchievementCriteriaInfo(achievementID, criteriaID); - local crit = { ["criteriaID"] = criteriaID }; - if criteriaString ~= nil and criteriaString ~= "" then - crit.name = criteriaString; - end - if assetID and assetID ~= 0 then - crit.assetID = assetID; - end - if reqQuantity and reqQuantity > 0 then - crit.rank = reqQuantity; - end - if criteriaType then - -- Unknown type, not sure what to do with this. - crit.criteriaType = criteriaType; - if crit.assetID then - if criteriaType == 27 then -- Quest Completion - crit.sourceQuest = assetID; - crit.criteriaType = nil; - crit.assetID = nil; - if crit.rank and crit.rank == 1 then - crit.rank = nil; - break; + -- all sub-group data will be populated via this method, so any pre-existing stuff should be cleaned out on the initial setup + if questObject.missingItem == 15 and questObject.missingCurr == 15 then + questObject.g = nil; + end + + -- app.DEBUG_PRINT = questObject.questID == 61949 and 61949; + -- if app.DEBUG_PRINT then print("TryPopulateQuestRewards",questObject.questID) end + -- print("TryPopulateQuestRewards",questObject.questID,questObject.missingItem,questObject.missingCurr) + if questObject.missingItem > 0 then + -- Get reward info + local numQuestRewards = GetNumQuestLogRewards(questObject.questID); + local skipCollectibleCurrencies = not app.Settings:GetTooltipSetting("WorldQuestsList:Currencies"); + -- numQuestRewards will often be 0 for fresh questID API calls... + -- pre-emptively call the following API method as well to get cached data earlier for the next refresh + GetQuestLogRewardInfo(1, questObject.questID); + -- if app.DEBUG_PRINT then print("TryPopulateQuestRewards:numQuestRewards",questObject.questID,numQuestRewards,questObject.missingItem) end + for j=1,numQuestRewards,1 do + local _, _, _, _, _, itemID = GetQuestLogRewardInfo(j, questObject.questID); + if itemID then + -- if app.DEBUG_PRINT then print("TryPopulateQuestRewards:found",questObject.questID,itemID) end + + QuestHarvester.AllTheThingsProcessing = true; + QuestHarvester:SetQuestLogItem("reward", j, questObject.questID); + local link = select(2, QuestHarvester:GetItem()); + QuestHarvester.AllTheThingsProcessing = false; + QuestHarvester:Hide(); + if link then + local item = {}; + app.ImportRawLink(item, link); + -- if item.itemID == 137483 then + -- app.DEBUG_PRINT = item.itemID; + -- print("item.initial parse") + -- end + -- if app.DEBUG_PRINT then print("Parse Link", link) end + if item.itemID then + local search = SearchForLink(link); + -- search will either match through bonusID, modID, or itemID in that priority + + -- put all the item information into a basic table + -- if app.DEBUG_PRINT then print("Base Item") app.PrintTable(item) end + -- block the group from being collectible as a cost if the option is not enabled for various 'currency' items + if skipCollectibleCurrencies and WorldQuestCurrencyItems[item.itemID] then + item.collectibleAsCost = false; + end + if search then + -- if app.DEBUG_PRINT then print("Initial search",#search,link) end + -- find the specific item which the link represents (not sure all of this is necessary with improved search) + local exactItemID = GetGroupItemIDWithModID(item); + local subItems = {}; + local refinedMatches = app.GroupBestMatchingItems(search, exactItemID); + if refinedMatches then + -- move from depth 3 to depth 1 to find the set of items which best matches for the root + for depth=3,1,-1 do + if refinedMatches[depth] then + -- if app.DEBUG_PRINT then print("refined",depth,#refinedMatches[depth]) end + for _,o in ipairs(refinedMatches[depth]) do + MergeProperties(item, o, true); + NestObjects(item, o.g); -- no clone since item is cloned later + end + end end - elseif criteriaType == 36 or criteriaType == 41 or criteriaType == 42 then - -- 36: Items (Generic) - -- 41: Items (Use/Eat) - -- 42: Items (Loot) - if crit.rank and crit.rank < 2 then - crit.provider = { "i", crit.assetID }; - else - crit.cost = { { "i", crit.assetID, crit.rank }}; + -- any matches with depth 0 will be nested + if refinedMatches[0] then + -- if app.DEBUG_PRINT then print("refined",0,#refinedMatches[0]) end + app.ArrayAppend(subItems, refinedMatches[0]); -- no clone since item is cloned later end - crit.criteriaType = nil; - crit.assetID = nil; - crit.rank = nil; - elseif criteriaType == 43 then -- Exploration?! - crit.explorationID = crit.assetID; - crit.criteriaType = nil; - crit.assetID = nil; - crit.rank = nil; - elseif criteriaType == 0 then -- NPC Kills - crit.provider = { "n", crit.assetID }; - if crit.rank and crit.rank < 2 then - crit.rank = nil; - end - crit.criteriaType = nil; - crit.assetID = nil; - elseif criteriaType == 96 then -- Collect Pets - crit.provider = { "n", crit.assetID }; - if crit.rank and crit.rank < 2 then - crit.rank = nil; - end - crit.criteriaType = nil; - crit.assetID = nil; - elseif criteriaType == 68 or criteriaType == 72 then -- Interact with Object (68) / Fish from a School (72) - crit.provider = { "o", crit.assetID }; - if crit.rank and crit.rank < 2 then - crit.rank = nil; - end - crit.criteriaType = nil; - crit.assetID = nil; - elseif criteriaType == 7 then -- Skill ID, Rank is Requirement - crit.requireSkill = crit.assetID; - crit.criteriaType = nil; - crit.assetID = nil; - elseif criteriaType == 40 then -- Skill ID Learned - crit.requireSkill = crit.assetID; - crit.criteriaType = nil; - crit.assetID = nil; - crit.rank = nil; - elseif criteriaType == 8 then -- Achievements as Children - crit.provider = { "a", crit.assetID }; - if crit.rank and crit.rank < 2 then - crit.rank = nil; - end - crit.criteriaType = nil; - crit.assetID = nil; - elseif criteriaType == 12 then -- Currencies (Collected Total) - if crit.rank and crit.rank < 2 then - crit.cost = { { "c", crit.assetID, 1 }}; - else - crit.cost = { { "c", crit.assetID, crit.rank }}; - end - crit.criteriaType = nil; - crit.assetID = nil; - crit.rank = nil; - elseif criteriaType == 26 then - -- 26: Environmental Deaths - -- 0: fatigue - -- 1: drowning - -- 2: falling - -- 3/5: fire/lava - -- https://wowwiki-archive.fandom.com/wiki/API_GetAchievementCriteriaInfo - if crit.rank and totalCriteria == 1 then - info.rank = crit.rank; - break; - end - elseif criteriaType == 29 or criteriaType == 69 then -- Cast X Spell Y Times - if crit.rank and totalCriteria == 1 then - info.rank = crit.rank; - break; - else - crit.spellID = crit.assetID; - crit.criteriaType = nil; - crit.assetID = nil; - end - elseif criteriaType == 46 then -- Minimum Faction Requirement - crit.minReputation = { crit.assetID, crit.rank }; - crit.criteriaType = nil; - crit.assetID = nil; - crit.rank = nil; - end - -- 28: Something to do with event-based encounters, not sure what assetID is. - -- 49: Something to do with Equipment Slots, assetID is the equipSlotID. (useless maybe?) - -- 52: Honorable kill on a specific Class, assetID is the ClassID. (useless maybe? might be able to use a class icon?) - -- 53: Honorable kill on a specific Class at level 35+, assetID is the ClassID. (useless maybe? might be able to use a class icon?) - -- 54: Show a critter you /love them, assetID is useless or not present. - -- 70: Honorable Kill at a specific place. - -- 71: Instance Clears, assetID is of an unknown type... might be Saved Instance ID? - -- 73: Mal'Ganis? Complete Objective? (useless) - -- 74: No idea, tracking of some kind - -- 92: Encounter Kills, of non-NPC type. (Group of NPCs - IE: Lilian Voss) - elseif criteriaType == 0 or criteriaType == 3 or criteriaType == 5 or criteriaType == 6 or criteriaType == 9 or criteriaType == 10 or criteriaType == 14 or criteriaType == 15 or criteriaType == 17 or criteriaType == 19 or criteriaType == 26 or criteriaType == 37 or criteriaType == 45 or criteriaType == 75 or criteriaType == 78 or criteriaType == 79 or criteriaType == 81 or criteriaType == 90 or criteriaType == 91 or criteriaType == 109 or criteriaType == 124 or criteriaType == 126 or criteriaType == 130 or criteriaType == 134 or criteriaType == 135 or criteriaType == 136 or criteriaType == 138 or criteriaType == 139 or criteriaType == 151 or criteriaType == 156 or criteriaType == 157 or criteriaType == 158 or criteriaType == 200 or criteriaType == 203 or criteriaType == 207 then - -- 0: Some tracking statistic, generally X/Y format and simple enough to not justify a type if no assetID is present. - -- 3: Collect X of something that's generic for Archeology - -- 5: Level Requirement - -- 6: Digsites (Archeology) - -- 9: Total Quests Completed - -- 10: Daily Quests, every day for X days. - -- 14: Total Daily Quests Completed - -- 15: Battleground battles - -- 17: Total Deaths - -- 19: Instances Run - -- 26: Environmental Deaths - -- 37: Ranked Arena Wins - -- 45: Bank Slots Purchased - -- 75: Mounts (Total - on one Character) - -- 78: Kill NPCs - -- 79: Cook Food - -- 81: Pet battle achievement points - -- 90: Gathering (Nodes) - -- 91: Pet Charm Totals - -- 109: Catch Fish - -- 124: Guild Member Repairs - -- 126: Guild Crafting - -- 130: Rated Battleground Wins - -- 134: Complete Quests - -- 135: Honorable Kills (Total) - -- 136: Kill Critters - -- 138: Guild Scenario Challenges Completed - -- 139: Guild Challenges Completed - -- 151: Guild Scenario Completed - -- 156: Collect Pets (Total) - -- 157: Collect Pets (Rare) - -- 158: Pet Battles - -- 200: Recruit Troops - -- 203: World Quests (Total Complete) - -- 207: Honor Earned (Total) - -- https://wowwiki-archive.fandom.com/wiki/API_GetAchievementCriteriaInfo - if crit.rank and totalCriteria == 1 then - info.rank = crit.rank; - break; - end - elseif criteriaType == 38 or criteriaType == 39 or criteriaType == 58 or criteriaType == 63 or criteriaType == 65 or criteriaType == 66 or criteriaType == 76 or criteriaType == 77 or criteriaType == 82 or criteriaType == 83 or criteriaType == 84 or criteriaType == 85 or criteriaType == 86 or criteriaType == 107 or criteriaType == 128 or criteriaType == 152 or criteriaType == 153 or criteriaType == 163 then -- Ignored - -- 38: Team Rating, which is irrelevant. - -- 39: Personal Rating, which is irrelevant. - -- 58: Killing Blows, might specifically be PvP. - -- 63: Total Gold (Spent on Travel) - -- 65: Total Gold (Spent on Barber Shop) - -- 66: Total Gold (Spent on Mail) - -- 76: Duels Won - -- 77: Duels Lost - -- 82: Auctions (Total Posted) - -- 83: Auctions (Highest Bid) - -- 84: Auctions (Total Purchases) - -- 85: Auctions (Highest Sold)] - -- 86: Most Gold Ever Owned - -- 107: Quests Abandoned - -- 128: Guild Bank Tabs - -- 152: Defeat Scenarios - -- 153: Ride to Location? - -- 163: Also ride to location - break; - elseif criteriaType == 59 or criteriaType == 62 or criteriaType == 67 or criteriaType == 80 then -- Gold Cost, if available. - -- 59: Total Gold (Vendors) - -- 62: Total Gold (Quest Rewards) - -- 67: Total Gold (Looted) - -- 80: Total Gold (Auctions) - if crit.rank and crit.rank > 1 then - if totalCriteria == 1 then - -- Generic, such as the Bread Winner - info.rank = crit.rank; - break; - else - crit.cost = { { "g", crit.assetID, crit.rank } }; - crit.criteriaType = nil; - crit.assetID = nil; - info.rank = nil; - end - else - break; end + -- then pull in any other sub-items which were not the item itself + NestObjects(item, subItems); -- no clone since item is cloned later end - -- 155: Collect Battle Pets from a Raid, no assetID though RIP - -- 158: Defeat Master Trainers - -- 161: Capture a Battle Pet in a Zone - -- 163: Defeat an Encounter of some kind? AssetID useless - -- 169: Construct a building, assetID might be the buildingID. + + -- at least one reward exists, so clear the missing data + questObject.missingItem = 0; + -- don't let cached groups pollute potentially inaccurate raw Data + item.link = nil; + -- if app.DEBUG_PRINT then print("Final Item") app.PrintTable(item) end + NestObject(questObject, item, true); end - tinsert(criteria, 1, crit); + -- if app.DEBUG_PRINT then app.DEBUG_PRINT = nil; end end - if #criteria > 0 then info.criteria = criteria; end end - - HarvestedAchievementDatabase[achievementID] = info; - AllTheThingsHarvestItems = HarvestedAchievementDatabase; - setmetatable(t, app.BaseAchievement); - rawset(t, "collected", true); - return link; end end - local name = t.name; - -- retries exceeded, so check the raw .name on the group (gets assigned when retries exceeded during cache attempt) - if name then rawset(t, "collected", true); end - return name; -end -app.BaseAchievementHarvester = app.BaseObjectFields(harvesterFields, "BaseAchievementHarvester"); -app.CreateAchievementHarvester = function(id, t) - return setmetatable(constructor(id, t, "achievementID"), app.BaseAchievementHarvester); -end + -- Add info for currency rewards as containers for their respective collectibles + if questObject.missingCurr > 0 then + local numCurrencies = GetNumQuestLogRewardCurrencies(questObject.questID); + local skipCollectibleCurrencies = not app.Settings:GetTooltipSetting("WorldQuestsList:Currencies"); + -- pre-emptively call the following API method as well to get cached data earlier for the next refresh + GetQuestLogRewardCurrencyInfo(1, questObject.questID); + -- numCurrencies will often be 0 for fresh questID API calls... + local currencyID; + for j=1,numCurrencies,1 do + currencyID = select(4, GetQuestLogRewardCurrencyInfo(j, questObject.questID)); + if currencyID then + -- if app.DEBUG_PRINT then print("TryPopulateQuestRewards_currencies:found",questObject.questID,currencyID,questObject.missingCurr) end -local function CheckAchievementCollectionStatus(achievementID) - if ATTAccountWideData then - local id,name,_,accCompleted,_,_,_,_,flags,_,_,isGuild = GetAchievementInfo(achievementID) - if id and not isGuild and accCompleted and bit.band(flags,0x1) == 0 then - ATTAccountWideData.Achievements[id] = 1; - end - end -end -RefreshAchievementCollection = function() - if ATTAccountWideData then - local maxid, achID = 0; - for achievementID,_ in pairs(fieldCache["achievementID"]) do - achID = tonumber(achievementID); - if achID > maxid then maxid = achID; end - end - for achievementID=maxid,1,-1 do - CheckAchievementCollectionStatus(achievementID); + currencyID = tonumber(currencyID); + local item = { ["currencyID"] = currencyID }; + -- block the group from being collectible as a cost if the option is not enabled + if skipCollectibleCurrencies then + item.collectibleAsCost = false; + end + _cache = SearchForField("currencyID", currencyID); + if _cache then + for _,data in ipairs(_cache) do + -- cache record is the item itself + if GroupMatchesParams(data, "currencyID", currencyID) then + MergeProperties(item, data); + -- cache record is associated with the item + else + NestObject(item, data); -- no clone since item is cloned later + end + end + end + questObject.missingCurr = 0; + NestObject(questObject, item, true); + end end end -end -app:RegisterEvent("ACHIEVEMENT_EARNED"); -app.events.ACHIEVEMENT_EARNED = CheckAchievementCollectionStatus; -end)(); --- Artifact Lib -(function() -local artifactItemIDs = { - [841] = 133755, -- Underlight Angler [Base Skin] - [988] = 133755, -- Underlight Angler [Fisherfriend of the Isles] - [989] = 133755, -- Underlight Angler [Fisherfriend of the Isles] - [1] = {}, -- Off-Hand ItemIDs -}; -local fields = { - ["key"] = function(t) - return "artifactID"; - end, - ["artifactinfo"] = function(t) - --[[ - local setID, appearanceID, appearanceName, displayIndex, appearanceUnlocked, unlockConditionText, - uiCameraID, altHandUICameraID, swatchR, swatchG, swatchB, - modelAlpha, modelDesaturation, suppressGlobalAnim = C_ArtifactUI_GetAppearanceInfoByID(t.artifactID); - ]]-- - local info = { C_ArtifactUI_GetAppearanceInfoByID(t.artifactID) }; - rawset(t, "artifactinfo", info); - return info; - end, - ["f"] = function(t) - return 11; - end, - ["collectible"] = function(t) - return app.CollectibleTransmog; - end, - ["collected"] = function(t) - if ATTAccountWideData.Artifacts[t.artifactID] then return 1; end - -- This artifact is listed for the current class - if not GetRelativeField(t, "nmc", true) and select(5, C_ArtifactUI_GetAppearanceInfoByID(t.artifactID)) then - ATTAccountWideData.Artifacts[t.artifactID] = 1; - return 1; - end - end, - ["text"] = function(t) - if not t.artifactinfo then return RETRIEVING_DATA; end - -- Artifact listing in the Main item sets category just show 'Variant #' but elsewhere show the Item's name - if t.parent and t.parent.headerID and (t.parent.headerID <= -5200 and t.parent.headerID >= -5205) then - return t.variantText; - end - return t.appearanceText; - end, - ["title"] = function(t) - return t.variantText; - end, - ["variantText"] = function(t) - return Colorize("Variant " .. t.artifactinfo[4], RGBToHex(t.artifactinfo[9] * 255, t.artifactinfo[10] * 255, t.artifactinfo[11] * 255)); - end, - ["appearanceText"] = function(t) - return "|cffe6cc80" .. (t.artifactinfo[3] or "???") .. "|r"; - end, - ["description"] = function(t) - return t.artifactinfo[6] or L["ARTIFACT_INTRO_REWARD"]; - end, - ["atlas"] = function(t) - return "Forge-ColorSwatchBorder"; - end, - ["atlas-background"] = function(t) - return "Forge-ColorSwatchBackground"; - end, - ["atlas-border"] = function(t) - return "Forge-ColorSwatch"; - end, - ["atlas-color"] = function(t) - return { t.artifactinfo[9], t.artifactinfo[10], t.artifactinfo[11], 1.0 }; - end, - ["model"] = function(t) - return t.parent and GetRelativeValue(t.parent, "model"); - end, - ["modelScale"] = function(t) - return t.parent and GetRelativeValue(t.parent, "modelScale") or 0.95; - end, - ["modelRotation"] = function(t) - return t.parent and GetRelativeValue(t.parent, "modelRotation") or 45; - end, - ["silentLink"] = function(t) - local itemID = t.silentItemID; - if itemID then - -- 1 -> Off-Hand Appearance - -- 2 -> Main-Hand Appearance - -- return select(2, GetItemInfo(sformat("item:%d::::::::%d:::11:::8:%d:", itemID, app.Level, t.artifactID))); - -- local link = sformat("item:%d::::::::%d:::11::%d:8:%d:", itemID, app.Level, t.isOffHand and 1 or 2, t.artifactID); - -- print("Artifact link",t.artifactID,itemID,link); - return select(2, GetItemInfo(sformat("item:%d:::::::::::11::%d:8:%d:", itemID, t.isOffHand and 1 or 2, t.artifactID))); - end - end, - ["silentItemID"] = function(t) - local itemID; - if t.isOffHand then - itemID = artifactItemIDs[1][t.artifactID]; - else - itemID = artifactItemIDs[t.artifactID]; - end - if itemID then - return itemID; - elseif t.parent and t.parent.headerID and (t.parent.headerID <= -5200 and t.parent.headerID >= -5205) then - itemID = GetRelativeValue(t.parent, "itemID"); - -- Store the relative ItemID in the artifactItemID cache so it can be referenced accurately by artifacts sourced in specific locations - if itemID then - if t.isOffHand then - artifactItemIDs[1][t.artifactID] = itemID; - else - artifactItemIDs[t.artifactID] = itemID; + -- done attempting to populate the quest object + if questObject.missingItem < 1 and questObject.missingCurr < 1 then + -- if app.DEBUG_PRINT then print("TryPopulateQuestRewards:populated",questObject.questID) end + questObject.OnUpdate = nil; + questObject.doUpdate = true; + + -- Troublesome scenarios to test when changing this logic: + -- BFA emissaries + -- BFA Azerite armor caches + -- Argus Rare WQ's + Rare Alt quest + + -- Finally ensure that any cached entries for the quest are copied into this version of the object + -- Needs to be SearchForField as non-quests can be pulled too + local cachedQuests = SearchForField("questID", questObject.questID); + if cachedQuests then + -- special care for API provided items + local apiItems = {}; + if questObject.g then + for _,item in ipairs(questObject.g) do + if item.itemID then + apiItems[item.itemID] = item; + end end - -- print("Artifact ItemID Cached",t.artifactID,t.isOffHand,itemID) end - return itemID; - end - end, - ["s"] = function(t) - -- Return the calculated 's' field if existing - if t._s then return t._s; end - local s = t.silentLink; - if s then - s = app.GetSourceID(s); - -- print("Artifact Source",s,t.silentLink) - if s and s > 0 then - rawset(t, "_s", s); - if ATTAccountWideData.Sources[s] ~= 1 and C_TransmogCollection_PlayerHasTransmogItemModifiedAppearance(s) then - -- print("Saved Known Source",s) - ATTAccountWideData.Sources[s] = 1; + local nonItemNested = {}; + -- merge in any DB data without replacing existing data + for _,data in ipairs(cachedQuests) do + -- only merge into the quest object properties from an object in cache with this questID + if data.questID and data.questID == questObject.questID then + MergeProperties(questObject, data, true); + -- need to exclusively copy cached values for certain fields since normal merge logic will not copy them + -- ref: quest 49675/58703 + if data.u then questObject.u = data.u; end + -- merge in sourced things under this quest object + if data.g then + for _,o in ipairs(data.g) do + -- nest cached non-items + if not o.itemID then + -- if app.DEBUG_PRINT then print("nested-nonItem",o.hash) end + tinsert(nonItemNested, o); + -- cached items need to merge with corresponding API item based on simple itemID + elseif apiItems[o.itemID] then + -- if app.DEBUG_PRINT then print("nested-merged",o.hash) end + MergeProperties(apiItems[o.itemID], o, true); + -- if it is not a WQ or is a 'raid' (world boss) + elseif questObject.isRaid or not questObject.isWorldQuest then + -- otherwise just get nested + -- if app.DEBUG_PRINT then print("nested-item",o.hash) end + tinsert(nonItemNested, o); + end + end + end + -- otherwise if this is a non-quest object flagged with this questID so it should be added under the quest + elseif data.key ~= "questID" then + tinsert(nonItemNested, data); end - return s; end + NestObjects(questObject, nonItemNested, true); end - end, -}; -app.BaseArtifact = app.BaseObjectFields(fields, "BaseArtifact"); -app.CreateArtifact = function(id, t) - return setmetatable(constructor(id, t, "artifactID"), app.BaseArtifact); -end -end)(); --- Azerite Essence Lib -(function() -local fields = { - ["key"] = function(t) - return "azeriteEssenceID"; - end, - ["info"] = function(t) - return C_AzeriteEssence.GetEssenceInfo(t.azeriteEssenceID) or {}; - end, - ["collectible"] = function(t) - return app.CollectibleAzeriteEssences; - end, - ["collected"] = function(t) - if (app.CurrentCharacter.AzeriteEssenceRanks[t.azeriteEssenceID] or 0) >= t.rank then - return 1; - end + -- Check for provider info + -- TODO: don't think this is necessary? it's trying to pull things listed under a creature when that creature is sourced under a 'Rares' header + -- if questObject.qgs and #questObject.qgs == 1 then + -- _cache = SearchForField("creatureID", questObject.qgs[1]); + -- if _cache then + -- for _,data in ipairs(_cache) do + -- if GetRelativeField(data, "headerID", -16) then -- Rares only! + -- print("merge creature data",data.hash,"=>",questObject.questID) + -- MergeProperties(questObject, data, true); + -- NestObjects(questObject, data.g, true); + -- end + -- end + -- end + -- end - local accountRank = ATTAccountWideData.AzeriteEssenceRanks[t.azeriteEssenceID] or 0; - local info = t.info; - if info and info.unlocked then - if t.rank and info.rank then - if info.rank >= t.rank then - app.CurrentCharacter.AzeriteEssenceRanks[t.azeriteEssenceID] = info.rank; - if info.rank > accountRank then ATTAccountWideData.AzeriteEssenceRanks[t.azeriteEssenceID] = info.rank; end - return 1; + -- Build out purchases if specified + -- if app.Settings:GetTooltipSetting("WorldQuestsList:Currencies") then + -- FillPurchases(questObject); + -- end + + -- Resolve all symbolic links now that the quest contains items + FillSymLinks(questObject, true); + + -- Special logic for Torn Invitation... maybe can clean up sometime + if questObject.g and #questObject.g > 0 then + for _,item in ipairs(questObject.g) do + if item.g then + for k,o in ipairs(item.g) do + if o.itemID == 140495 then -- Torn Invitation + local searchResults = app.SearchForField("questID", 44058); -- Volpin the Elusive + NestObjects(o, searchResults, true); + end + end end - else - return 1; end end + BuildGroups(questObject, questObject.g); + else + -- print("set questObject.doUpdate",questObject.questID) + questObject.doUpdate = true; + end - if app.AccountWideAzeriteEssences and accountRank >= t.rank then - return 2; - end - end, - ["text"] = function(t) - return t.link; - end, - ["lvl"] = function(t) - return 50; - end, - ["icon"] = function(t) - return t.info.icon or "Interface/ICONS/INV_Glowing Azerite Spire"; - end, - ["name"] = function(t) - return t.info.name; - end, - ["link"] = function(t) - return C_AzeriteEssence.GetEssenceHyperlink(t.azeriteEssenceID, t.rank); - end, - ["rank"] = function(t) - return t.info.rank or 0; - end, -}; -app.BaseAzeriteEssence = app.BaseObjectFields(fields, "BaseAzeriteEssence"); -app.CreateAzeriteEssence = function(id, t) - return setmetatable(constructor(id, t, "azeriteEssenceID"), app.BaseAzeriteEssence); + -- app.DEBUG_PRINT = nil; end -end)(); - --- Battle Pet Lib -(function() --- localized global APIs -local C_PetBattles_GetAbilityInfoByID = C_PetBattles.GetAbilityInfoByID; -local C_PetJournal_GetNumCollectedInfo = C_PetJournal.GetNumCollectedInfo; -local C_PetJournal_GetPetInfoByPetID = C_PetJournal.GetPetInfoByPetID; -local C_PetJournal_GetPetInfoBySpeciesID = C_PetJournal.GetPetInfoBySpeciesID; - -local cache = app.CreateCache("speciesID"); -local function CacheInfo(t, field) - local _t, id = cache.GetCached(t); - -- speciesName, speciesIcon, petType, companionID, tooltipSource, tooltipDescription, isWild, - -- canBattle, isTradeable, isUnique, obtainable, creatureDisplayID = C_PetJournal.GetPetInfoBySpeciesID(speciesID) - local speciesName, speciesIcon, petType, _, _, tooltipDescription, _, _, _, _, _, creatureDisplayID = C_PetJournal_GetPetInfoBySpeciesID(id); - if speciesName then - _t.name = speciesName; - _t.icon = speciesIcon; - _t.petTypeID = petType; - _t.lore = tooltipDescription; - _t.displayID = creatureDisplayID; - _t.text = "|cff0070dd"..speciesName.."|r"; - if field then return _t[field]; end +-- Will print a warning message and play a warning sound if the given QuestID being completed will prevent being able to complete a breadcrumb +-- (as far as ATT is capable of knowing) +app.CheckForBreadcrumbPrevention = function(title, questID) + local nextQuests = app.SearchForField("nextQuests", questID); + if nextQuests then + local warning; + for _,group in pairs(nextQuests) do + if not group.collected and app.RecursiveGroupRequirementsFilter(group) then + app.print(sformat(L["QUEST_PREVENTS_BREADCRUMB_COLLECTION_FORMAT"], title, app:Linkify(questID, app.Colors.ChatLink, "search:questID:"..questID), group.text or RETRIEVING_DATA, app:Linkify(group.questID, app.Colors.Locked, "search:questID:"..group.questID))); + warning = true; + end + end + if warning then app:PlayRemoveSound(); end end end -local function default_link(t) - if t.itemID then - return select(2, GetItemInfo(t.itemID)); + +local fields = RawCloneData(questFields); +fields.collectible = questFields.collectibleAsReputation; +fields.collected = questFields.collectedAsReputation; +app.BaseQuestWithReputation = app.BaseObjectFields(fields, "BaseQuestWithReputation"); +app.CreateQuest = function(id, t) + if t and rawget(t, "maxReputation") then + return setmetatable(constructor(id, t, "questID"), app.BaseQuestWithReputation); end - return t.text; + return setmetatable(constructor(id, t, "questID"), app.BaseQuest); end -local CollectedSpeciesHelper = setmetatable({}, { - __index = function(t, key) - if C_PetJournal_GetNumCollectedInfo(key) > 0 then - rawset(t, key, 1); - return 1; +app.CreateQuestWithFactionData = function(t) + local questData = app.FactionID == Enum.FlightPathFaction.Horde and t.hqd or t.aqd; + for key,value in pairs(questData) do t[key] = value; end + return setmetatable(t, app.BaseQuest); +end +-- Causes a group to remain visible if it is replayable, regardless of collection status +app.ShowIfReplayableQuest = function(data) + data.visible = C_QuestLog_IsQuestReplayable(data.questID) or app.CollectedItemVisibilityFilter(data); + return true; +end +local function QueryCompletedQuests() + local t = CompletedQuests; + local freshCompletes = C_QuestLog_GetAllCompletedQuestIDs(); + -- print("total completed quests new/previous",#freshCompletes,rawget(t, "_TOTAL") or 0) + local oldReportSetting = app.Settings:GetTooltipSetting("Report:CompletedQuests"); + -- check if Blizzard is being dumb / should we print a summary instead of individual lines + local questDiff = #freshCompletes - (rawget(t, "_TOTAL") or 0); + if app.IsReady then + if oldReportSetting and questDiff > 50 then + print(questDiff,"Quests Completed"); + elseif oldReportSetting and questDiff < -50 then + print(questDiff,"Quests Unflagged"); end end -}); -local fields = { - ["key"] = function(t) - return "speciesID"; - end, - ["filterID"] = function(t) - return 101; - end, - ["collectible"] = function(t) - return app.CollectibleBattlePets; - end, - ["collected"] = function(t) - if CollectedSpeciesHelper[t.speciesID] then - return 1; - end - local altSpeciesID = t.altSpeciesID; - if altSpeciesID and CollectedSpeciesHelper[altSpeciesID]then - return 2; + questDiff = math.abs(questDiff); + if questDiff > 50 then + app.Settings:SetTooltipSetting("Report:CompletedQuests", false); + end + local completedKeys = {}; + -- allow individual prints + for _,v in ipairs(freshCompletes) do + t[v] = true; + completedKeys[v] = true; + end + -- check for 'unflagged' questIDs (this seems to basically not impact lag at all... i hope) + for q,_ in pairs(t) do + if not completedKeys[q] and q ~= "_TOTAL" then + t[q] = nil; -- delete the key + t[q] = false; -- trigger the metatable function end - end, - ["text"] = function(t) - return cache.GetCachedField(t, "text", CacheInfo); - end, - ["icon"] = function(t) - return cache.GetCachedField(t, "icon", CacheInfo); - end, - ["lore"] = function(t) - return cache.GetCachedField(t, "lore", CacheInfo); - end, - ["displayID"] = function(t) - return cache.GetCachedField(t, "displayID", CacheInfo); - end, - ["petTypeID"] = function(t) - return cache.GetCachedField(t, "petTypeID", CacheInfo); - end, - ["name"] = function(t) - return cache.GetCachedField(t, "name", CacheInfo); - end, - ["link"] = function(t) - return cache.GetCachedField(t, "link", default_link); - end, - ["tsm"] = function(t) - return sformat("p:%d:1:3", t.speciesID); - end, -}; -app.BaseSpecies = app.BaseObjectFields(fields, "BaseSpecies"); -app.CreateSpecies = function(id, t) - return setmetatable(constructor(id, t, "speciesID"), app.BaseSpecies); + end + if questDiff > 50 then + app.Settings:SetTooltipSetting("Report:CompletedQuests", oldReportSetting); + end end - -app.events.NEW_PET_ADDED = function(petID) - local speciesID = select(1, C_PetJournal_GetPetInfoByPetID(petID)); - -- print("NEW_PET_ADDED", petID, speciesID); - if speciesID and C_PetJournal_GetNumCollectedInfo(speciesID) > 0 and not rawget(CollectedSpeciesHelper, speciesID) then - -- print("not already learned pet") - rawset(CollectedSpeciesHelper, speciesID, 1); - UpdateSearchResults(SearchForField("speciesID", speciesID)); - app:PlayFanfare(); - app:TakeScreenShot(); - wipe(searchCache); +app.QueryCompletedQuests = QueryCompletedQuests; +-- A set of quests which indicate a needed refresh to the Custom Collect status of the character +local CustomCollectQuests = { + [56775] = 1, -- New Player Experience Starting Quest + [59926] = 1, -- New Player Experience Starting Quest + [58911] = 1, -- New Player Experience Ending Quest + [60359] = 1, -- New Player Experience Ending Quest + [62713] = 1, -- Shadowlands - SL_SKIP (Threads of Fate) + [65076] = 1, -- Shadowlands - Covenant - Kyrian + [65077] = 1, -- Shadowlands - Covenant - Venthyr + [65078] = 1, -- Shadowlands - Covenant - Night Fae + [65079] = 1, -- Shadowlands - Covenant - Necrolord +}; +local QuestCompletionHelper = function(questID) + if questID then + -- Only increase progress for Quests as Collectible users. + if app.CollectibleQuests or app.CollectibleQuestsLocked then + -- Search ATT for the related quests. + local searchResults = SearchForField("questID", questID); + UpdateSearchResults(searchResults, true); + end + -- Certain quests being completed should trigger a refresh of the Custom Collect status of the character (i.e. Covenant Switches, Threads of Fate, etc.) + if CustomCollectQuests[questID] then + Callback(app.RefreshCustomCollectibility); + end end end -app.events.PET_JOURNAL_PET_DELETED = function(petID) - -- /dump C_PetJournal.GetPetInfoByPetID("BattlePet-0-00001006503D") - -- local speciesID = select(1, C_PetJournal.GetPetInfoByPetID(petID)); - -- NOTE: Above APIs do not work in the DELETED API, THANKS BLIZZARD - -- print("PET_JOURNAL_PET_DELETED", petID,C_PetJournal.GetPetInfoByPetID(petID)); +local function RefreshQuestCompletionState(questID) + -- print("QuestRefresh",questID) + if questID then + CompletedQuests[questID] = true; + else + QueryCompletedQuests(); + end - -- Check against all of the collected species for a species that is no longer 1/X - local atLeastOne = false; - for speciesID,collected in pairs(CollectedSpeciesHelper) do - if C_PetJournal_GetNumCollectedInfo(speciesID) < 1 then - rawset(CollectedSpeciesHelper, speciesID, nil); - atLeastOne = true; - end + for questID,_ in pairs(DirtyQuests) do + QuestCompletionHelper(tonumber(questID)); end - if atLeastOne then - app:PlayRemoveSound(); - app:RefreshData(false, true); - -- wipe(searchCache); -- handled by refresh data + -- soft update if any quests were even completed to ensure visible changes occur + if DirtyQuests.DIRTY then + app:UpdateWindows(); end + -- re-register the criteria update event + app:RegisterEvent("CRITERIA_UPDATE"); + wipe(DirtyQuests); + wipe(npcQuestsCache); end - -local fields = { - ["key"] = function(t) - return "petAbilityID"; - end, - ["text"] = function(t) - return select(2, C_PetBattles_GetAbilityInfoByID(t.petAbilityID)); - end, - ["icon"] = function(t) - return select(3, C_PetBattles_GetAbilityInfoByID(t.petAbilityID)); - end, - ["description"] = function(t) - return select(5, C_PetBattles_GetAbilityInfoByID(t.petAbilityID)); - end, -}; -app.BasePetAbility = app.BaseObjectFields(fields, "BasePetAbility"); -app.CreatePetAbility = function(id, t) - return setmetatable(constructor(id, t, "petAbilityID"), app.BasePetAbility); +app.RefreshQuestInfo = function(questID) + -- print("RefreshQuestInfo",questID) + -- unregister criteria update until the quest refresh actually completes + app:UnregisterEvent("CRITERIA_UPDATE"); + if questID then + RefreshQuestCompletionState(questID); + else + AfterCombatOrDelayedCallback(RefreshQuestCompletionState, 1); + end end -local fields = { - ["key"] = function(t) - return "petTypeID"; - end, - ["text"] = function(t) - return _G["BATTLE_PET_NAME_" .. t.petTypeID]; - end, - ["icon"] = function(t) - return "Interface\\Icons\\Icon_PetFamily_"..PET_TYPE_SUFFIX[t.petTypeID]; - end, - ["filterID"] = function(t) - return 101; - end, -}; -app.BasePetType = app.BaseObjectFields(fields, "BasePetType"); -app.CreatePetType = function(id, t) - return setmetatable(constructor(id, t, "petTypeID"), app.BasePetType); -end -end)(); +-- Quest Objective Lib +-- Not used in Retail anymore +-- local fields = { +-- ["key"] = function(t) +-- return "objectiveID"; +-- end, +-- ["name"] = function(t) +-- local objInfo = t.parent.objectiveInfo; +-- if objInfo then +-- local objective = objInfo[t.objectiveID]; +-- if objective then return objective.text; end +-- end +-- return L["QUEST_OBJECTIVE_INVALID"]; +-- end, +-- ["icon"] = function(t) +-- if t.providers then +-- for k,v in ipairs(t.providers) do +-- if v[2] > 0 then +-- if v[1] == "o" then +-- return app.ObjectIcons[v[2]] or "Interface\\Worldmap\\Gear_64Grey"; +-- elseif v[1] == "i" then +-- return select(5, GetItemInfoInstant(v[2])) or "Interface\\Worldmap\\Gear_64Grey"; +-- end +-- end +-- end +-- end +-- if t.spellID then return select(3, GetSpellInfo(t.spellID)); end +-- return t.parent.icon or "Interface\\Worldmap\\Gear_64Grey"; +-- end, +-- ["model"] = function(t) +-- if t.providers then +-- for k,v in ipairs(t.providers) do +-- if v[2] > 0 then +-- if v[1] == "o" then +-- return app.ObjectModels[v[2]]; +-- end +-- end +-- end +-- end +-- end, +-- ["objectiveID"] = function(t) +-- return 1; +-- end, +-- ["questID"] = function(t) +-- return t.parent.questID; +-- end, +-- ["isDaily"] = function(t) +-- return t.parent.isDaily; +-- end, +-- ["isWeekly"] = function(t) +-- return t.parent.isWeekly; +-- end, +-- ["isMonthly"] = function(t) +-- return t.parent.isMonthly; +-- end, +-- ["isYearly"] = function(t) +-- return t.parent.isYearly; +-- end, +-- ["isWorldQuest"] = function(t) +-- return t.parent.isWorldQuest; +-- end, +-- ["repeatable"] = function(t) +-- return t.parent.repeatable; +-- end, +-- ["collectible"] = function(t) +-- return t.questID and C_QuestLog_IsOnQuest(t.questID); +-- end, +-- ["trackable"] = app.ReturnTrue, +-- ["collected"] = function(t) +-- -- If the parent is collected, return immediately. +-- local collected = t.parent.collected; +-- if collected then return collected; end --- Category Lib -(function() -local fields = { - ["key"] = function(t) - return "categoryID"; - end, - ["name"] = function(t) - return AllTheThingsAD.LocalizedCategoryNames[t.categoryID] or ("Unknown Category #" .. t.categoryID); - end, - ["icon"] = function(t) - return AllTheThings.CategoryIcons[t.categoryID] or "Interface/ICONS/INV_Garrison_Blueprints1"; - end, -}; -app.BaseCategory = app.BaseObjectFields(fields, "BaseCategory"); -app.CreateCategory = function(id, t) - return setmetatable(constructor(id, t, "categoryID"), app.BaseCategory); -end -end)(); +-- -- Check to see if the objective was completed. +-- local objInfo = t.parent.objectiveInfo; +-- if objInfo then +-- local objective = objInfo[t.objectiveID]; +-- if objective then return objective.finished and 1; end +-- end +-- end, +-- ["saved"] = function(t) +-- -- If the parent is saved, return immediately. +-- if t.parent.saved then return true; end --- Character Class Lib +-- -- Check to see if the objective was completed. +-- local objInfo = t.parent.objectiveInfo; +-- if objInfo then +-- local objective = objInfo[t.objectiveID]; +-- if objective then return objective.finished and 1; end +-- end +-- end, +-- }; +-- app.BaseQuestObjective = app.BaseObjectFields(fields, "BaseQuestObjective"); +-- app.CreateQuestObjective = function(id, t) +-- return setmetatable(constructor(id, t, "objectiveID"), app.BaseQuestObjective); +-- end +--]] + +-- Vignette Lib (function() -local class_id_cache = {}; -for i=1,GetNumClasses() do - class_id_cache[select(2, GetClassInfo(i))] = i; -end -local classIcons = { - [1] = "Interface\\Icons\\ClassIcon_Warrior", - [2] = "Interface\\Icons\\ClassIcon_Paladin", - [3] = "Interface\\Icons\\ClassIcon_Hunter", - [4] = "Interface\\Icons\\ClassIcon_Rogue", - [5] = "Interface\\Icons\\ClassIcon_Priest", - [6] = "Interface\\Icons\\ClassIcon_DeathKnight", - [7] = "Interface\\Icons\\ClassIcon_Shaman", - [8] = "Interface\\Icons\\ClassIcon_Mage", - [9] = "Interface\\Icons\\ClassIcon_Warlock", - [10] = "Interface\\Icons\\ClassIcon_Monk", - [11] = "Interface\\Icons\\ClassIcon_Druid", - [12] = "Interface\\Icons\\ClassIcon_DemonHunter", -}; -local GetClassIDFromClassFile = function(classFile) - for i,icon in pairs(classIcons) do - local info = C_CreatureInfo.GetClassInfo(i); - if info and info.classFile == classFile then - return i; +-- Vignettes copy Quest fields +local fields = RawCloneData(questFields); +local function BuildTextFromNPCIDs(t, npcIDs) + if not npcIDs or #npcIDs == 0 then app.report("Invalid Vignette! "..(t.hash or "[NOHASH]")) end + local retry, name; + local textTbl = {}; + for i,npcID in ipairs(npcIDs) do + name = app.NPCNameFromID[npcID]; + retry = retry or not name or name == RETRIEVING_DATA; + if not retry then + textTbl[i * 2 - 1] = name; + if i > 1 then + textTbl[(i - 1) * 2] = ", "; + end end end + if retry then return RETRIEVING_DATA; end + name = table.concat(textTbl); + rawset(t, "name", name); + return name; end -app.ClassDB = setmetatable({}, { __index = function(t, className) - for i,_ in pairs(classIcons) do - local info = C_CreatureInfo.GetClassInfo(i); - if info and info.className == className then - rawset(t, className, i); - return i; - end +-- Custom Vignette fields +fields.name = function(t) + if t.qgs or t.crs then + return BuildTextFromNPCIDs(t, t.qgs or t.crs); + elseif t.qg or t.creatureID then + return BuildTextFromNPCIDs(t, { t.qg or t.creatureID }); end -end }); -local math_floor = math.floor; -local cache = app.CreateCache("classID"); + return BuildTextFromNPCIDs(t); +end; +fields.icon = function(t) return "Interface\\Icons\\INV_Misc_Head_Dragon_Black"; end; +fields.isVignette = app.ReturnTrue; +app.BaseVignette = app.BaseObjectFields(fields, "BaseVignette"); +app.CreateVignette = function(id, t) + return setmetatable(constructor(id, t, "questID"), app.BaseVignette); +end +end)(); + +app:RegisterEvent("QUEST_SESSION_JOINED"); +end)(); + +-- Achievement Lib +(function() +local GetAchievementCategory, GetAchievementNumCriteria, GetCategoryInfo, GetStatistic = GetAchievementCategory, GetAchievementNumCriteria, GetCategoryInfo, GetStatistic; +local cache = app.CreateCache("achievementID"); local function CacheInfo(t, field) local _t, id = cache.GetCached(t); - -- specc can be included in the id - local classID = math_floor(id); - rawset(t, "classKey", classID); - local specc_decimal = 1000 * (id - classID); - local specc = math_floor(specc_decimal + 0.00001); - if specc > 0 then - local text = select(2, GetSpecializationInfoForSpecID(specc)); - --[[ - if t.mapID then - text = app.GetMapName(t.mapID) .. " (" .. text .. ")"; - elseif t.maps then - text = app.GetMapName(t.maps[1]) .. " (" .. text .. ")"; - end - --]] - text = "|c" .. t.classColors.colorStr .. text .. "|r"; - rawset(t, "text", text); - _t.text = text; - _t.icon = select(4, GetSpecializationInfoForSpecID(specc)); - else - local text = GetClassInfo(t.classID); - --[[ - if t.mapID then - text = app.GetMapName(t.mapID) .. " (" .. text .. ")"; - elseif t.maps then - text = app.GetMapName(t.maps[1]) .. " (" .. text .. ")"; - end - --]] - text = "|c" .. t.classColors.colorStr .. text .. "|r"; - rawset(t, "text", text); - _t.text = text; - _t.icon = classIcons[t.classID] - end + --local IDNumber, Name, Points, Completed, Month, Day, Year, Description, Flags, Image, RewardText, isGuildAch = GetAchievementInfo(t.achievementID); + local _, name, _, _, _, _, _, _, _, icon = GetAchievementInfo(id); + _t.link = GetAchievementLink(id); + _t.name = name or ("Achievement #"..id); + _t.icon = icon or QUESTION_MARK_ICON; if field then return _t[field]; end end +app.AchievementFilter = 4; local fields = { ["key"] = function(t) - return "classID"; + return "achievementID"; end, - ["text"] = function(t) - local text = cache.GetCachedField(t, "text", CacheInfo); - if t.mapID then - text = app.GetMapName(t.mapID) .. " (" .. text .. ")"; - elseif t.maps then - text = app.GetMapName(t.maps[1]) .. " (" .. text .. ")"; + ["achievementID"] = function(t) + local achievementID = t.altAchID and app.FactionID == Enum.FlightPathFaction.Horde and t.altAchID or t.achID; + if achievementID then + rawset(t, "achievementID", achievementID); + return achievementID; end - text = "|c" .. t.classColors.colorStr .. text .. "|r"; - rawset(t, "text", text); - return text; - end, - ["icon"] = function(t) - return cache.GetCachedField(t, "icon", CacheInfo); - -- return classIcons[t.classID]; - end, - ["c"] = function(t) - local c = { t.classID }; - rawset(t, "c", c); - return c; - end, - ["nmc"] = function(t) - return t.classID ~= app.ClassIndex; - end, - ["classColors"] = function(t) - return RAID_CLASS_COLORS[select(2, GetClassInfo(t.classID))]; - end, -}; -app.BaseCharacterClass = app.BaseObjectFields(fields, "BaseCharacterClass"); -app.CreateCharacterClass = function(id, t) - return setmetatable(constructor(id, t, "classID"), app.BaseCharacterClass); -end -local unitFields = { - ["key"] = function(t) - return "unit"; end, ["text"] = function(t) - for guid,character in pairs(ATTCharacterData) do - if guid == t.unit or character.name == t.unit then - rawset(t, "text", character.text); - rawset(t, "level", character.lvl); - if character.classID then - rawset(t, "classID", character.classID); - rawset(t, "class", C_CreatureInfo.GetClassInfo(character.classID).className); - end - if character.raceID then - rawset(t, "raceID", character.raceID); - rawset(t, "race", C_CreatureInfo.GetRaceInfo(character.raceID).raceName); - end - return character.text; - end - end - - local name, realm = UnitName(t.unit); - if name then - if realm and realm ~= "" then name = name .. "-" .. realm; end - local _, classFile, classID = UnitClass(t.unit); - if classFile then - rawset(t, "classID", classID); - name = "|c" .. RAID_CLASS_COLORS[classFile].colorStr .. name .. "|r"; - end - return name; - end - return t.unit; + return t.link or t.name; end, - ["icon"] = function(t) - if t.classID then return classIcons[t.classID]; end + ["link"] = function(t) + return cache.GetCachedField(t, "link", CacheInfo); end, ["name"] = function(t) - return UnitName(t.unit); + return cache.GetCachedField(t, "name", CacheInfo); end, - ["guid"] = function(t) - return UnitGUID(t.unit); + ["icon"] = function(t) + return cache.GetCachedField(t, "icon", CacheInfo); end, - ["title"] = function(t) - if IsInGroup() then - if rawget(t, "isML") then return MASTER_LOOTER; end - if UnitIsGroupLeader(t.unit) then return RAID_LEADER; end - end + ["collectible"] = function(t) + return app.CollectibleAchievements; end, - ["description"] = function(t) - return LEVEL .. " " .. (t.level or RETRIEVING_DATA) .. " " .. (t.race or RETRIEVING_DATA) .. " " .. (t.class or RETRIEVING_DATA); + ["collected"] = function(t) + if app.CurrentCharacter.Achievements[t.achievementID] then return 1; end + if select(13, GetAchievementInfo(t.achievementID)) then + app.CurrentCharacter.Achievements[t.achievementID] = 1; + ATTAccountWideData.Achievements[t.achievementID] = 1; + return 1; + end + if app.AccountWideAchievements and ATTAccountWideData.Achievements[t.achievementID] then return 2; end end, - ["level"] = function(t) - return UnitLevel(t.unit); + ["parentCategoryID"] = function(t) + return GetAchievementCategory(t.achievementID) or -1; end, - ["race"] = function(t) - return UnitRace(t.unit); + ["statistic"] = function(t) + if GetAchievementNumCriteria(t.achievementID) == 1 then + local quantity, reqQuantity = select(4, GetAchievementCriteriaInfo(t.achievementID, 1)); + if quantity and reqQuantity and reqQuantity > 1 then + return tostring(quantity) .. " / " .. tostring(reqQuantity); + end + end + local statistic = GetStatistic(t.achievementID); + if statistic and statistic ~= '0' then + return statistic; + end end, - ["class"] = function(t) - return UnitClass(t.unit); + ["sortProgress"] = function(t) + if t.collected then + return 1; + end + -- only calculate achievement progress using achievements where the single criteria is the 'progress bar' + if GetAchievementNumCriteria(t.achievementID) == 1 then + local quantity, reqQuantity = select(4, GetAchievementCriteriaInfo(t.achievementID, 1)); + if quantity and reqQuantity and reqQuantity > 1 then + -- print("ach-prog",t.achievementID,quantity,reqQuantity); + return (quantity / reqQuantity); + end + end + return 0; end, + ["OnUpdate"] = function(t) ResolveSymbolicLink(t); end, }; -app.BaseUnit = app.BaseObjectFields(unitFields, "BaseUnit"); -app.CreateUnit = function(unit, t) - return setmetatable(constructor(unit, t, "unit"), app.BaseUnit); +app.BaseAchievement = app.BaseObjectFields(fields, "BaseAchievement"); +app.CreateAchievement = function(id, t) + return setmetatable(constructor(id, t, "achID"), app.BaseAchievement); end -end)(); --- Currency Lib -(function() -local C_CurrencyInfo_GetCurrencyInfo = C_CurrencyInfo.GetCurrencyInfo; -local C_CurrencyInfo_GetCurrencyLink = C_CurrencyInfo.GetCurrencyLink; -local cache = app.CreateCache("currencyID"); -local function default_text(t) - return t.link or t.name; -end -local function default_info(t) - return C_CurrencyInfo_GetCurrencyInfo(t.currencyID); -end -local function default_link(t) - return C_CurrencyInfo_GetCurrencyLink(t.currencyID, 1); -end -local function default_costCollectibles(t) - local id = t.currencyID; - if id then - local results = app.SearchForField("currencyIDAsCost", id); - if results and #results > 0 then - -- app.PrintDebug("default_costCollectibles",t.hash,#results) - return results; - end - end - return app.EmptyTable; -end -local fields = { +local categoryFields = { ["key"] = function(t) - return "currencyID"; - end, - ["_cache"] = function(t) - return cache; - end, - ["text"] = function(t) - return cache.GetCachedField(t, "text", default_text); - end, - ["info"] = function(t) - return cache.GetCachedField(t, "info", default_info); - end, - ["link"] = function(t) - return cache.GetCachedField(t, "link", default_link); - end, - ["icon"] = function(t) - local info = t.info; - return info and info.iconFileID; + return "achievementCategoryID"; end, ["name"] = function(t) - local info = t.info; - return info and info.name or ("Currency #" .. t.currencyID); - end, - ["costCollectibles"] = function(t) - return cache.GetCachedField(t, "costCollectibles", default_costCollectibles); + return GetCategoryInfo(t.achievementCategoryID); end, - ["collectibleAsCost"] = app.CollectibleAsCost, - ["collectedAsCost"] = app.CollectedAsCost, - ["costTotal"] = function(t) - return t.collectibleAsCost and 1 or 0; + ["icon"] = function(t) + return app.asset("Category_Achievements"); end, - ["costProgress"] = function(t) - return t.collectedAsCost and 1 or 0; + ["parentCategoryID"] = function(t) + return select(2, GetCategoryInfo(t.achievementCategoryID)) or -1; end, }; -app.BaseCurrencyClass = app.BaseObjectFields(fields, "BaseCurrencyClass"); -app.CreateCurrencyClass = function(id, t) - return setmetatable(constructor(id, t, "currencyID"), app.BaseCurrencyClass); +app.BaseAchievementCategory = app.BaseObjectFields(categoryFields, "BaseAchievementCategory"); +app.CreateAchievementCategory = function(id, t) + return setmetatable(constructor(id, t, "achievementCategoryID"), app.BaseAchievementCategory); end -end)(); --- Death Tracker Lib -(function() -local OnUpdateForDeathTrackerLib = function(t) - if app.MODE_DEBUG then -- app.Settings:Get("Thing:Deaths"); - t.visible = app.GroupVisibilityFilter(t); - local stat = select(1, GetStatistic(60)) or "0"; - if stat == "--" then stat = "0"; end - local deaths = tonumber(stat); - if deaths > 0 and deaths > app.CurrentCharacter.Deaths then - app.CurrentCharacter.Deaths = deaths; - ATTAccountWideData.Deaths = ATTAccountWideData.Deaths + (deaths - app.CurrentCharacter.Deaths); - end - t.parent.progress = t.parent.progress + t.progress; - t.parent.total = t.parent.total + t.total; - else - t.visible = false; +-- Achievement Criteria Lib +local function GetParentAchievementInfo(t, key) + local sourceAch = t.sourceParent or t.parent; + if sourceAch and sourceAch.achievementID == t.achievementID then + rawset(t, key, sourceAch[key]); + return rawget(t, key); end - return false; end -local fields = { +local criteriaFields = { ["key"] = function(t) - return "deaths"; - end, - ["text"] = function(t) - return "Total Deaths"; - end, - ["icon"] = function(t) - return app.asset("Category_Deaths"); - end, - ["progress"] = function(t) - return math.min(1000, app.AccountWideDeaths and ATTAccountWideData.Deaths or app.CurrentCharacter.Deaths); - end, - ["total"] = function(t) - return 1000; + return "criteriaID"; end, - ["description"] = function(t) - return "The ATT Gods must be sated. Go forth and attempt to level, mortal!\n\n 'Live! Die! Live Again!'\n"; + ["achievementID"] = function(t) + local sourceAch = t.sourceParent or t.parent; + local achievementID = t.altAchID and app.FactionID == Enum.FlightPathFaction.Horde and t.altAchID or t.achID or (sourceAch and (sourceAch.achievementID or (sourceAch.parent and sourceAch.parent.achievementID))); + if achievementID then + rawset(t, "achievementID", achievementID); + return achievementID; + end end, - ["OnTooltip"] = function(t) - local c = {}; - for guid,character in pairs(ATTCharacterData) do - if character and character.Deaths and character.Deaths > 0 then - tinsert(c, character); - end + ["name"] = function(t) + if t.link then return t.link; end + if t.encounterID then + return select(1, EJ_GetEncounterInfo(t.encounterID)); end - if #c > 0 then - GameTooltip:AddLine(" "); - GameTooltip:AddLine("Deaths Per Character:"); - app.Sort(c, function(a, b) - return a.Deaths > b.Deaths; - end); - for i,data in ipairs(c) do - GameTooltip:AddDoubleLine(" " .. string.gsub(data.text, "-" .. GetRealmName(), ""), data.Deaths, 1, 1, 1); + if t.achievementID then + local m = GetAchievementNumCriteria(t.achievementID); + if m and t.criteriaID <= m then + return GetAchievementCriteriaInfo(t.achievementID, t.criteriaID, true); end end + return L["WRONG_FACTION"]; end, - ["OnUpdate"] = function(t) - return OnUpdateForDeathTrackerLib; - end, -}; -app.BaseDeathClass = app.BaseObjectFields(fields, "BaseDeathClass"); -app.CreateDeathClass = function() - return setmetatable({}, app.BaseDeathClass); -end -end)(); - --- Difficulty Lib -(function() -app.DifficultyColors = { - [2] = "ff0070dd", - [5] = "ff0070dd", - [6] = "ff0070dd", - [7] = "ff9d9d9d", - [15] = "ff0070dd", - [16] = "ffa335ee", - [17] = "ff9d9d9d", - [23] = "ffa335ee", - [24] = "ffe6cc80", - [33] = "ffe6cc80", -}; -app.DifficultyIcons = { - [-1] = app.asset("Difficulty_LFR"), - [-2] = app.asset("Difficulty_Normal"), - [-3] = app.asset("Difficulty_Heroic"), - [-4] = app.asset("Difficulty_Mythic"), - [1] = app.asset("Difficulty_Normal"), - [2] = app.asset("Difficulty_Heroic"), - [3] = app.asset("Difficulty_Normal"), - [4] = app.asset("Difficulty_Normal"), - [5] = app.asset("Difficulty_Heroic"), - [6] = app.asset("Difficulty_Heroic"), - [7] = app.asset("Difficulty_LFR"), - [9] = app.asset("Difficulty_Mythic"), - [11] = app.asset("Difficulty_Normal"), - [12] = app.asset("Difficulty_Heroic"), - [14] = app.asset("Difficulty_Normal"), - [15] = app.asset("Difficulty_Heroic"), - [16] = app.asset("Difficulty_Mythic"), - [17] = app.asset("Difficulty_LFR"), - [18] = app.asset("Category_Event"), - [23] = app.asset("Difficulty_Mythic"), - [24] = app.asset("Difficulty_Timewalking"), - [33] = app.asset("Difficulty_Timewalking"), -}; -local fields = { - ["key"] = function(t) - return "difficultyID"; - end, - ["text"] = function(t) - return L["CUSTOM_DIFFICULTIES"][t.difficultyID] or GetDifficultyInfo(t.difficultyID) or "Unknown Difficulty"; - end, - ["icon"] = function(t) - return app.DifficultyIcons[t.difficultyID]; - end, - ["saved"] = function(t) - return t.locks; + ["description"] = function(t) + if t.encounterID then + return select(2, EJ_GetEncounterInfo(t.encounterID)); + end end, - ["locks"] = function(t) - local locks = t.parent and t.parent.locks; - if locks then - if t.parent.isLockoutShared and not (t.difficultyID == 7 or t.difficultyID == 17) then - rawset(t, "locks", locks.shared); - return locks.shared; - else - -- Look for this difficulty's lockout. - for difficultyKey, lock in pairs(locks) do - if difficultyKey == "shared" then - -- ignore this one - elseif difficultyKey == t.difficultyID then - rawset(t, "locks", lock); - return lock; - end - end + ["link"] = function(t) + if t.itemID then + local _, link, _, _, _, _, _, _, _, icon = GetItemInfo(t.itemID); + if link then + t.text = link; + t.link = link; + t.icon = icon; + return link; end end end, - ["u"] = function(t) - if t.difficultyID == 24 or t.difficultyID == 33 then - return 1016; + ["displayID"] = function(t) + if t.encounterID then + -- local id, name, description, displayInfo, iconImage = EJ_GetCreatureInfo(1, t.encounterID); + return select(4, EJ_GetCreatureInfo(t.index, t.encounterID)); end end, - ["description"] = function(t) - if t.difficultyID == 24 or t.difficultyID == 33 then - return L["WE_JUST_HATE_TIMEWALKING"]; + ["displayInfo"] = function(t) + if t.encounterID then + local displayInfos, displayInfo = {}; + for i=1,MAX_CREATURES_PER_ENCOUNTER do + displayInfo = select(4, EJ_GetCreatureInfo(i, t.encounterID)); + if displayInfo then + tinsert(displayInfos, displayInfo); + else + break; + end + end + return displayInfos; end end, -}; -app.BaseDifficulty = app.BaseObjectFields(fields, "BaseDifficulty"); -app.CreateDifficulty = function(id, t) - return setmetatable(constructor(id, t, "difficultyID"), app.BaseDifficulty); -end -end)(); - --- Encounter Lib -(function() -local cache = app.CreateCache("encounterID"); -local function CacheInfo(t, field) - local _t, id = cache.GetCached(t); - local name, lore, _, _, link = EJ_GetEncounterInfo(id); - _t.name = name; - _t.lore = lore; - _t.link = link; - _t.displayID = select(4, EJ_GetCreatureInfo(1, id)); - if field then return _t[field]; end -end -local function default_displayInfo(t) - local displayInfos, id, displayInfo = {}, t.encounterID; - for i=1,MAX_CREATURES_PER_ENCOUNTER do - displayInfo = select(4, EJ_GetCreatureInfo(i, id)); - if displayInfo then - tinsert(displayInfos, displayInfo); - else - break; + ["trackable"] = app.ReturnTrue, + ["collected"] = function(t) + local achievementID = t.achievementID; + if achievementID then + if app.CurrentCharacter.Achievements[achievementID] then return 1; end + if app.AccountWideAchievements and ATTAccountWideData.Achievements[achievementID] then return 2; end + if t.criteriaID and t.criteriaID <= (GetAchievementNumCriteria(achievementID) or -1) then + return select(3, GetAchievementCriteriaInfo(achievementID, t.criteriaID, true)); + end end - end - return displayInfos; -end -local fields = { - ["key"] = function(t) - return "encounterID"; - end, - ["name"] = function(t) - return cache.GetCachedField(t, "name", CacheInfo); - end, - ["lore"] = function(t) - return cache.GetCachedField(t, "lore", CacheInfo); - end, - ["link"] = function(t) - return cache.GetCachedField(t, "link", CacheInfo); end, - ["displayID"] = function(t) - return cache.GetCachedField(t, "displayID", CacheInfo); + ["saved"] = function(t) + local achievementID = t.achievementID; + if achievementID then + if app.CurrentCharacter.Achievements[achievementID] then return true; end + if t.criteriaID and t.criteriaID <= (GetAchievementNumCriteria(achievementID) or -1) then + return select(3, GetAchievementCriteriaInfo(achievementID, t.criteriaID, true)); + end + end end, - ["displayInfo"] = function(t) - return cache.GetCachedField(t, "displayInfo", default_displayInfo); + ["index"] = function(t) + return 1; end, - ["icon"] = function(t) - return app.DifficultyIcons[GetRelativeValue(t, "difficultyID") or 1]; + -- Use parent achievement if info not listed directly in the criteria + ["c"] = function(t) + return GetParentAchievementInfo(t, "c"); end, - ["trackable"] = function(t) - return t.questID; + ["classID"] = function(t) + return GetParentAchievementInfo(t, "classID"); end, - ["saved"] = function(t) - -- only consider encounters saved if saved for the current character - return IsQuestFlaggedCompleted(t.questID); + ["races"] = function(t) + return GetParentAchievementInfo(t, "races"); end, - ["index"] = function(t) - return 1; + ["r"] = function(t) + return GetParentAchievementInfo(t, "r"); end, }; -app.BaseEncounter = app.BaseObjectFields(fields, "BaseEncounter"); -app.CreateEncounter = function(id, t) - return setmetatable(constructor(id, t, "encounterID"), app.BaseEncounter); +criteriaFields.collectible = fields.collectible; +criteriaFields.icon = fields.icon; +app.BaseAchievementCriteria = app.BaseObjectFields(criteriaFields, "BaseAchievementCriteria"); +app.CreateAchievementCriteria = function(id, t) + return setmetatable(constructor(id, t, "criteriaID"), app.BaseAchievementCriteria); end -end)(); --- Faction Lib -(function() -local GetFriendshipReputation = GetFriendshipReputation; -local StandingByID = { - { -- 1: HATED - ["color"] = GetProgressColor(0), - ["threshold"] = -42000, - }, - { -- 2: HOSTILE - ["color"] = "00FF0000", - ["threshold"] = -6000, - }, - { -- 3: UNFRIENDLY - ["color"] = "00EE6622", - ["threshold"] = -3000, - }, - { -- 4: NEUTRAL - ["color"] = "00FFFF00", - ["threshold"] = 0, - }, - { -- 5: FRIENDLY - ["color"] = "0000FF00", - ["threshold"] = 3000, - }, - { -- 6: HONORED - ["color"] = "0000FF88", - ["threshold"] = 9000, - }, - { -- 7: REVERED - ["color"] = "0000FFCC", - ["threshold"] = 21000, - }, - { -- 8: EXALTED - ["color"] = GetProgressColor(1), - ["threshold"] = 42000, - }, -}; -app.FactionNameByID = setmetatable({}, { __index = function(t, id) - local name = select(1, GetFactionInfoByID(id)) or select(4, GetFriendshipReputation(id)); - if name then - rawset(t, id, name); - rawset(app.FactionIDByName, name, id); - return name; - end -end }); -app.FactionIDByName = setmetatable({}, { __index = function(t, name) - for i=1,3000,1 do - if app.FactionNameByID[i] == name then - rawset(t, name, i); - return i; - end - end -end }); -app.FACTION_RACES = { - [1] = { - 1, -- Human - 3, -- Dwarf - 4, -- Night Elf - 7, -- Gnome - 11, -- Draenei - 22, -- Worgen - 25, -- Pandaren [Alliance] - 29, -- Void Elf - 30, -- Lightforged - 32, -- Kul Tiran - 34, -- Dark Iron - 37, -- Mechagnome - }, - [2] = { - 2, -- Orc - 5, -- Undead - 6, -- Tauren - 8, -- Troll - 9, -- Goblin - 10, -- Blood Elf - 26, -- Pandaren [Horde] - 27, -- Nightborne - 28, -- Highmountain - 31, -- Zandalari - 35, -- Vulpera - 36, -- Mag'har - } + +local HarvestedAchievementDatabase = {}; +local harvesterFields = RawCloneData(fields); +harvesterFields.visible = app.ReturnTrue; +harvesterFields.collectible = app.ReturnTrue; +harvesterFields.collected = app.ReturnFalse; +harvesterFields.text = function(t) + local achievementID = t.achievementID; + if achievementID then + local IDNumber, Name, Points, Completed, Month, Day, Year, Description, Flags, Image, RewardText, isGuildAch = GetAchievementInfo(achievementID); + if Name then + local info = { + ["name"] = Name, + ["achievementID"] = IDNumber, + ["parentCategoryID"] = GetAchievementCategory(achievementID) or -1, + ["icon"] = Image, + }; + if Description ~= nil and Description ~= "" then + info.description = Description; + end + local totalCriteria = GetAchievementNumCriteria(achievementID); + if totalCriteria > 0 then + local criteria = {}; + for criteriaID=totalCriteria,1,-1 do + local criteriaString, criteriaType, completed, quantity, reqQuantity, charName, flags, assetID, quantityString = GetAchievementCriteriaInfo(achievementID, criteriaID); + local crit = { ["criteriaID"] = criteriaID }; + if criteriaString ~= nil and criteriaString ~= "" then + crit.name = criteriaString; + end + if assetID and assetID ~= 0 then + crit.assetID = assetID; + end + if reqQuantity and reqQuantity > 0 then + crit.rank = reqQuantity; + end + if criteriaType then + -- Unknown type, not sure what to do with this. + crit.criteriaType = criteriaType; + if crit.assetID then + if criteriaType == 27 then -- Quest Completion + crit.sourceQuest = assetID; + crit.criteriaType = nil; + crit.assetID = nil; + if crit.rank and crit.rank == 1 then + crit.rank = nil; + break; + end + elseif criteriaType == 36 or criteriaType == 41 or criteriaType == 42 then + -- 36: Items (Generic) + -- 41: Items (Use/Eat) + -- 42: Items (Loot) + if crit.rank and crit.rank < 2 then + crit.provider = { "i", crit.assetID }; + else + crit.cost = { { "i", crit.assetID, crit.rank }}; + end + crit.criteriaType = nil; + crit.assetID = nil; + crit.rank = nil; + elseif criteriaType == 43 then -- Exploration?! + crit.explorationID = crit.assetID; + crit.criteriaType = nil; + crit.assetID = nil; + crit.rank = nil; + elseif criteriaType == 0 then -- NPC Kills + crit.provider = { "n", crit.assetID }; + if crit.rank and crit.rank < 2 then + crit.rank = nil; + end + crit.criteriaType = nil; + crit.assetID = nil; + elseif criteriaType == 96 then -- Collect Pets + crit.provider = { "n", crit.assetID }; + if crit.rank and crit.rank < 2 then + crit.rank = nil; + end + crit.criteriaType = nil; + crit.assetID = nil; + elseif criteriaType == 68 or criteriaType == 72 then -- Interact with Object (68) / Fish from a School (72) + crit.provider = { "o", crit.assetID }; + if crit.rank and crit.rank < 2 then + crit.rank = nil; + end + crit.criteriaType = nil; + crit.assetID = nil; + elseif criteriaType == 7 then -- Skill ID, Rank is Requirement + crit.requireSkill = crit.assetID; + crit.criteriaType = nil; + crit.assetID = nil; + elseif criteriaType == 40 then -- Skill ID Learned + crit.requireSkill = crit.assetID; + crit.criteriaType = nil; + crit.assetID = nil; + crit.rank = nil; + elseif criteriaType == 8 then -- Achievements as Children + crit.provider = { "a", crit.assetID }; + if crit.rank and crit.rank < 2 then + crit.rank = nil; + end + crit.criteriaType = nil; + crit.assetID = nil; + elseif criteriaType == 12 then -- Currencies (Collected Total) + if crit.rank and crit.rank < 2 then + crit.cost = { { "c", crit.assetID, 1 }}; + else + crit.cost = { { "c", crit.assetID, crit.rank }}; + end + crit.criteriaType = nil; + crit.assetID = nil; + crit.rank = nil; + elseif criteriaType == 26 then + -- 26: Environmental Deaths + -- 0: fatigue + -- 1: drowning + -- 2: falling + -- 3/5: fire/lava + -- https://wowwiki-archive.fandom.com/wiki/API_GetAchievementCriteriaInfo + if crit.rank and totalCriteria == 1 then + info.rank = crit.rank; + break; + end + elseif criteriaType == 29 or criteriaType == 69 then -- Cast X Spell Y Times + if crit.rank and totalCriteria == 1 then + info.rank = crit.rank; + break; + else + crit.spellID = crit.assetID; + crit.criteriaType = nil; + crit.assetID = nil; + end + elseif criteriaType == 46 then -- Minimum Faction Requirement + crit.minReputation = { crit.assetID, crit.rank }; + crit.criteriaType = nil; + crit.assetID = nil; + crit.rank = nil; + end + -- 28: Something to do with event-based encounters, not sure what assetID is. + -- 49: Something to do with Equipment Slots, assetID is the equipSlotID. (useless maybe?) + -- 52: Honorable kill on a specific Class, assetID is the ClassID. (useless maybe? might be able to use a class icon?) + -- 53: Honorable kill on a specific Class at level 35+, assetID is the ClassID. (useless maybe? might be able to use a class icon?) + -- 54: Show a critter you /love them, assetID is useless or not present. + -- 70: Honorable Kill at a specific place. + -- 71: Instance Clears, assetID is of an unknown type... might be Saved Instance ID? + -- 73: Mal'Ganis? Complete Objective? (useless) + -- 74: No idea, tracking of some kind + -- 92: Encounter Kills, of non-NPC type. (Group of NPCs - IE: Lilian Voss) + elseif criteriaType == 0 or criteriaType == 3 or criteriaType == 5 or criteriaType == 6 or criteriaType == 9 or criteriaType == 10 or criteriaType == 14 or criteriaType == 15 or criteriaType == 17 or criteriaType == 19 or criteriaType == 26 or criteriaType == 37 or criteriaType == 45 or criteriaType == 75 or criteriaType == 78 or criteriaType == 79 or criteriaType == 81 or criteriaType == 90 or criteriaType == 91 or criteriaType == 109 or criteriaType == 124 or criteriaType == 126 or criteriaType == 130 or criteriaType == 134 or criteriaType == 135 or criteriaType == 136 or criteriaType == 138 or criteriaType == 139 or criteriaType == 151 or criteriaType == 156 or criteriaType == 157 or criteriaType == 158 or criteriaType == 200 or criteriaType == 203 or criteriaType == 207 then + -- 0: Some tracking statistic, generally X/Y format and simple enough to not justify a type if no assetID is present. + -- 3: Collect X of something that's generic for Archeology + -- 5: Level Requirement + -- 6: Digsites (Archeology) + -- 9: Total Quests Completed + -- 10: Daily Quests, every day for X days. + -- 14: Total Daily Quests Completed + -- 15: Battleground battles + -- 17: Total Deaths + -- 19: Instances Run + -- 26: Environmental Deaths + -- 37: Ranked Arena Wins + -- 45: Bank Slots Purchased + -- 75: Mounts (Total - on one Character) + -- 78: Kill NPCs + -- 79: Cook Food + -- 81: Pet battle achievement points + -- 90: Gathering (Nodes) + -- 91: Pet Charm Totals + -- 109: Catch Fish + -- 124: Guild Member Repairs + -- 126: Guild Crafting + -- 130: Rated Battleground Wins + -- 134: Complete Quests + -- 135: Honorable Kills (Total) + -- 136: Kill Critters + -- 138: Guild Scenario Challenges Completed + -- 139: Guild Challenges Completed + -- 151: Guild Scenario Completed + -- 156: Collect Pets (Total) + -- 157: Collect Pets (Rare) + -- 158: Pet Battles + -- 200: Recruit Troops + -- 203: World Quests (Total Complete) + -- 207: Honor Earned (Total) + -- https://wowwiki-archive.fandom.com/wiki/API_GetAchievementCriteriaInfo + if crit.rank and totalCriteria == 1 then + info.rank = crit.rank; + break; + end + elseif criteriaType == 38 or criteriaType == 39 or criteriaType == 58 or criteriaType == 63 or criteriaType == 65 or criteriaType == 66 or criteriaType == 76 or criteriaType == 77 or criteriaType == 82 or criteriaType == 83 or criteriaType == 84 or criteriaType == 85 or criteriaType == 86 or criteriaType == 107 or criteriaType == 128 or criteriaType == 152 or criteriaType == 153 or criteriaType == 163 then -- Ignored + -- 38: Team Rating, which is irrelevant. + -- 39: Personal Rating, which is irrelevant. + -- 58: Killing Blows, might specifically be PvP. + -- 63: Total Gold (Spent on Travel) + -- 65: Total Gold (Spent on Barber Shop) + -- 66: Total Gold (Spent on Mail) + -- 76: Duels Won + -- 77: Duels Lost + -- 82: Auctions (Total Posted) + -- 83: Auctions (Highest Bid) + -- 84: Auctions (Total Purchases) + -- 85: Auctions (Highest Sold)] + -- 86: Most Gold Ever Owned + -- 107: Quests Abandoned + -- 128: Guild Bank Tabs + -- 152: Defeat Scenarios + -- 153: Ride to Location? + -- 163: Also ride to location + break; + elseif criteriaType == 59 or criteriaType == 62 or criteriaType == 67 or criteriaType == 80 then -- Gold Cost, if available. + -- 59: Total Gold (Vendors) + -- 62: Total Gold (Quest Rewards) + -- 67: Total Gold (Looted) + -- 80: Total Gold (Auctions) + if crit.rank and crit.rank > 1 then + if totalCriteria == 1 then + -- Generic, such as the Bread Winner + info.rank = crit.rank; + break; + else + crit.cost = { { "g", crit.assetID, crit.rank } }; + crit.criteriaType = nil; + crit.assetID = nil; + info.rank = nil; + end + else + break; + end + end + -- 155: Collect Battle Pets from a Raid, no assetID though RIP + -- 158: Defeat Master Trainers + -- 161: Capture a Battle Pet in a Zone + -- 163: Defeat an Encounter of some kind? AssetID useless + -- 169: Construct a building, assetID might be the buildingID. + end + tinsert(criteria, 1, crit); + end + if #criteria > 0 then info.criteria = criteria; end + end + + HarvestedAchievementDatabase[achievementID] = info; + AllTheThingsHarvestItems = HarvestedAchievementDatabase; + setmetatable(t, app.BaseAchievement); + rawset(t, "collected", true); + return link; + end + end + + local name = t.name; + -- retries exceeded, so check the raw .name on the group (gets assigned when retries exceeded during cache attempt) + if name then rawset(t, "collected", true); end + return name; +end +app.BaseAchievementHarvester = app.BaseObjectFields(harvesterFields, "BaseAchievementHarvester"); +app.CreateAchievementHarvester = function(id, t) + return setmetatable(constructor(id, t, "achievementID"), app.BaseAchievementHarvester); +end + +local function CheckAchievementCollectionStatus(achievementID) + if ATTAccountWideData then + local id,name,_,accCompleted,_,_,_,_,flags,_,_,isGuild = GetAchievementInfo(achievementID) + if id and not isGuild and accCompleted and bit.band(flags,0x1) == 0 then + ATTAccountWideData.Achievements[id] = 1; + end + end +end +RefreshAchievementCollection = function() + if ATTAccountWideData then + local maxid, achID = 0; + for achievementID,_ in pairs(fieldCache["achievementID"]) do + achID = tonumber(achievementID); + if achID > maxid then maxid = achID; end + end + for achievementID=maxid,1,-1 do + CheckAchievementCollectionStatus(achievementID); + end + end +end +app:RegisterEvent("ACHIEVEMENT_EARNED"); +app.events.ACHIEVEMENT_EARNED = CheckAchievementCollectionStatus; +end)(); + +-- Artifact Lib +(function() +local artifactItemIDs = { + [841] = 133755, -- Underlight Angler [Base Skin] + [988] = 133755, -- Underlight Angler [Fisherfriend of the Isles] + [989] = 133755, -- Underlight Angler [Fisherfriend of the Isles] + [1] = {}, -- Off-Hand ItemIDs +}; +local fields = { + ["key"] = function(t) + return "artifactID"; + end, + ["artifactinfo"] = function(t) + --[[ + local setID, appearanceID, appearanceName, displayIndex, appearanceUnlocked, unlockConditionText, + uiCameraID, altHandUICameraID, swatchR, swatchG, swatchB, + modelAlpha, modelDesaturation, suppressGlobalAnim = C_ArtifactUI_GetAppearanceInfoByID(t.artifactID); + ]]-- + local info = { C_ArtifactUI_GetAppearanceInfoByID(t.artifactID) }; + rawset(t, "artifactinfo", info); + return info; + end, + ["f"] = function(t) + return 11; + end, + ["collectible"] = function(t) + return app.CollectibleTransmog; + end, + ["collected"] = function(t) + if ATTAccountWideData.Artifacts[t.artifactID] then return 1; end + -- This artifact is listed for the current class + if not GetRelativeField(t, "nmc", true) and select(5, C_ArtifactUI_GetAppearanceInfoByID(t.artifactID)) then + ATTAccountWideData.Artifacts[t.artifactID] = 1; + return 1; + end + end, + ["text"] = function(t) + if not t.artifactinfo then return RETRIEVING_DATA; end + -- Artifact listing in the Main item sets category just show 'Variant #' but elsewhere show the Item's name + if t.parent and t.parent.headerID and (t.parent.headerID <= -5200 and t.parent.headerID >= -5205) then + return t.variantText; + end + return t.appearanceText; + end, + ["title"] = function(t) + return t.variantText; + end, + ["variantText"] = function(t) + return Colorize("Variant " .. t.artifactinfo[4], RGBToHex(t.artifactinfo[9] * 255, t.artifactinfo[10] * 255, t.artifactinfo[11] * 255)); + end, + ["appearanceText"] = function(t) + return "|cffe6cc80" .. (t.artifactinfo[3] or "???") .. "|r"; + end, + ["description"] = function(t) + return t.artifactinfo[6] or L["ARTIFACT_INTRO_REWARD"]; + end, + ["atlas"] = function(t) + return "Forge-ColorSwatchBorder"; + end, + ["atlas-background"] = function(t) + return "Forge-ColorSwatchBackground"; + end, + ["atlas-border"] = function(t) + return "Forge-ColorSwatch"; + end, + ["atlas-color"] = function(t) + return { t.artifactinfo[9], t.artifactinfo[10], t.artifactinfo[11], 1.0 }; + end, + ["model"] = function(t) + return t.parent and GetRelativeValue(t.parent, "model"); + end, + ["modelScale"] = function(t) + return t.parent and GetRelativeValue(t.parent, "modelScale") or 0.95; + end, + ["modelRotation"] = function(t) + return t.parent and GetRelativeValue(t.parent, "modelRotation") or 45; + end, + ["silentLink"] = function(t) + local itemID = t.silentItemID; + if itemID then + -- 1 -> Off-Hand Appearance + -- 2 -> Main-Hand Appearance + -- return select(2, GetItemInfo(sformat("item:%d::::::::%d:::11:::8:%d:", itemID, app.Level, t.artifactID))); + -- local link = sformat("item:%d::::::::%d:::11::%d:8:%d:", itemID, app.Level, t.isOffHand and 1 or 2, t.artifactID); + -- print("Artifact link",t.artifactID,itemID,link); + return select(2, GetItemInfo(sformat("item:%d:::::::::::11::%d:8:%d:", itemID, t.isOffHand and 1 or 2, t.artifactID))); + end + end, + ["silentItemID"] = function(t) + local itemID; + if t.isOffHand then + itemID = artifactItemIDs[1][t.artifactID]; + else + itemID = artifactItemIDs[t.artifactID]; + end + if itemID then + return itemID; + elseif t.parent and t.parent.headerID and (t.parent.headerID <= -5200 and t.parent.headerID >= -5205) then + itemID = GetRelativeValue(t.parent, "itemID"); + -- Store the relative ItemID in the artifactItemID cache so it can be referenced accurately by artifacts sourced in specific locations + if itemID then + if t.isOffHand then + artifactItemIDs[1][t.artifactID] = itemID; + else + artifactItemIDs[t.artifactID] = itemID; + end + -- print("Artifact ItemID Cached",t.artifactID,t.isOffHand,itemID) + end + return itemID; + end + end, + ["s"] = function(t) + -- Return the calculated 's' field if existing + if t._s then return t._s; end + local s = t.silentLink; + if s then + s = app.GetSourceID(s); + -- print("Artifact Source",s,t.silentLink) + if s and s > 0 then + rawset(t, "_s", s); + if ATTAccountWideData.Sources[s] ~= 1 and C_TransmogCollection_PlayerHasTransmogItemModifiedAppearance(s) then + -- print("Saved Known Source",s) + ATTAccountWideData.Sources[s] = 1; + end + return s; + end + end + end, +}; +app.BaseArtifact = app.BaseObjectFields(fields, "BaseArtifact"); +app.CreateArtifact = function(id, t) + return setmetatable(constructor(id, t, "artifactID"), app.BaseArtifact); +end +end)(); + +-- Azerite Essence Lib +(function() +local fields = { + ["key"] = function(t) + return "azeriteEssenceID"; + end, + ["info"] = function(t) + return C_AzeriteEssence.GetEssenceInfo(t.azeriteEssenceID) or {}; + end, + ["collectible"] = function(t) + return app.CollectibleAzeriteEssences; + end, + ["collected"] = function(t) + if (app.CurrentCharacter.AzeriteEssenceRanks[t.azeriteEssenceID] or 0) >= t.rank then + return 1; + end + + local accountRank = ATTAccountWideData.AzeriteEssenceRanks[t.azeriteEssenceID] or 0; + local info = t.info; + if info and info.unlocked then + if t.rank and info.rank then + if info.rank >= t.rank then + app.CurrentCharacter.AzeriteEssenceRanks[t.azeriteEssenceID] = info.rank; + if info.rank > accountRank then ATTAccountWideData.AzeriteEssenceRanks[t.azeriteEssenceID] = info.rank; end + return 1; + end + else + return 1; + end + end + + if app.AccountWideAzeriteEssences and accountRank >= t.rank then + return 2; + end + end, + ["text"] = function(t) + return t.link; + end, + ["lvl"] = function(t) + return 50; + end, + ["icon"] = function(t) + return t.info.icon or "Interface/ICONS/INV_Glowing Azerite Spire"; + end, + ["name"] = function(t) + return t.info.name; + end, + ["link"] = function(t) + return C_AzeriteEssence.GetEssenceHyperlink(t.azeriteEssenceID, t.rank); + end, + ["rank"] = function(t) + return t.info.rank or 0; + end, }; -app.ColorizeStandingText = function(standingID, text) - local standing = StandingByID[standingID]; - if standing then - return Colorize(text, standing.color); - else - local rgb = FACTION_BAR_COLORS[standingID]; - return Colorize(text, RGBToHex(rgb.r * 255, rgb.g * 255, rgb.b * 255)); +app.BaseAzeriteEssence = app.BaseObjectFields(fields, "BaseAzeriteEssence"); +app.CreateAzeriteEssence = function(id, t) + return setmetatable(constructor(id, t, "azeriteEssenceID"), app.BaseAzeriteEssence); +end +end)(); + +-- Battle Pet Lib +(function() +-- localized global APIs +local C_PetBattles_GetAbilityInfoByID = C_PetBattles.GetAbilityInfoByID; +local C_PetJournal_GetNumCollectedInfo = C_PetJournal.GetNumCollectedInfo; +local C_PetJournal_GetPetInfoByPetID = C_PetJournal.GetPetInfoByPetID; +local C_PetJournal_GetPetInfoBySpeciesID = C_PetJournal.GetPetInfoBySpeciesID; + +local cache = app.CreateCache("speciesID"); +local function CacheInfo(t, field) + local _t, id = cache.GetCached(t); + -- speciesName, speciesIcon, petType, companionID, tooltipSource, tooltipDescription, isWild, + -- canBattle, isTradeable, isUnique, obtainable, creatureDisplayID = C_PetJournal.GetPetInfoBySpeciesID(speciesID) + local speciesName, speciesIcon, petType, _, _, tooltipDescription, _, _, _, _, _, creatureDisplayID = C_PetJournal_GetPetInfoBySpeciesID(id); + if speciesName then + _t.name = speciesName; + _t.icon = speciesIcon; + _t.petTypeID = petType; + _t.lore = tooltipDescription; + _t.displayID = creatureDisplayID; + _t.text = "|cff0070dd"..speciesName.."|r"; + if field then return _t[field]; end end end -app.GetFactionIDByName = function(name) - name = strtrim(name); - return app.FactionIDByName[name] or name; +local function default_link(t) + if t.itemID then + return select(2, GetItemInfo(t.itemID)); + end + return t.text; end -app.GetFactionStanding = function(reputationPoints) - -- Total earned rep from GetFactionInfoByID is a value AWAY FROM ZERO, not a value within the standing bracket. - if reputationPoints then - for i=#StandingByID,1,-1 do - local threshold = StandingByID[i].threshold; - if reputationPoints >= threshold then - return i, threshold < 0 and (threshold - reputationPoints) or (reputationPoints - threshold); - end +local CollectedSpeciesHelper = setmetatable({}, { + __index = function(t, key) + if C_PetJournal_GetNumCollectedInfo(key) > 0 then + rawset(t, key, 1); + return 1; + end + end +}); +local fields = { + ["key"] = function(t) + return "speciesID"; + end, + ["filterID"] = function(t) + return 101; + end, + ["collectible"] = function(t) + return app.CollectibleBattlePets; + end, + ["collected"] = function(t) + if CollectedSpeciesHelper[t.speciesID] then + return 1; + end + local altSpeciesID = t.altSpeciesID; + if altSpeciesID and CollectedSpeciesHelper[altSpeciesID]then + return 2; + end + end, + ["text"] = function(t) + return cache.GetCachedField(t, "text", CacheInfo); + end, + ["icon"] = function(t) + return cache.GetCachedField(t, "icon", CacheInfo); + end, + ["lore"] = function(t) + return cache.GetCachedField(t, "lore", CacheInfo); + end, + ["displayID"] = function(t) + return cache.GetCachedField(t, "displayID", CacheInfo); + end, + ["petTypeID"] = function(t) + return cache.GetCachedField(t, "petTypeID", CacheInfo); + end, + ["name"] = function(t) + return cache.GetCachedField(t, "name", CacheInfo); + end, + ["link"] = function(t) + return cache.GetCachedField(t, "link", default_link); + end, + ["tsm"] = function(t) + return sformat("p:%d:1:3", t.speciesID); + end, +}; +app.BaseSpecies = app.BaseObjectFields(fields, "BaseSpecies"); +app.CreateSpecies = function(id, t) + return setmetatable(constructor(id, t, "speciesID"), app.BaseSpecies); +end + +app.events.NEW_PET_ADDED = function(petID) + local speciesID = select(1, C_PetJournal_GetPetInfoByPetID(petID)); + -- print("NEW_PET_ADDED", petID, speciesID); + if speciesID and C_PetJournal_GetNumCollectedInfo(speciesID) > 0 and not rawget(CollectedSpeciesHelper, speciesID) then + -- print("not already learned pet") + rawset(CollectedSpeciesHelper, speciesID, 1); + UpdateSearchResults(SearchForField("speciesID", speciesID)); + app:PlayFanfare(); + app:TakeScreenShot(); + wipe(searchCache); + end +end +app.events.PET_JOURNAL_PET_DELETED = function(petID) + -- /dump C_PetJournal.GetPetInfoByPetID("BattlePet-0-00001006503D") + -- local speciesID = select(1, C_PetJournal.GetPetInfoByPetID(petID)); + -- NOTE: Above APIs do not work in the DELETED API, THANKS BLIZZARD + -- print("PET_JOURNAL_PET_DELETED", petID,C_PetJournal.GetPetInfoByPetID(petID)); + + -- Check against all of the collected species for a species that is no longer 1/X + local atLeastOne = false; + for speciesID,collected in pairs(CollectedSpeciesHelper) do + if C_PetJournal_GetNumCollectedInfo(speciesID) < 1 then + rawset(CollectedSpeciesHelper, speciesID, nil); + atLeastOne = true; + end + end + if atLeastOne then + app:PlayRemoveSound(); + app:RefreshData(false, true); + -- wipe(searchCache); -- handled by refresh data + end +end + +local fields = { + ["key"] = function(t) + return "petAbilityID"; + end, + ["text"] = function(t) + return select(2, C_PetBattles_GetAbilityInfoByID(t.petAbilityID)); + end, + ["icon"] = function(t) + return select(3, C_PetBattles_GetAbilityInfoByID(t.petAbilityID)); + end, + ["description"] = function(t) + return select(5, C_PetBattles_GetAbilityInfoByID(t.petAbilityID)); + end, +}; +app.BasePetAbility = app.BaseObjectFields(fields, "BasePetAbility"); +app.CreatePetAbility = function(id, t) + return setmetatable(constructor(id, t, "petAbilityID"), app.BasePetAbility); +end + +local fields = { + ["key"] = function(t) + return "petTypeID"; + end, + ["text"] = function(t) + return _G["BATTLE_PET_NAME_" .. t.petTypeID]; + end, + ["icon"] = function(t) + return "Interface\\Icons\\Icon_PetFamily_"..PET_TYPE_SUFFIX[t.petTypeID]; + end, + ["filterID"] = function(t) + return 101; + end, +}; +app.BasePetType = app.BaseObjectFields(fields, "BasePetType"); +app.CreatePetType = function(id, t) + return setmetatable(constructor(id, t, "petTypeID"), app.BasePetType); +end +end)(); + +-- Category Lib +(function() +local fields = { + ["key"] = function(t) + return "categoryID"; + end, + ["name"] = function(t) + return AllTheThingsAD.LocalizedCategoryNames[t.categoryID] or ("Unknown Category #" .. t.categoryID); + end, + ["icon"] = function(t) + return AllTheThings.CategoryIcons[t.categoryID] or "Interface/ICONS/INV_Garrison_Blueprints1"; + end, +}; +app.BaseCategory = app.BaseObjectFields(fields, "BaseCategory"); +app.CreateCategory = function(id, t) + return setmetatable(constructor(id, t, "categoryID"), app.BaseCategory); +end +end)(); + +-- Character Class Lib +(function() +local class_id_cache = {}; +for i=1,GetNumClasses() do + class_id_cache[select(2, GetClassInfo(i))] = i; +end +local classIcons = { + [1] = "Interface\\Icons\\ClassIcon_Warrior", + [2] = "Interface\\Icons\\ClassIcon_Paladin", + [3] = "Interface\\Icons\\ClassIcon_Hunter", + [4] = "Interface\\Icons\\ClassIcon_Rogue", + [5] = "Interface\\Icons\\ClassIcon_Priest", + [6] = "Interface\\Icons\\ClassIcon_DeathKnight", + [7] = "Interface\\Icons\\ClassIcon_Shaman", + [8] = "Interface\\Icons\\ClassIcon_Mage", + [9] = "Interface\\Icons\\ClassIcon_Warlock", + [10] = "Interface\\Icons\\ClassIcon_Monk", + [11] = "Interface\\Icons\\ClassIcon_Druid", + [12] = "Interface\\Icons\\ClassIcon_DemonHunter", +}; +local GetClassIDFromClassFile = function(classFile) + for i,icon in pairs(classIcons) do + local info = C_CreatureInfo.GetClassInfo(i); + if info and info.classFile == classFile then + return i; end end - return 1, 0 -end -app.GetFactionStandingText = function(standingID) - return app.ColorizeStandingText(standingID, _G["FACTION_STANDING_LABEL" .. standingID] or UNKNOWN); end -app.GetFactionStandingThresholdFromString = function(replevel) - replevel = strtrim(replevel); - for standing=1,8,1 do - if _G["FACTION_STANDING_LABEL" .. standing] == replevel then - return StandingByID[standing].threshold; +app.ClassDB = setmetatable({}, { __index = function(t, className) + for i,_ in pairs(classIcons) do + local info = C_CreatureInfo.GetClassInfo(i); + if info and info.className == className then + rawset(t, className, i); + return i; end end -end -app.IsFactionExclusive = function(factionID) - return factionID == 934 or factionID == 932; -end -local cache = app.CreateCache("factionID"); +end }); +local math_floor = math.floor; +local cache = app.CreateCache("classID"); local function CacheInfo(t, field) local _t, id = cache.GetCached(t); - -- do not attempt caching more than 1 time per factionID since not every cached field may have a cached value - if _t.name then return end - local factionInfo = { GetFactionInfoByID(id) }; - local friendshipInfo = { GetFriendshipReputation(id) }; - local name = factionInfo[1] or friendshipInfo[4]; - local lore = factionInfo[2]; - _t.name = name or (t.creatureID and app.NPCNameFromID[t.creatureID]) or (FACTION .. " #" .. id); - if lore then - _t.lore = lore; - elseif not name then - _t.description = L["FACTION_SPECIFIC_REP"]; - end - if friendshipInfo[1] then - rawset(t, "isFriend", true); - local friendship = friendshipInfo[5]; - if friendship then - if _t.lore then - _t.lore = _t.lore.."\n\n"..friendship; - else - _t.lore = friendship; - end - end + -- specc can be included in the id + local classID = math_floor(id); + rawset(t, "classKey", classID); + local specc_decimal = 1000 * (id - classID); + local specc = math_floor(specc_decimal + 0.00001); + if specc > 0 then + local text = select(2, GetSpecializationInfoForSpecID(specc)); + --[[ + if t.mapID then + text = app.GetMapName(t.mapID) .. " (" .. text .. ")"; + elseif t.maps then + text = app.GetMapName(t.maps[1]) .. " (" .. text .. ")"; + end + --]] + text = "|c" .. t.classColors.colorStr .. text .. "|r"; + rawset(t, "text", text); + _t.text = text; + _t.icon = select(4, GetSpecializationInfoForSpecID(specc)); + else + local text = GetClassInfo(t.classID); + --[[ + if t.mapID then + text = app.GetMapName(t.mapID) .. " (" .. text .. ")"; + elseif t.maps then + text = app.GetMapName(t.maps[1]) .. " (" .. text .. ")"; + end + --]] + text = "|c" .. t.classColors.colorStr .. text .. "|r"; + rawset(t, "text", text); + _t.text = text; + _t.icon = classIcons[t.classID] end if field then return _t[field]; end end local fields = { ["key"] = function(t) - return "factionID"; - end, - ["name"] = function(t) - return cache.GetCachedField(t, "name", CacheInfo); - end, - ["description"] = function(t) - return cache.GetCachedField(t, "description", CacheInfo); + return "classID"; end, - ["lore"] = function(t) - return cache.GetCachedField(t, "lore", CacheInfo); + ["text"] = function(t) + local text = cache.GetCachedField(t, "text", CacheInfo); + if t.mapID then + text = app.GetMapName(t.mapID) .. " (" .. text .. ")"; + elseif t.maps then + text = app.GetMapName(t.maps[1]) .. " (" .. text .. ")"; + end + text = "|c" .. t.classColors.colorStr .. text .. "|r"; + rawset(t, "text", text); + return text; end, ["icon"] = function(t) - return t.achievementID and select(10, GetAchievementInfo(t.achievementID)) - or L["FACTION_ID_ICONS"][t.factionID] - or t.isFriend and select(6, GetFriendshipReputation(t.factionID)) - or app.asset("Category_Factions"); + return cache.GetCachedField(t, "icon", CacheInfo); + -- return classIcons[t.classID]; end, - ["link"] = function(t) - return t.achievementID and GetAchievementLink(t.achievementID); + ["c"] = function(t) + local c = { t.classID }; + rawset(t, "c", c); + return c; end, - ["achievementID"] = function(t) - local achievementID = t.altAchID and app.FactionID == Enum.FlightPathFaction.Horde and t.altAchID or t.achID; - if achievementID then - rawset(t, "achievementID", achievementID); - return achievementID; - end + ["nmc"] = function(t) + return t.classID ~= app.ClassIndex; end, - ["filterID"] = function(t) - return 112; + ["classColors"] = function(t) + return RAID_CLASS_COLORS[select(2, GetClassInfo(t.classID))]; end, - ["trackable"] = app.ReturnTrue, - ["collectible"] = function(t) - if app.CollectibleReputations then - -- If your reputation is higher than the maximum for a different faction, return partial completion. - if not app.AccountWideReputations and t.maxReputation and t.maxReputation[1] ~= t.factionID and (select(3, GetFactionInfoByID(t.maxReputation[1])) or 4) >= app.GetFactionStanding(t.maxReputation[2]) then - return false; - end - return true; - end - return false; +}; +app.BaseCharacterClass = app.BaseObjectFields(fields, "BaseCharacterClass"); +app.CreateCharacterClass = function(id, t) + return setmetatable(constructor(id, t, "classID"), app.BaseCharacterClass); +end +local unitFields = { + ["key"] = function(t) + return "unit"; end, - ["collected"] = function(t) - local factionID = t.factionID; - if app.CurrentCharacter.Factions[factionID] then return 1; end - if t.standing >= t.maxstanding then - app.CurrentCharacter.Factions[factionID] = 1; - ATTAccountWideData.Factions[factionID] = 1; - return 1; - end - local friendID, _, _, _, _, _, _, _, nextFriendThreshold = GetFriendshipReputation(factionID); - if friendID and not nextFriendThreshold then - app.CurrentCharacter.Factions[factionID] = 1; - ATTAccountWideData.Factions[factionID] = 1; - return 1; - end - if app.AccountWideReputations and ATTAccountWideData.Factions[factionID] then return 2; end - - -- If there's an associated achievement, return partial completion. - if t.achievementID and select(4, GetAchievementInfo(t.achievementID)) then - return 2; + ["text"] = function(t) + for guid,character in pairs(ATTCharacterData) do + if guid == t.unit or character.name == t.unit then + rawset(t, "text", character.text); + rawset(t, "level", character.lvl); + if character.classID then + rawset(t, "classID", character.classID); + rawset(t, "class", C_CreatureInfo.GetClassInfo(character.classID).className); + end + if character.raceID then + rawset(t, "raceID", character.raceID); + rawset(t, "race", C_CreatureInfo.GetRaceInfo(character.raceID).raceName); + end + return character.text; + end end - -- If this can be completed by completing a different achievement, return partial completion. - if t.altAchievements then - for i,achID in ipairs(t.altAchievements) do - if select(4, GetAchievementInfo(achID)) then - return 2; - end + local name, realm = UnitName(t.unit); + if name then + if realm and realm ~= "" then name = name .. "-" .. realm; end + local _, classFile, classID = UnitClass(t.unit); + if classFile then + rawset(t, "classID", classID); + name = "|c" .. RAID_CLASS_COLORS[classFile].colorStr .. name .. "|r"; end + return name; end + return t.unit; end, - ["saved"] = function(t) - local factionID = t.factionID; - if app.CurrentCharacter.Factions[factionID] then return 1; end - if t.standing >= t.maxstanding then - app.CurrentCharacter.Factions[factionID] = 1; - ATTAccountWideData.Factions[factionID] = 1; - return 1; + ["icon"] = function(t) + if t.classID then return classIcons[t.classID]; end + end, + ["name"] = function(t) + return UnitName(t.unit); + end, + ["guid"] = function(t) + return UnitGUID(t.unit); + end, + ["title"] = function(t) + if IsInGroup() then + if rawget(t, "isML") then return MASTER_LOOTER; end + if UnitIsGroupLeader(t.unit) then return RAID_LEADER; end end - local friendID, _, _, _, _, _, _, _, nextFriendThreshold = GetFriendshipReputation(factionID); - if friendID and not nextFriendThreshold then - app.CurrentCharacter.Factions[factionID] = 1; - ATTAccountWideData.Factions[factionID] = 1; - return 1; + end, + ["description"] = function(t) + return LEVEL .. " " .. (t.level or RETRIEVING_DATA) .. " " .. (t.race or RETRIEVING_DATA) .. " " .. (t.class or RETRIEVING_DATA); + end, + ["level"] = function(t) + return UnitLevel(t.unit); + end, + ["race"] = function(t) + return UnitRace(t.unit); + end, + ["class"] = function(t) + return UnitClass(t.unit); + end, +}; +app.BaseUnit = app.BaseObjectFields(unitFields, "BaseUnit"); +app.CreateUnit = function(unit, t) + return setmetatable(constructor(unit, t, "unit"), app.BaseUnit); +end +end)(); + +-- Currency Lib +(function() +local C_CurrencyInfo_GetCurrencyInfo = C_CurrencyInfo.GetCurrencyInfo; +local C_CurrencyInfo_GetCurrencyLink = C_CurrencyInfo.GetCurrencyLink; +local cache = app.CreateCache("currencyID"); +local function default_text(t) + return t.link or t.name; +end +local function default_info(t) + return C_CurrencyInfo_GetCurrencyInfo(t.currencyID); +end +local function default_link(t) + return C_CurrencyInfo_GetCurrencyLink(t.currencyID, 1); +end +local function default_costCollectibles(t) + local id = t.currencyID; + if id then + local results = app.SearchForField("currencyIDAsCost", id); + if results and #results > 0 then + -- app.PrintDebug("default_costCollectibles",t.hash,#results) + return results; end + end + return app.EmptyTable; +end +local fields = { + ["key"] = function(t) + return "currencyID"; end, - ["title"] = function(t) - if t.isFriend then - local reputation = t.reputation; - local amount, ceiling = select(2, app.GetFactionStanding(reputation)), t.ceiling; - local title = select(7, GetFriendshipReputation(t.factionID)); - if ceiling then - title = title .. DESCRIPTION_SEPARATOR .. amount .. " / " .. ceiling; - if reputation < 42000 then - return title .. " (" .. (42000 - reputation) .. ")"; - end - end - return title; - else - local reputation = t.reputation; - local amount, ceiling = select(2, app.GetFactionStanding(reputation)), t.ceiling; - local title = _G["FACTION_STANDING_LABEL" .. t.standing]; - if ceiling then - title = title .. DESCRIPTION_SEPARATOR .. amount .. " / " .. ceiling; - if reputation < 42000 then - return title .. " (" .. (42000 - reputation) .. " to " .. _G["FACTION_STANDING_LABEL8"] .. ")"; - end - end - return title; - end + ["_cache"] = function(t) + return cache; end, - ["isFriend"] = function(t) - if select(1, GetFriendshipReputation(t.factionID)) then - rawset(t, "isFriend", true); - return true; - else - rawset(t, "isFriend", false); - return false; - end + ["text"] = function(t) + return cache.GetCachedField(t, "text", default_text); end, - ["reputation"] = function(t) - return select(6, GetFactionInfoByID(t.factionID)); + ["info"] = function(t) + return cache.GetCachedField(t, "info", default_info); end, - ["ceiling"] = function(t) - local _, _, _, m, ma = GetFactionInfoByID(t.factionID); - return ma and m and (ma - m); + ["link"] = function(t) + return cache.GetCachedField(t, "link", default_link); end, - ["standing"] = function(t) - return select(3, GetFactionInfoByID(t.factionID)) or 1; + ["icon"] = function(t) + local info = t.info; + return info and info.iconFileID; end, - ["maxstanding"] = function(t) - if t.minReputation and t.minReputation[1] == t.factionID then - return app.GetFactionStanding(t.minReputation[2]); - end - return 8; + ["name"] = function(t) + local info = t.info; + return info and info.name or ("Currency #" .. t.currencyID); end, - ["sortProgress"] = function(t) - return ((t.reputation or -42000) + 42000) / 84000; + ["costCollectibles"] = function(t) + return cache.GetCachedField(t, "costCollectibles", default_costCollectibles); + end, + ["collectibleAsCost"] = app.CollectibleAsCost, + -- ["collectedAsCost"] = app.CollectedAsCost, + ["costTotal"] = function(t) + return t.collectibleAsCost and 1 or 0; + end, + ["costProgress"] = function(t) + return t.collectedAsCost and 1 or 0; end, }; -app.BaseFaction = app.BaseObjectFields(fields, "BaseFaction"); -app.CreateFaction = function(id, t) - return setmetatable(constructor(id, t, "factionID"), app.BaseFaction); +app.BaseCurrencyClass = app.BaseObjectFields(fields, "BaseCurrencyClass"); +app.CreateCurrencyClass = function(id, t) + return setmetatable(constructor(id, t, "currencyID"), app.BaseCurrencyClass); end -app.OnUpdateReputationRequired = function(t) - -- The only non-regular update processing this group should have - -- is if the User is not in Debug/Account and should not see it due to the reputation requirement not being met - if not app.MODE_DEBUG_OR_ACCOUNT and t.minReputation and (select(6, GetFactionInfoByID(t.minReputation[1])) or 0) < t.minReputation[2] then +end)(); + +-- Death Tracker Lib +(function() +local OnUpdateForDeathTrackerLib = function(t) + if app.MODE_DEBUG then -- app.Settings:Get("Thing:Deaths"); + t.visible = app.GroupVisibilityFilter(t); + local stat = select(1, GetStatistic(60)) or "0"; + if stat == "--" then stat = "0"; end + local deaths = tonumber(stat); + if deaths > 0 and deaths > app.CurrentCharacter.Deaths then + app.CurrentCharacter.Deaths = deaths; + ATTAccountWideData.Deaths = ATTAccountWideData.Deaths + (deaths - app.CurrentCharacter.Deaths); + end + t.parent.progress = t.parent.progress + t.progress; + t.parent.total = t.parent.total + t.total; + else t.visible = false; - return true; end - -- Returns false since we need to just call the regular update group logic return false; end -end)(); - --- Filter Lib -(function() local fields = { ["key"] = function(t) - return "filterID"; + return "deaths"; end, ["text"] = function(t) - return L["FILTER_ID_TYPES"][t.filterID]; - end, - ["name"] = function(t) - return t.text; + return "Total Deaths"; end, ["icon"] = function(t) - return L["FILTER_ID_ICONS"][t.filterID]; + return app.asset("Category_Deaths"); + end, + ["progress"] = function(t) + return math.min(1000, app.AccountWideDeaths and ATTAccountWideData.Deaths or app.CurrentCharacter.Deaths); + end, + ["total"] = function(t) + return 1000; + end, + ["description"] = function(t) + return "The ATT Gods must be sated. Go forth and attempt to level, mortal!\n\n 'Live! Die! Live Again!'\n"; + end, + ["OnTooltip"] = function(t) + local c = {}; + for guid,character in pairs(ATTCharacterData) do + if character and character.Deaths and character.Deaths > 0 then + tinsert(c, character); + end + end + if #c > 0 then + GameTooltip:AddLine(" "); + GameTooltip:AddLine("Deaths Per Character:"); + app.Sort(c, function(a, b) + return a.Deaths > b.Deaths; + end); + for i,data in ipairs(c) do + GameTooltip:AddDoubleLine(" " .. string.gsub(data.text, "-" .. GetRealmName(), ""), data.Deaths, 1, 1, 1); + end + end + end, + ["OnUpdate"] = function(t) + return OnUpdateForDeathTrackerLib; end, }; -app.BaseFilter = app.BaseObjectFields(fields, "BaseFilter"); -app.CreateFilter = function(id, t) - return setmetatable(constructor(id, t, "filterID"), app.BaseFilter); +app.BaseDeathClass = app.BaseObjectFields(fields, "BaseDeathClass"); +app.CreateDeathClass = function() + return setmetatable({}, app.BaseDeathClass); end end)(); --- Flight Path Lib +-- Difficulty Lib (function() -local arrOfNodes = { - 1, -- Durotar (All of Kalimdor) - 36, -- Burning Steppes (All of Eastern Kingdoms) - 94, -- Eversong Woods (and Ghostlands + Isle of Quel'Danas) - 97, -- Azuremyst Isle (and Bloodmyst) - 100, -- Hellfire Peninsula (All of Outland) - 118, -- Icecrown (All of Northrend) - 422, -- Dread Wastes (All of Pandaria) - 525, -- Frostfire Ridge (All of Draenor) - 630, -- Azsuna (All of Broken Isles) - -- Argus only returns specific Flight Points per map - 885, -- Antoran Wastes - 830, -- Krokuun - 882, -- Mac'Aree - 831, -- Upper Deck [The Vindicaar: Krokuun] - 883, -- Upper Deck [The Vindicaar: Mac'Aree] - 886, -- Upper Deck [The Vindicaar: Antoran Wastes] - - 862, -- Zuldazar - 896, -- Drustvar - 1355, -- Nazjatar - 1550, -- The Shadowlands - 1409, -- Exile's Reach +app.DifficultyColors = { + [2] = "ff0070dd", + [5] = "ff0070dd", + [6] = "ff0070dd", + [7] = "ff9d9d9d", + [15] = "ff0070dd", + [16] = "ffa335ee", + [17] = "ff9d9d9d", + [23] = "ffa335ee", + [24] = "ffe6cc80", + [33] = "ffe6cc80", +}; +app.DifficultyIcons = { + [-1] = app.asset("Difficulty_LFR"), + [-2] = app.asset("Difficulty_Normal"), + [-3] = app.asset("Difficulty_Heroic"), + [-4] = app.asset("Difficulty_Mythic"), + [1] = app.asset("Difficulty_Normal"), + [2] = app.asset("Difficulty_Heroic"), + [3] = app.asset("Difficulty_Normal"), + [4] = app.asset("Difficulty_Normal"), + [5] = app.asset("Difficulty_Heroic"), + [6] = app.asset("Difficulty_Heroic"), + [7] = app.asset("Difficulty_LFR"), + [9] = app.asset("Difficulty_Mythic"), + [11] = app.asset("Difficulty_Normal"), + [12] = app.asset("Difficulty_Heroic"), + [14] = app.asset("Difficulty_Normal"), + [15] = app.asset("Difficulty_Heroic"), + [16] = app.asset("Difficulty_Mythic"), + [17] = app.asset("Difficulty_LFR"), + [18] = app.asset("Category_Event"), + [23] = app.asset("Difficulty_Mythic"), + [24] = app.asset("Difficulty_Timewalking"), + [33] = app.asset("Difficulty_Timewalking"), }; -local C_TaxiMap_GetTaxiNodesForMap = C_TaxiMap.GetTaxiNodesForMap; -local C_TaxiMap_GetAllTaxiNodes = C_TaxiMap.GetAllTaxiNodes; -app.CacheFlightPathData = function() - if not app.CacheFlightPathData_Ran then - -- app.DEBUG_PRINT = true; - local newNodes, node = {}; - for i,mapID in ipairs(arrOfNodes) do - -- if mapID == 882 then app.DEBUG_PRINT = true; end - local allNodeData = C_TaxiMap_GetTaxiNodesForMap(mapID); - if allNodeData then - for j,nodeData in ipairs(allNodeData) do - -- if nodeData.nodeID == 63 then app.DEBUG_PRINT = true; end - -- if app.DEBUG_PRINT then app.PrintTable(nodeData) end - node = app.FlightPathDB[nodeData.nodeID]; - if node then - -- if app.DEBUG_PRINT then print("DB node") end - -- associate in-game or our own cached data with the Sourced FP - -- can only apply in-game data when it exists... - if nodeData.name then node.name = nodeData.name; end - if nodeData.faction then - node.faction = nodeData.faction; - elseif nodeData.atlasName then - if nodeData.atlasName == "TaxiNode_Alliance" then - node.faction = 2; - elseif nodeData.atlasName == "TaxiNode_Horde" then - node.faction = 1; - end - end - -- if app.DEBUG_PRINT then app.PrintTable(node) end - elseif nodeData.name and true then -- Turn this off when you're done harvesting. - -- if app.DEBUG_PRINT then print("*NEW* Node") end - node = {}; - node.name = "*NEW* " .. nodeData.name; - if nodeData.faction then - node.faction = nodeData.faction; - elseif nodeData.atlasName then - if nodeData.atlasName == "TaxiNode_Alliance" then - node.faction = 2; - elseif nodeData.atlasName == "TaxiNode_Horde" then - node.faction = 1; - end - end - -- app.PrintTable(node) - app.FlightPathDB[nodeData.nodeID] = node; - newNodes[nodeData.nodeID] = node; - end - -- app.DEBUG_PRINT = nil; - end - end - -- app.DEBUG_PRINT = nil; - end - app.CacheFlightPathData_Ran = true; - SetDataMember("NewFlightPathData", newNodes); - -- return if some new flight path was found - -- print("CacheFlightPathData Found new nodes?",foundNew) - -- app.PrintTable(newNodes); - -- app.DEBUG_PRINT = nil; - return true; - end -end local fields = { ["key"] = function(t) - return "flightPathID"; - end, - ["info"] = function(t) - local info = app.FlightPathDB[t.flightPathID]; - if info then - rawset(t, "info", info); - if info.mapID then app.CacheField(t, "mapID", info.mapID); end - if info.qg then app.CacheField(t, "creatureID", info.qg); end - return info; - end - return app.EmptyTable; + return "difficultyID"; end, - ["name"] = function(t) - return t.info.name or L["VISIT_FLIGHT_MASTER"]; + ["text"] = function(t) + return L["CUSTOM_DIFFICULTIES"][t.difficultyID] or GetDifficultyInfo(t.difficultyID) or "Unknown Difficulty"; end, ["icon"] = function(t) - local r = t.r; - if r then - if r == Enum.FlightPathFaction.Horde then - return app.asset("fp_horde"); + return app.DifficultyIcons[t.difficultyID]; + end, + ["saved"] = function(t) + return t.locks; + end, + ["locks"] = function(t) + local locks = t.parent and t.parent.locks; + if locks then + if t.parent.isLockoutShared and not (t.difficultyID == 7 or t.difficultyID == 17) then + rawset(t, "locks", locks.shared); + return locks.shared; else - return app.asset("fp_alliance"); + -- Look for this difficulty's lockout. + for difficultyKey, lock in pairs(locks) do + if difficultyKey == "shared" then + -- ignore this one + elseif difficultyKey == t.difficultyID then + rawset(t, "locks", lock); + return lock; + end + end end end - return app.asset("fp_neutral"); end, - ["altQuests"] = function(t) - return t.info.altQuests; + ["u"] = function(t) + if t.difficultyID == 24 or t.difficultyID == 33 then + return 1016; + end end, ["description"] = function(t) - local description = t.info.description; - return (description and (description .."\n\n") or "") .. L["FLIGHT_PATHS_DESC"]; - end, - ["collectible"] = function(t) - return app.CollectibleFlightPaths; - end, - ["collected"] = function(t) - if app.CurrentCharacter.FlightPaths[t.flightPathID] then return 1; end - if app.AccountWideFlightPaths and ATTAccountWideData.FlightPaths[t.flightPathID] then return 2; end - if app.MODE_DEBUG_OR_ACCOUNT then return false; end - if t.altQuests then - for i,questID in ipairs(t.altQuests) do - if IsQuestFlaggedCompleted(questID) then - return 2; - end - end + if t.difficultyID == 24 or t.difficultyID == 33 then + return L["WE_JUST_HATE_TIMEWALKING"]; end end, - ["trackable"] = app.ReturnTrue, - ["saved"] = function(t) - return app.CurrentCharacter.FlightPaths[t.flightPathID]; +}; +app.BaseDifficulty = app.BaseObjectFields(fields, "BaseDifficulty"); +app.CreateDifficulty = function(id, t) + return setmetatable(constructor(id, t, "difficultyID"), app.BaseDifficulty); +end +end)(); + +-- Encounter Lib +(function() +local cache = app.CreateCache("encounterID"); +local function CacheInfo(t, field) + local _t, id = cache.GetCached(t); + local name, lore, _, _, link = EJ_GetEncounterInfo(id); + _t.name = name; + _t.lore = lore; + _t.link = link; + _t.displayID = select(4, EJ_GetCreatureInfo(1, id)); + if field then return _t[field]; end +end +local function default_displayInfo(t) + local displayInfos, id, displayInfo = {}, t.encounterID; + for i=1,MAX_CREATURES_PER_ENCOUNTER do + displayInfo = select(4, EJ_GetCreatureInfo(i, id)); + if displayInfo then + tinsert(displayInfos, displayInfo); + else + break; + end + end + return displayInfos; +end +local fields = { + ["key"] = function(t) + return "encounterID"; end, - ["coord"] = function(t) - return t.info.coord; + ["name"] = function(t) + return cache.GetCachedField(t, "name", CacheInfo); end, - ["c"] = function(t) - return t.info.c; + ["lore"] = function(t) + return cache.GetCachedField(t, "lore", CacheInfo); end, - ["r"] = function(t) - local faction = t.info.faction; - if faction and faction > 0 then - return faction; - end + ["link"] = function(t) + return cache.GetCachedField(t, "link", CacheInfo); end, - ["u"] = function(t) - return t.info.u; + ["displayID"] = function(t) + return cache.GetCachedField(t, "displayID", CacheInfo); end, - ["crs"] = function(t) - return t.info.qg and { t.info.qg }; + ["displayInfo"] = function(t) + return cache.GetCachedField(t, "displayInfo", default_displayInfo); end, - ["mapID"] = function(t) - return t.info.mapID; + ["icon"] = function(t) + return app.DifficultyIcons[GetRelativeValue(t, "difficultyID") or 1]; end, - ["nmc"] = function(t) - local c = t.c; - if c and not containsValue(c, app.ClassIndex) then - rawset(t, "nmc", true); -- "Not My Class" - return true; - end - rawset(t, "nmc", false); -- "My Class" - return false; + ["trackable"] = function(t) + return t.questID; end, - ["nmr"] = function(t) - local r = t.r; - return r and r ~= app.FactionID; + ["saved"] = function(t) + -- only consider encounters saved if saved for the current character + return IsQuestFlaggedCompleted(t.questID); end, - ["sourceQuests"] = function(t) - return t.info.sourceQuests; + ["index"] = function(t) + return 1; end, }; -app.BaseFlightPath = app.BaseObjectFields(fields, "BaseFlightPath"); -app.CreateFlightPath = function(id, t) - return setmetatable(constructor(id, t, "flightPathID"), app.BaseFlightPath); +app.BaseEncounter = app.BaseObjectFields(fields, "BaseEncounter"); +app.CreateEncounter = function(id, t) + return setmetatable(constructor(id, t, "encounterID"), app.BaseEncounter); +end +end)(); + +-- Faction Lib +(function() +local GetFriendshipReputation = GetFriendshipReputation; +local StandingByID = { + { -- 1: HATED + ["color"] = GetProgressColor(0), + ["threshold"] = -42000, + }, + { -- 2: HOSTILE + ["color"] = "00FF0000", + ["threshold"] = -6000, + }, + { -- 3: UNFRIENDLY + ["color"] = "00EE6622", + ["threshold"] = -3000, + }, + { -- 4: NEUTRAL + ["color"] = "00FFFF00", + ["threshold"] = 0, + }, + { -- 5: FRIENDLY + ["color"] = "0000FF00", + ["threshold"] = 3000, + }, + { -- 6: HONORED + ["color"] = "0000FF88", + ["threshold"] = 9000, + }, + { -- 7: REVERED + ["color"] = "0000FFCC", + ["threshold"] = 21000, + }, + { -- 8: EXALTED + ["color"] = GetProgressColor(1), + ["threshold"] = 42000, + }, +}; +app.FactionNameByID = setmetatable({}, { __index = function(t, id) + local name = select(1, GetFactionInfoByID(id)) or select(4, GetFriendshipReputation(id)); + if name then + rawset(t, id, name); + rawset(app.FactionIDByName, name, id); + return name; + end +end }); +app.FactionIDByName = setmetatable({}, { __index = function(t, name) + for i=1,3000,1 do + if app.FactionNameByID[i] == name then + rawset(t, name, i); + return i; + end + end +end }); +app.FACTION_RACES = { + [1] = { + 1, -- Human + 3, -- Dwarf + 4, -- Night Elf + 7, -- Gnome + 11, -- Draenei + 22, -- Worgen + 25, -- Pandaren [Alliance] + 29, -- Void Elf + 30, -- Lightforged + 32, -- Kul Tiran + 34, -- Dark Iron + 37, -- Mechagnome + }, + [2] = { + 2, -- Orc + 5, -- Undead + 6, -- Tauren + 8, -- Troll + 9, -- Goblin + 10, -- Blood Elf + 26, -- Pandaren [Horde] + 27, -- Nightborne + 28, -- Highmountain + 31, -- Zandalari + 35, -- Vulpera + 36, -- Mag'har + } +}; +app.ColorizeStandingText = function(standingID, text) + local standing = StandingByID[standingID]; + if standing then + return Colorize(text, standing.color); + else + local rgb = FACTION_BAR_COLORS[standingID]; + return Colorize(text, RGBToHex(rgb.r * 255, rgb.g * 255, rgb.b * 255)); + end end -app.events.TAXIMAP_OPENED = function() - local allNodeData = C_TaxiMap_GetAllTaxiNodes(app.GetCurrentMapID()); - if allNodeData then - local newFPs, nodeID; - local currentCharFPs, acctFPs = app.CurrentCharacter.FlightPaths, ATTAccountWideData.FlightPaths; - for j,nodeData in ipairs(allNodeData) do - if nodeData.state and nodeData.state < 2 then - nodeID = nodeData.nodeID; - if not currentCharFPs[nodeID] then - acctFPs[nodeID] = 1; - currentCharFPs[nodeID] = 1; - if not newFPs then newFPs = { nodeID } - else tinsert(newFPs, nodeID); end - end +app.GetFactionIDByName = function(name) + name = strtrim(name); + return app.FactionIDByName[name] or name; +end +app.GetFactionStanding = function(reputationPoints) + -- Total earned rep from GetFactionInfoByID is a value AWAY FROM ZERO, not a value within the standing bracket. + if reputationPoints then + for i=#StandingByID,1,-1 do + local threshold = StandingByID[i].threshold; + if reputationPoints >= threshold then + return i, threshold < 0 and (threshold - reputationPoints) or (reputationPoints - threshold); end end - UpdateRawIDs("flightPathID", newFPs); end + return 1, 0 end -end)(); - --- Follower Lib -(function() -local C_Garrison_GetFollowerInfo = C_Garrison.GetFollowerInfo; -local C_Garrison_GetFollowerLink = C_Garrison.GetFollowerLink; -local C_Garrison_GetFollowerLinkByID = C_Garrison.GetFollowerLinkByID; -local C_Garrison_IsFollowerCollected = C_Garrison.IsFollowerCollected; - -local cache = app.CreateCache("followerID"); +app.GetFactionStandingText = function(standingID) + return app.ColorizeStandingText(standingID, _G["FACTION_STANDING_LABEL" .. standingID] or UNKNOWN); +end +app.GetFactionStandingThresholdFromString = function(replevel) + replevel = strtrim(replevel); + for standing=1,8,1 do + if _G["FACTION_STANDING_LABEL" .. standing] == replevel then + return StandingByID[standing].threshold; + end + end +end +app.IsFactionExclusive = function(factionID) + return factionID == 934 or factionID == 932; +end +local cache = app.CreateCache("factionID"); local function CacheInfo(t, field) local _t, id = cache.GetCached(t); - local info = C_Garrison_GetFollowerInfo(id); - if info then - _t.name = info.name; - _t.text = info.name; - _t.lvl = info.level; - _t.icon = info.portraitIconID; - _t.title = info.className; - _t.displayID = info.displayIDs and info.displayIDs[1] and info.displayIDs[1].id; + -- do not attempt caching more than 1 time per factionID since not every cached field may have a cached value + if _t.name then return end + local factionInfo = { GetFactionInfoByID(id) }; + local friendshipInfo = { GetFriendshipReputation(id) }; + local name = factionInfo[1] or friendshipInfo[4]; + local lore = factionInfo[2]; + _t.name = name or (t.creatureID and app.NPCNameFromID[t.creatureID]) or (FACTION .. " #" .. id); + if lore then + _t.lore = lore; + elseif not name then + _t.description = L["FACTION_SPECIFIC_REP"]; + end + if friendshipInfo[1] then + rawset(t, "isFriend", true); + local friendship = friendshipInfo[5]; + if friendship then + if _t.lore then + _t.lore = _t.lore.."\n\n"..friendship; + else + _t.lore = friendship; + end + end end if field then return _t[field]; end end local fields = { ["key"] = function(t) - return "followerID"; - end, - ["text"] = function(t) - return cache.GetCachedField(t, "text", CacheInfo); + return "factionID"; end, ["name"] = function(t) return cache.GetCachedField(t, "name", CacheInfo); end, - ["icon"] = function(t) - return cache.GetCachedField(t, "icon", CacheInfo); - end, - ["lvl"] = function(t) - return cache.GetCachedField(t, "lvl", CacheInfo); + ["description"] = function(t) + return cache.GetCachedField(t, "description", CacheInfo); end, - ["title"] = function(t) - return cache.GetCachedField(t, "title", CacheInfo); + ["lore"] = function(t) + return cache.GetCachedField(t, "lore", CacheInfo); end, - ["displayID"] = function(t) - return cache.GetCachedField(t, "displayID", CacheInfo); + ["icon"] = function(t) + return t.achievementID and select(10, GetAchievementInfo(t.achievementID)) + or L["FACTION_ID_ICONS"][t.factionID] + or t.isFriend and select(6, GetFriendshipReputation(t.factionID)) + or app.asset("Category_Factions"); end, ["link"] = function(t) - if app.CurrentCharacter.Followers[t.followerID] then - return C_Garrison_GetFollowerLink(t.followerID); - else - return C_Garrison_GetFollowerLinkByID(t.followerID); + return t.achievementID and GetAchievementLink(t.achievementID); + end, + ["achievementID"] = function(t) + local achievementID = t.altAchID and app.FactionID == Enum.FlightPathFaction.Horde and t.altAchID or t.achID; + if achievementID then + rawset(t, "achievementID", achievementID); + return achievementID; end end, - ["description"] = function(t) - return L["FOLLOWERS_COLLECTION_DESC"]; + ["filterID"] = function(t) + return 112; end, + ["trackable"] = app.ReturnTrue, ["collectible"] = function(t) - return app.CollectibleFollowers; + if app.CollectibleReputations then + -- If your reputation is higher than the maximum for a different faction, return partial completion. + if not app.AccountWideReputations and t.maxReputation and t.maxReputation[1] ~= t.factionID and (select(3, GetFactionInfoByID(t.maxReputation[1])) or 4) >= app.GetFactionStanding(t.maxReputation[2]) then + return false; + end + return true; + end + return false; end, ["collected"] = function(t) - if app.CurrentCharacter.Followers[t.followerID] then return 1; end - if C_Garrison_IsFollowerCollected(t.followerID) then - app.CurrentCharacter.Followers[t.followerID] = 1; - ATTAccountWideData.Followers[t.followerID] = 1; + local factionID = t.factionID; + if app.CurrentCharacter.Factions[factionID] then return 1; end + if t.standing >= t.maxstanding then + app.CurrentCharacter.Factions[factionID] = 1; + ATTAccountWideData.Factions[factionID] = 1; return 1; end - if app.AccountWideFollowers and ATTAccountWideData.Followers[t.followerID] then return 2; end - end, -}; -app.BaseFollower = app.BaseObjectFields(fields, "BaseFollower"); -app.CreateFollower = function(id, t) - return setmetatable(constructor(id, t, "followerID"), app.BaseFollower); -end -end)(); - --- Garrison Lib -(function() -local C_Garrison_GetBuildingInfo = C_Garrison.GetBuildingInfo; -local C_Garrison_GetMissionName = C_Garrison.GetMissionName; -local C_Garrison_GetTalentInfo = C_Garrison.GetTalentInfo; + local friendID, _, _, _, _, _, _, _, nextFriendThreshold = GetFriendshipReputation(factionID); + if friendID and not nextFriendThreshold then + app.CurrentCharacter.Factions[factionID] = 1; + ATTAccountWideData.Factions[factionID] = 1; + return 1; + end + if app.AccountWideReputations and ATTAccountWideData.Factions[factionID] then return 2; end -local cache = app.CreateCache("buildingID"); -local function CacheInfo(t, field) - local _t, id = cache.GetCached(t); - local _, name, _, icon, lore, _, _, _, _, _, uncollected = C_Garrison_GetBuildingInfo(id); - _t.name = name; - _t.text = name; - _t.lore = lore; - _t.icon = _t.icon or icon; - if not uncollected then - app.CurrentCharacter.Buildings[t.buildingID] = 1; - ATTAccountWideData.Buildings[t.buildingID] = 1; - end - -- item on a building can replace fields - if t.itemID then - local _, link, _, _, _, _, _, _, _, icon = GetItemInfo(t.itemID); - if link then - _t.icon = icon or _t.icon; - _t.link = link; + -- If there's an associated achievement, return partial completion. + if t.achievementID and select(4, GetAchievementInfo(t.achievementID)) then + return 2; end - end - if field then return _t[field]; end -end -local fields = { - ["key"] = function(t) - return "buildingID"; - end, - ["text"] = function(t) - return t.link or cache.GetCachedField(t, "text", CacheInfo); + -- If this can be completed by completing a different achievement, return partial completion. + if t.altAchievements then + for i,achID in ipairs(t.altAchievements) do + if select(4, GetAchievementInfo(achID)) then + return 2; + end + end + end end, - ["link"] = function(t) - return cache.GetCachedField(t, "link", CacheInfo); + ["saved"] = function(t) + local factionID = t.factionID; + if app.CurrentCharacter.Factions[factionID] then return 1; end + if t.standing >= t.maxstanding then + app.CurrentCharacter.Factions[factionID] = 1; + ATTAccountWideData.Factions[factionID] = 1; + return 1; + end + local friendID, _, _, _, _, _, _, _, nextFriendThreshold = GetFriendshipReputation(factionID); + if friendID and not nextFriendThreshold then + app.CurrentCharacter.Factions[factionID] = 1; + ATTAccountWideData.Factions[factionID] = 1; + return 1; + end end, - ["name"] = function(t) - return cache.GetCachedField(t, "name", CacheInfo); + ["title"] = function(t) + if t.isFriend then + local reputation = t.reputation; + local amount, ceiling = select(2, app.GetFactionStanding(reputation)), t.ceiling; + local title = select(7, GetFriendshipReputation(t.factionID)); + if ceiling then + title = title .. DESCRIPTION_SEPARATOR .. amount .. " / " .. ceiling; + if reputation < 42000 then + return title .. " (" .. (42000 - reputation) .. ")"; + end + end + return title; + else + local reputation = t.reputation; + local amount, ceiling = select(2, app.GetFactionStanding(reputation)), t.ceiling; + local title = _G["FACTION_STANDING_LABEL" .. t.standing]; + if ceiling then + title = title .. DESCRIPTION_SEPARATOR .. amount .. " / " .. ceiling; + if reputation < 42000 then + return title .. " (" .. (42000 - reputation) .. " to " .. _G["FACTION_STANDING_LABEL8"] .. ")"; + end + end + return title; + end end, - ["lore"] = function(t) - return cache.GetCachedField(t, "lore", CacheInfo); + ["isFriend"] = function(t) + if select(1, GetFriendshipReputation(t.factionID)) then + rawset(t, "isFriend", true); + return true; + else + rawset(t, "isFriend", false); + return false; + end end, - ["icon"] = function(t) - return cache.GetCachedField(t, "icon", CacheInfo); + ["reputation"] = function(t) + return select(6, GetFactionInfoByID(t.factionID)); end, - ["filterID"] = function(t) - return t.itemID and 200; + ["ceiling"] = function(t) + local _, _, _, m, ma = GetFactionInfoByID(t.factionID); + return ma and m and (ma - m); end, - ["collectible"] = function(t) - return t.itemID and app.CollectibleRecipes; + ["standing"] = function(t) + return select(3, GetFactionInfoByID(t.factionID)) or 1; end, - ["collected"] = function(t) - local id = t.buildingID; - if app.CurrentCharacter.Buildings[id] then return 1; end - if not select(11, C_Garrison_GetBuildingInfo(id)) then - app.CurrentCharacter.Buildings[id] = 1; - ATTAccountWideData.Buildings[id] = 1; - return 1; + ["maxstanding"] = function(t) + if t.minReputation and t.minReputation[1] == t.factionID then + return app.GetFactionStanding(t.minReputation[2]); end - if app.AccountWideRecipes and ATTAccountWideData.Buildings[id] then return 2; end - end, -}; -app.BaseGarrisonBuilding = app.BaseObjectFields(fields, "BaseGarrisonBuilding"); -app.CreateGarrisonBuilding = function(id, t) - return setmetatable(constructor(id, t, "buildingID"), app.BaseGarrisonBuilding); -end - -local fields = { - ["key"] = function(t) - return "missionID"; - end, - ["text"] = function(t) - return C_Garrison_GetMissionName(t.missionID); + return 8; end, - ["icon"] = function(t) - return "Interface/ICONS/INV_Icon_Mission_Complete_Order"; + ["sortProgress"] = function(t) + return ((t.reputation or -42000) + 42000) / 84000; end, }; -app.BaseGarrisonMission = app.BaseObjectFields(fields, "BaseGarrisonMission"); -app.CreateGarrisonMission = function(id, t) - return setmetatable(constructor(id, t, "missionID"), app.BaseGarrisonMission); +app.BaseFaction = app.BaseObjectFields(fields, "BaseFaction"); +app.CreateFaction = function(id, t) + return setmetatable(constructor(id, t, "factionID"), app.BaseFaction); +end +app.OnUpdateReputationRequired = function(t) + -- The only non-regular update processing this group should have + -- is if the User is not in Debug/Account and should not see it due to the reputation requirement not being met + if not app.MODE_DEBUG_OR_ACCOUNT and t.minReputation and (select(6, GetFactionInfoByID(t.minReputation[1])) or 0) < t.minReputation[2] then + t.visible = false; + return true; + end + -- Returns false since we need to just call the regular update group logic + return false; end +end)(); +-- Filter Lib +(function() local fields = { ["key"] = function(t) - return "talentID"; - end, - ["info"] = function(t) - return C_Garrison_GetTalentInfo(t.talentID) or {}; + return "filterID"; end, ["text"] = function(t) - return t.info.name; - end, - ["icon"] = function(t) - return t.info.icon or "Interface/ICONS/INV_Icon_Mission_Complete_Order"; + return L["FILTER_ID_TYPES"][t.filterID]; end, - ["description"] = function(t) - return t.info.description; + ["name"] = function(t) + return t.text; end, - ["trackable"] = app.ReturnTrue, - ["saved"] = function(t) - return IsQuestFlaggedCompleted(t.questID) or t.info.researched; + ["icon"] = function(t) + return L["FILTER_ID_ICONS"][t.filterID]; end, }; -app.BaseGarrisonTalent = app.BaseObjectFields(fields, "BaseGarrisonTalent"); -app.CreateGarrisonTalent = function(id, t) - return setmetatable(constructor(id, t, "talentID"), app.BaseGarrisonTalent); +app.BaseFilter = app.BaseObjectFields(fields, "BaseFilter"); +app.CreateFilter = function(id, t) + return setmetatable(constructor(id, t, "filterID"), app.BaseFilter); end end)(); --- Gear Set Lib +-- Flight Path Lib (function() -local C_TransmogSets_GetSetInfo = C_TransmogSets.GetSetInfo; ---[[ 9.1 TEST -C_TransmogSets.GetSetSources = function(setID) - local setAppearances = C_TransmogSets.GetSetPrimaryAppearances(setID); - if not setAppearances then - return nil; - end - local lookupTable = { }; - for i, appearanceInfo in ipairs(setAppearances) do - lookupTable[appearanceInfo.appearanceID] = appearanceInfo.collected; - end - return lookupTable; -end ---]] -local C_TransmogSets_GetSetSources = C_TransmogSets.GetSetSources; -local fields = { - ["key"] = function(t) - return "setID"; - end, - ["info"] = function(t) - return C_TransmogSets_GetSetInfo(t.setID) or {}; - end, - ["text"] = function(t) - return t.info.name; - end, - ["icon"] = function(t) - local sources = t.sources; - if sources then - for sourceID, value in pairs(sources) do - local sourceInfo = C_TransmogCollection_GetSourceInfo(sourceID); - if sourceInfo and sourceInfo.invType == 2 then - local icon = select(5, GetItemInfoInstant(sourceInfo.itemID)); - if icon then rawset(t, "icon", icon); end - return icon; +local arrOfNodes = { + 1, -- Durotar (All of Kalimdor) + 36, -- Burning Steppes (All of Eastern Kingdoms) + 94, -- Eversong Woods (and Ghostlands + Isle of Quel'Danas) + 97, -- Azuremyst Isle (and Bloodmyst) + 100, -- Hellfire Peninsula (All of Outland) + 118, -- Icecrown (All of Northrend) + 422, -- Dread Wastes (All of Pandaria) + 525, -- Frostfire Ridge (All of Draenor) + 630, -- Azsuna (All of Broken Isles) + -- Argus only returns specific Flight Points per map + 885, -- Antoran Wastes + 830, -- Krokuun + 882, -- Mac'Aree + 831, -- Upper Deck [The Vindicaar: Krokuun] + 883, -- Upper Deck [The Vindicaar: Mac'Aree] + 886, -- Upper Deck [The Vindicaar: Antoran Wastes] + + 862, -- Zuldazar + 896, -- Drustvar + 1355, -- Nazjatar + 1550, -- The Shadowlands + 1409, -- Exile's Reach +}; +local C_TaxiMap_GetTaxiNodesForMap = C_TaxiMap.GetTaxiNodesForMap; +local C_TaxiMap_GetAllTaxiNodes = C_TaxiMap.GetAllTaxiNodes; +app.CacheFlightPathData = function() + if not app.CacheFlightPathData_Ran then + -- app.DEBUG_PRINT = true; + local newNodes, node = {}; + for i,mapID in ipairs(arrOfNodes) do + -- if mapID == 882 then app.DEBUG_PRINT = true; end + local allNodeData = C_TaxiMap_GetTaxiNodesForMap(mapID); + if allNodeData then + for j,nodeData in ipairs(allNodeData) do + -- if nodeData.nodeID == 63 then app.DEBUG_PRINT = true; end + -- if app.DEBUG_PRINT then app.PrintTable(nodeData) end + node = app.FlightPathDB[nodeData.nodeID]; + if node then + -- if app.DEBUG_PRINT then print("DB node") end + -- associate in-game or our own cached data with the Sourced FP + -- can only apply in-game data when it exists... + if nodeData.name then node.name = nodeData.name; end + if nodeData.faction then + node.faction = nodeData.faction; + elseif nodeData.atlasName then + if nodeData.atlasName == "TaxiNode_Alliance" then + node.faction = 2; + elseif nodeData.atlasName == "TaxiNode_Horde" then + node.faction = 1; + end + end + -- if app.DEBUG_PRINT then app.PrintTable(node) end + elseif nodeData.name and true then -- Turn this off when you're done harvesting. + -- if app.DEBUG_PRINT then print("*NEW* Node") end + node = {}; + node.name = "*NEW* " .. nodeData.name; + if nodeData.faction then + node.faction = nodeData.faction; + elseif nodeData.atlasName then + if nodeData.atlasName == "TaxiNode_Alliance" then + node.faction = 2; + elseif nodeData.atlasName == "TaxiNode_Horde" then + node.faction = 1; + end + end + -- app.PrintTable(node) + app.FlightPathDB[nodeData.nodeID] = node; + newNodes[nodeData.nodeID] = node; + end + -- app.DEBUG_PRINT = nil; end end + -- app.DEBUG_PRINT = nil; end - return QUESTION_MARK_ICON; - end, - ["description"] = function(t) - local info = t.info; - if info.description then - if info.label then return info.label .. " (" .. info.description .. ")"; end - return info.description; - end - return info.label; - end, - ["header"] = function(t) - return t.info.label; - end, - ["subheader"] = function(t) - return t.info.description; - end, - ["title"] = function(t) - return t.info.requiredFaction; - end, - ["sources"] = function(t) - local sources = C_TransmogSets_GetSetSources(t.setID); - if sources then - rawset(t, "sources", sources); - return sources; - end - end, -}; -app.BaseGearSet = app.BaseObjectFields(fields, "BaseGearSet"); -app.CreateGearSet = function(id, t) - return setmetatable(constructor(id, t, "setID"), app.BaseGearSet); + app.CacheFlightPathData_Ran = true; + SetDataMember("NewFlightPathData", newNodes); + -- return if some new flight path was found + -- print("CacheFlightPathData Found new nodes?",foundNew) + -- app.PrintTable(newNodes); + -- app.DEBUG_PRINT = nil; + return true; + end end - local fields = { ["key"] = function(t) - return "s"; + return "flightPathID"; end, ["info"] = function(t) - return C_TransmogCollection_GetSourceInfo(rawget(t, "s")) or {}; - end, - ["itemID"] = function(t) - local itemID = t.info.itemID; - if itemID then - rawset(t, "itemID", itemID); - return itemID; + local info = app.FlightPathDB[t.flightPathID]; + if info then + rawset(t, "info", info); + if info.mapID then app.CacheField(t, "mapID", info.mapID); end + if info.qg then app.CacheField(t, "creatureID", info.qg); end + return info; end - end, - ["text"] = function(t) - return t.link; - end, - ["link"] = function(t) - return t.itemID and select(2, GetItemInfo(t.itemID)); + return app.EmptyTable; end, ["name"] = function(t) - return t.itemID and select(1, GetItemInfo(t.itemID)); + return t.info.name or L["VISIT_FLIGHT_MASTER"]; end, ["icon"] = function(t) - return t.itemID and select(5, GetItemInfoInstant(t.itemID)); - end, - ["collectible"] = function(t) - return rawget(t, "s") and app.CollectibleTransmog; - end, - ["collected"] = function(t) - return ATTAccountWideData.Sources[rawget(t, "s")]; - end, - ["modItemID"] = function(t) - rawset(t, "modItemID", GetGroupItemIDWithModID(t) or t.itemID); - return rawget(t, "modItemID"); + local r = t.r; + if r then + if r == Enum.FlightPathFaction.Horde then + return app.asset("fp_horde"); + else + return app.asset("fp_alliance"); + end + end + return app.asset("fp_neutral"); end, - ["specs"] = function(t) - return t.itemID and GetFixedItemSpecInfo(t.itemID); + ["altQuests"] = function(t) + return t.info.altQuests; end, - ["invType"] = function(t) - return t.info.invType or 99; + ["description"] = function(t) + local description = t.info.description; + return (description and (description .."\n\n") or "") .. L["FLIGHT_PATHS_DESC"]; end, -}; -app.BaseGearSource = app.BaseObjectFields(fields, "BaseGearSource"); -app.CreateGearSource = function(id) - return setmetatable({ s = id}, app.BaseGearSource); -end - -local fields = { - ["key"] = function(t) - return "setID"; + ["collectible"] = function(t) + return app.CollectibleFlightPaths; end, - ["info"] = function(t) - return C_TransmogSets_GetSetInfo(t.setID) or {}; + ["collected"] = function(t) + if app.CurrentCharacter.FlightPaths[t.flightPathID] then return 1; end + if app.AccountWideFlightPaths and ATTAccountWideData.FlightPaths[t.flightPathID] then return 2; end + if app.MODE_DEBUG_OR_ACCOUNT then return false; end + if t.altQuests then + for i,questID in ipairs(t.altQuests) do + if IsQuestFlaggedCompleted(questID) then + return 2; + end + end + end end, - ["text"] = function(t) - return t.info.label; + ["trackable"] = app.ReturnTrue, + ["saved"] = function(t) + return app.CurrentCharacter.FlightPaths[t.flightPathID]; end, - ["icon"] = function(t) - return t.achievementID and select(10, GetAchievementInfo(t.achievementID)); + ["coord"] = function(t) + return t.info.coord; end, - ["link"] = function(t) - return t.achievementID and GetAchievementLink(t.achievementID); + ["c"] = function(t) + return t.info.c; end, - ["achievementID"] = function(t) - local achievementID = t.altAchID and app.FactionID == Enum.FlightPathFaction.Horde and t.altAchID or t.achID; - if achievementID then - rawset(t, "achievementID", achievementID); - return achievementID; + ["r"] = function(t) + local faction = t.info.faction; + if faction and faction > 0 then + return faction; end end, -}; -app.BaseGearSetHeader = app.BaseObjectFields(fields, "BaseGearSetHeader"); -app.CreateGearSetHeader = function(id, t) - return setmetatable(constructor(id, t, "setID"), app.BaseGearSetHeader); -end - -local fields = { - ["key"] = function(t) - return "setID"; + ["u"] = function(t) + return t.info.u; end, - ["info"] = function(t) - return C_TransmogSets_GetSetInfo(t.setID) or {}; + ["crs"] = function(t) + return t.info.qg and { t.info.qg }; end, - ["text"] = function(t) - return t.info.description; + ["mapID"] = function(t) + return t.info.mapID; end, - ["icon"] = function(t) - return t.achievementID and select(10, GetAchievementInfo(t.achievementID)); + ["nmc"] = function(t) + local c = t.c; + if c and not containsValue(c, app.ClassIndex) then + rawset(t, "nmc", true); -- "Not My Class" + return true; + end + rawset(t, "nmc", false); -- "My Class" + return false; end, - ["link"] = function(t) - return t.achievementID and GetAchievementLink(t.achievementID); + ["nmr"] = function(t) + local r = t.r; + return r and r ~= app.FactionID; end, - ["achievementID"] = function(t) - local achievementID = t.altAchID and app.FactionID == Enum.FlightPathFaction.Horde and t.altAchID or t.achID; - if achievementID then - rawset(t, "achievementID", achievementID); - return achievementID; - end + ["sourceQuests"] = function(t) + return t.info.sourceQuests; end, }; -app.BaseGearSetSubHeader = app.BaseObjectFields(fields, "BaseGearSetSubHeader"); -app.CreateGearSetSubHeader = function(id, t) - return setmetatable(constructor(id, t, "setID"), app.BaseGearSetSubHeader); +app.BaseFlightPath = app.BaseObjectFields(fields, "BaseFlightPath"); +app.CreateFlightPath = function(id, t) + return setmetatable(constructor(id, t, "flightPathID"), app.BaseFlightPath); end -end)(); - --- Holiday Lib -(function() -local function GetHolidayCache() - local cache = GetTempDataMember("HOLIDAY_CACHE"); - if not cache then - cache = {}; - SetTempDataMember("HOLIDAY_CACHE", cache); - SetDataMember("HOLIDAY_CACHE", cache); - local date = C_DateAndTime.GetCurrentCalendarTime(); - if date.month > 8 then - C_Calendar.SetAbsMonth(date.month - 8, date.year); - else - C_Calendar.SetAbsMonth(date.month + 4, date.year - 1); - end - --local date = C_Calendar.GetDate(); - for month=1,12,1 do - -- We kick off the search from January 1 at the start of the year using SetAbsMonth/GetMonthInfo. All successive functions are built from the returns of these. - local absMonth = C_Calendar.SetAbsMonth(month, date.year); - local monthInfo = C_Calendar.GetMonthInfo(absMonth); - for day=1,monthInfo.numDays,1 do - local numEvents = C_Calendar.GetNumDayEvents(0, day); - if numEvents > 0 then - for index=1,numEvents,1 do - local event = C_Calendar.GetDayEvent(0, day, index); - if event then -- If this is nil, then attempting to index it on the same line will toss an error. - if event.calendarType == "HOLIDAY" and (not event.sequenceType or event.sequenceType == "" or event.sequenceType == "START") then - if event.iconTexture then - local t = cache[event.iconTexture]; - if not t then - t = { - ["name"] = event.title, - ["icon"] = event.iconTexture, - ["times"] = {}, - }; - cache[event.iconTexture] = t; - elseif event.iconTexture == 235465 then - -- Harvest Festival and Pilgrims Bounty use the same icon... - t = { - ["name"] = event.title, - ["icon"] = event.iconTexture, - ["times"] = {}, - }; - cache[235466] = t; - end - tinsert(t.times, - { - ["start"] = time({ - year=event.startTime.year, - month=event.startTime.month, - day=event.startTime.monthDay, - hour=event.startTime.hour, - minute=event.startTime.minute, - }), - ["end"] = time({ - year=event.endTime.year, - month=event.endTime.month, - day=event.endTime.monthDay, - hour=event.endTime.hour, - minute=event.endTime.minute, - }), - ["startTime"] = event.startTime, - ["endTime"] = event.endTime, - }); - end - end - end - end +app.events.TAXIMAP_OPENED = function() + local allNodeData = C_TaxiMap_GetAllTaxiNodes(app.GetCurrentMapID()); + if allNodeData then + local newFPs, nodeID; + local currentCharFPs, acctFPs = app.CurrentCharacter.FlightPaths, ATTAccountWideData.FlightPaths; + for j,nodeData in ipairs(allNodeData) do + if nodeData.state and nodeData.state < 2 then + nodeID = nodeData.nodeID; + if not currentCharFPs[nodeID] then + acctFPs[nodeID] = 1; + currentCharFPs[nodeID] = 1; + if not newFPs then newFPs = { nodeID } + else tinsert(newFPs, nodeID); end end end end + UpdateRawIDs("flightPathID", newFPs); end - return cache; -end -local texcoord = { 0.0, 0.7109375, 0.0, 0.7109375 }; -local fields = { - ["key"] = function(t) - return "holidayID"; - end, - ["info"] = function(t) - local info = GetHolidayCache()[t.holidayID]; - if info then - rawset(t, "info", info); - return info; - end - return {}; - end, - ["name"] = function(t) - return t.info.name; - end, - ["text"] = function(t) - return t.info.name; - end, - ["icon"] = function(t) - -- Use the custom icon if defined - if L["HOLIDAY_ID_ICONS"][t.holidayID] then - rawset(t, "icon", L["HOLIDAY_ID_ICONS"][t.holidayID]); - return rawget(t, "icon"); - end - return t.holidayID == 235466 and 235465 or t.holidayID; - end, - ["texcoord"] = function(t) - return not rawget(t, "icon") and texcoord; - end, -}; -app.BaseHoliday = app.BaseObjectFields(fields, "BaseHoliday"); -app.CreateHoliday = function(id, t) - return setmetatable(constructor(id, t, "holidayID"), app.BaseHoliday); end end)(); - --- Illusion Lib --- TODO: add caching for consistency/move to sub-item lib? + +-- Follower Lib (function() +local C_Garrison_GetFollowerInfo = C_Garrison.GetFollowerInfo; +local C_Garrison_GetFollowerLink = C_Garrison.GetFollowerLink; +local C_Garrison_GetFollowerLinkByID = C_Garrison.GetFollowerLinkByID; +local C_Garrison_IsFollowerCollected = C_Garrison.IsFollowerCollected; + +local cache = app.CreateCache("followerID"); +local function CacheInfo(t, field) + local _t, id = cache.GetCached(t); + local info = C_Garrison_GetFollowerInfo(id); + if info then + _t.name = info.name; + _t.text = info.name; + _t.lvl = info.level; + _t.icon = info.portraitIconID; + _t.title = info.className; + _t.displayID = info.displayIDs and info.displayIDs[1] and info.displayIDs[1].id; + end + if field then return _t[field]; end +end local fields = { ["key"] = function(t) - return "illusionID"; - end, - ["filterID"] = function(t) - return 103; + return "followerID"; end, ["text"] = function(t) - if t.itemID then - local name, link = GetItemInfo(t.itemID); - if link then - rawset(t, "name", name); - name = "|cffff80ff[" .. name .. "]|r"; - rawset(t, "link", link); - rawset(t, "text", name); - return name; - end - end - return t.silentLink; + return cache.GetCachedField(t, "text", CacheInfo); end, ["name"] = function(t) - return t.text; + return cache.GetCachedField(t, "name", CacheInfo); end, ["icon"] = function(t) - return "Interface/ICONS/INV_Enchant_Disenchant"; + return cache.GetCachedField(t, "icon", CacheInfo); + end, + ["lvl"] = function(t) + return cache.GetCachedField(t, "lvl", CacheInfo); + end, + ["title"] = function(t) + return cache.GetCachedField(t, "title", CacheInfo); + end, + ["displayID"] = function(t) + return cache.GetCachedField(t, "displayID", CacheInfo); end, ["link"] = function(t) - if t.itemID then - local name, link = GetItemInfo(t.itemID); - if link then - rawset(t, "name", name); - name = "|cffff80ff[" .. name .. "]|r"; - rawset(t, "link", link); - rawset(t, "text", name); - return link; - end + if app.CurrentCharacter.Followers[t.followerID] then + return C_Garrison_GetFollowerLink(t.followerID); + else + return C_Garrison_GetFollowerLinkByID(t.followerID); end end, + ["description"] = function(t) + return L["FOLLOWERS_COLLECTION_DESC"]; + end, ["collectible"] = function(t) - return app.CollectibleIllusions; + return app.CollectibleFollowers; end, ["collected"] = function(t) - return ATTAccountWideData.Illusions[t.illusionID]; - end, - ["silentLink"] = function(t) - --[[ 9.1 TEST - local _, hyperlink = C_TransmogCollection.GetIllusionStrings(t.illusionID); - return hyperlink; - --]] - return select(3, C_TransmogCollection_GetIllusionSourceInfo(t.illusionID)); + if app.CurrentCharacter.Followers[t.followerID] then return 1; end + if C_Garrison_IsFollowerCollected(t.followerID) then + app.CurrentCharacter.Followers[t.followerID] = 1; + ATTAccountWideData.Followers[t.followerID] = 1; + return 1; + end + if app.AccountWideFollowers and ATTAccountWideData.Followers[t.followerID] then return 2; end end, }; -app.BaseIllusion = app.BaseObjectFields(fields, "BaseIllusion"); -app.CreateIllusion = function(id, t) - return setmetatable(constructor(id, t, "illusionID"), app.BaseIllusion); +app.BaseFollower = app.BaseObjectFields(fields, "BaseFollower"); +app.CreateFollower = function(id, t) + return setmetatable(constructor(id, t, "followerID"), app.BaseFollower); end end)(); --- Instance Lib +-- Garrison Lib (function() -local cache = app.CreateCache("instanceID"); +local C_Garrison_GetBuildingInfo = C_Garrison.GetBuildingInfo; +local C_Garrison_GetMissionName = C_Garrison.GetMissionName; +local C_Garrison_GetTalentInfo = C_Garrison.GetTalentInfo; + +local cache = app.CreateCache("buildingID"); local function CacheInfo(t, field) local _t, id = cache.GetCached(t); - local name, lore, _, _, _, icon, _, link = EJ_GetInstanceInfo(id); + local _, name, _, icon, lore, _, _, _, _, _, uncollected = C_Garrison_GetBuildingInfo(id); _t.name = name; + _t.text = name; _t.lore = lore; - _t.icon = icon; - _t.link = link; + _t.icon = _t.icon or icon; + if not uncollected then + app.CurrentCharacter.Buildings[t.buildingID] = 1; + ATTAccountWideData.Buildings[t.buildingID] = 1; + end + -- item on a building can replace fields + if t.itemID then + local _, link, _, _, _, _, _, _, _, icon = GetItemInfo(t.itemID); + if link then + _t.icon = icon or _t.icon; + _t.link = link; + end + end if field then return _t[field]; end end + local fields = { ["key"] = function(t) - return "instanceID"; + return "buildingID"; end, - ["icon"] = function(t) - return cache.GetCachedField(t, "icon", CacheInfo); + ["text"] = function(t) + return t.link or cache.GetCachedField(t, "text", CacheInfo); + end, + ["link"] = function(t) + return cache.GetCachedField(t, "link", CacheInfo); end, ["name"] = function(t) return cache.GetCachedField(t, "name", CacheInfo); @@ -10116,2700 +10455,2363 @@ local fields = { ["lore"] = function(t) return cache.GetCachedField(t, "lore", CacheInfo); end, - ["link"] = function(t) - return cache.GetCachedField(t, "link", CacheInfo); + ["icon"] = function(t) + return cache.GetCachedField(t, "icon", CacheInfo); end, - ["back"] = function(t) - if app.CurrentMapID == t.mapID or (t.maps and contains(t.maps, app.CurrentMapID)) then - return 1; - end + ["filterID"] = function(t) + return t.itemID and 200; end, - ["saved"] = function(t) - return t.locks; + ["collectible"] = function(t) + return t.itemID and app.CollectibleRecipes; end, - ["locks"] = function(t) - local locks = app.CurrentCharacter.Lockouts[t.name]; - if locks then - rawset(t, "locks", locks); - return locks; + ["collected"] = function(t) + local id = t.buildingID; + if app.CurrentCharacter.Buildings[id] then return 1; end + if not select(11, C_Garrison_GetBuildingInfo(id)) then + app.CurrentCharacter.Buildings[id] = 1; + ATTAccountWideData.Buildings[id] = 1; + return 1; end + if app.AccountWideRecipes and ATTAccountWideData.Buildings[id] then return 2; end end, - ["isLockoutShared"] = app.ReturnFalse, }; -app.BaseInstance = app.BaseObjectFields(fields, "BaseInstance"); -app.CreateInstance = function(id, t) - return setmetatable(constructor(id, t, "instanceID"), app.BaseInstance); +app.BaseGarrisonBuilding = app.BaseObjectFields(fields, "BaseGarrisonBuilding"); +app.CreateGarrisonBuilding = function(id, t) + return setmetatable(constructor(id, t, "buildingID"), app.BaseGarrisonBuilding); end -end)(); --- Item Lib -(function() --- TODO: Once Item information is stored in a single source table, this mechanism can reference that instead of using a cache table here -local cache = app.CreateCache("modItemID"); --- Consolidated function to handle how many retries for information an Item may have -local function HandleItemRetries(t) - local _t, id = cache.GetCached(t); - local retries = rawget(_t, "retries"); - if retries then - if retries > app.MaximumItemInfoRetries then - local itemName = "Item #" .. tostring(id) .. "*"; - rawset(_t, "title", L["FAILED_ITEM_INFO"]); - rawset(_t, "link", nil); - rawset(_t, "s", nil); - -- print("itemRetriesMax",itemName,rawget(t, "retries")) - -- save the "name" field in the source group to prevent further requests to the cache - rawset(t, "name", itemName); - return itemName; - else - rawset(_t, "retries", retries + 1); - end - else - rawset(_t, "retries", 1); - end -end --- Consolidated function to cache available Item information -local function RawSetItemInfoFromLink(t, link) - local name, link, quality, _, _, _, _, _, _, icon, _, _, _, b = GetItemInfo(link); - if link then - --[[ -- Debug Prints - local _t, id = cache.GetCached(t); - print("rawset item info",id,link,name,quality,b) - --]] - t = cache.GetCached(t); - rawset(t, "retries", nil); - rawset(t, "name", name); - rawset(t, "link", link); - rawset(t, "icon", icon); - rawset(t, "q", quality); - if quality > 6 then - -- heirlooms return as 1 but are technically BoE for our concern - rawset(t, "b", 2); - else - rawset(t, "b", b); - end - return link; - else - HandleItemRetries(t); - end -end -local function default_link(t) - -- item already has a pre-determined itemLink so use that - if t.rawlink then return RawSetItemInfoFromLink(t, t.rawlink); end - -- need to 'create' a valid accurate link for this item - local itemLink = t.itemID; - if itemLink then - local bonusID = t.bonusID; - local modID = t.modID; - if not bonusID or bonusID < 1 then - bonusID = nil; - end - if not modID or modID < 1 then - modID = nil; - end - if bonusID and modID then - itemLink = sformat("item:%d:::::::::::%d:1:%d:", itemLink, modID, bonusID); - elseif bonusID then - itemLink = sformat("item:%d::::::::::::1:%d:", itemLink, bonusID); - elseif modID then - -- bonusID 3524 seems to imply "use ModID to determine SourceID" since without it, everything with ModID resolves as the base SourceID from links - itemLink = sformat("item:%d:::::::::::%d:1:3524:", itemLink, modID); - else - itemLink = sformat("item:%d:::::::::::::", itemLink); - end - -- save this link so it doesn't need to be built again - rawset(t, "rawlink", itemLink); - return RawSetItemInfoFromLink(t, itemLink); - end -end -local function default_icon(t) - return t.itemID and select(5, GetItemInfoInstant(t.itemID)) or "Interface\\Icons\\INV_Misc_QuestionMark"; +local fields = { + ["key"] = function(t) + return "missionID"; + end, + ["text"] = function(t) + return C_Garrison_GetMissionName(t.missionID); + end, + ["icon"] = function(t) + return "Interface/ICONS/INV_Icon_Mission_Complete_Order"; + end, +}; +app.BaseGarrisonMission = app.BaseObjectFields(fields, "BaseGarrisonMission"); +app.CreateGarrisonMission = function(id, t) + return setmetatable(constructor(id, t, "missionID"), app.BaseGarrisonMission); end -local function default_specs(t) - return GetFixedItemSpecInfo(t.itemID); + +local fields = { + ["key"] = function(t) + return "talentID"; + end, + ["info"] = function(t) + -- TODO: use cache + return C_Garrison_GetTalentInfo(t.talentID) or {}; + end, + ["text"] = function(t) + return t.info.name; + end, + ["icon"] = function(t) + return t.info.icon or "Interface/ICONS/INV_Icon_Mission_Complete_Order"; + end, + ["description"] = function(t) + return t.info.description; + end, + ["trackable"] = app.ReturnTrue, + ["saved"] = function(t) + return IsQuestFlaggedCompleted(t.questID) or t.info.researched; + end, +}; +app.BaseGarrisonTalent = app.BaseObjectFields(fields, "BaseGarrisonTalent"); +app.CreateGarrisonTalent = function(id, t) + return setmetatable(constructor(id, t, "talentID"), app.BaseGarrisonTalent); end -local function default_costCollectibles(t) - local results, id; - local modItemID = t.modItemID; - -- Search by modItemID if possible for accuracy - if modItemID and modItemID ~= t.itemID then - id = modItemID; - results = app.SearchForField("itemIDAsCost", id); - -- if app.DEBUG_PRINT then print("itemIDAsCost.modItemID",id,results and #results) end - end - -- If no results, search by itemID + modID only if different - if not results then - id = GetGroupItemIDWithModID(nil, t.itemID, t.modID); - if id ~= modItemID then - results = app.SearchForField("itemIDAsCost", id); - -- if app.DEBUG_PRINT then print("itemIDAsCost.modID",id,results and #results) end - end - end - -- If no results, search by plain itemID only - if not results and t.itemID then - id = t.itemID; - results = app.SearchForField("itemIDAsCost", id); +end)(); + +-- Gear Set Lib +(function() +local C_TransmogSets_GetSetInfo = C_TransmogSets.GetSetInfo; +--[[ 9.1 TEST +C_TransmogSets.GetSetSources = function(setID) + local setAppearances = C_TransmogSets.GetSetPrimaryAppearances(setID); + if not setAppearances then + return nil; end - if results and #results > 0 then - -- not sure we need to copy these into another table - -- app.PrintDebug("default_costCollectibles",t.hash,id,#results) - return results; + local lookupTable = { }; + for i, appearanceInfo in ipairs(setAppearances) do + lookupTable[appearanceInfo.appearanceID] = appearanceInfo.collected; end - return app.EmptyTable; + return lookupTable; end -local itemFields = { +--]] +local C_TransmogSets_GetSetSources = C_TransmogSets.GetSetSources; +local fields = { ["key"] = function(t) - return "itemID"; + return "setID"; end, - ["_cache"] = function(t) - return cache; + ["info"] = function(t) + return C_TransmogSets_GetSetInfo(t.setID) or {}; end, ["text"] = function(t) - return t.link or t.name; + return t.info.name; end, ["icon"] = function(t) - return cache.GetCachedField(t, "icon", default_icon); - end, - ["link"] = function(t) - return cache.GetCachedField(t, "link", default_link); + local sources = t.sources; + if sources then + for sourceID, value in pairs(sources) do + local sourceInfo = C_TransmogCollection_GetSourceInfo(sourceID); + if sourceInfo and sourceInfo.invType == 2 then + local icon = select(5, GetItemInfoInstant(sourceInfo.itemID)); + if icon then rawset(t, "icon", icon); end + return icon; + end + end + end + return QUESTION_MARK_ICON; end, - ["name"] = function(t) - return cache.GetCachedField(t, "name"); + ["description"] = function(t) + local info = t.info; + if info.description then + if info.label then return info.label .. " (" .. info.description .. ")"; end + return info.description; + end + return info.label; end, - ["specs"] = function(t) - return cache.GetCachedField(t, "specs", default_specs); + ["header"] = function(t) + return t.info.label; end, - ["retries"] = function(t) - return cache.GetCachedField(t, "retries"); + ["subheader"] = function(t) + return t.info.description; end, - ["q"] = function(t) - return cache.GetCachedField(t, "q"); + ["title"] = function(t) + return t.info.requiredFaction; end, - ["b"] = function(t) - return cache.GetCachedField(t, "b") or 2; + ["sources"] = function(t) + local sources = C_TransmogSets_GetSetSources(t.setID); + if sources then + rawset(t, "sources", sources); + return sources; + end end, - ["title"] = function(t) - return cache.GetCachedField(t, "title"); +}; +app.BaseGearSet = app.BaseObjectFields(fields, "BaseGearSet"); +app.CreateGearSet = function(id, t) + return setmetatable(constructor(id, t, "setID"), app.BaseGearSet); +end + +local fields = { + ["key"] = function(t) + return "s"; end, - ["f"] = function(t) - -- Unknown item type after Parser, so make sure we save the filter for later references - rawset(t, "f", -1); - return rawget(t, "f"); + ["info"] = function(t) + return C_TransmogCollection_GetSourceInfo(rawget(t, "s")) or {}; end, - ["tsm"] = function(t) - local itemLink = t.itemID; - if itemLink then - local bonusID = t.bonusID; - if bonusID and bonusID > 0 then - return sformat("i:%d:0:1:%d", itemLink, bonusID); - --elseif t.modID then - -- NOTE: At this time, TSM3 does not support modID. (RIP) - --return sformat("i:%d:%d:1:3524", itemLink, t.modID); - end - return sformat("i:%d", itemLink); + ["itemID"] = function(t) + local itemID = t.info.itemID; + if itemID then + rawset(t, "itemID", itemID); + return itemID; end end, - ["repeatable"] = function(t) - return rawget(t, "isDaily") or rawget(t, "isWeekly") or rawget(t, "isMonthly") or rawget(t, "isYearly") or rawget(t, "isWorldQuest"); + ["text"] = function(t) + return t.link; + end, + ["link"] = function(t) + return t.itemID and select(2, GetItemInfo(t.itemID)); + end, + ["name"] = function(t) + return t.itemID and select(1, GetItemInfo(t.itemID)); + end, + ["icon"] = function(t) + return t.itemID and select(5, GetItemInfoInstant(t.itemID)); + end, + ["collectible"] = function(t) + return rawget(t, "s") and app.CollectibleTransmog; + end, + ["collected"] = function(t) + return ATTAccountWideData.Sources[rawget(t, "s")]; end, ["modItemID"] = function(t) rawset(t, "modItemID", GetGroupItemIDWithModID(t) or t.itemID); return rawget(t, "modItemID"); end, - ["indicatorIcon"] = function(t) - return app.GetQuestIndicator(t); + ["specs"] = function(t) + return t.itemID and GetFixedItemSpecInfo(t.itemID); end, - ["trackableAsQuest"] = app.ReturnTrue, - ["collectibleAsAchievement"] = function(t) - return app.CollectibleAchievements; + ["invType"] = function(t) + return t.info.invType or 99; end, - ["costCollectibles"] = function(t) - return cache.GetCachedField(t, "costCollectibles", default_costCollectibles); +}; +app.BaseGearSource = app.BaseObjectFields(fields, "BaseGearSource"); +app.CreateGearSource = function(id) + return setmetatable({ s = id}, app.BaseGearSource); +end + +local fields = { + ["key"] = function(t) + return "setID"; end, - ["collectibleAsCost"] = app.CollectibleAsCost, - ["collectedAsCost"] = app.CollectedAsCost, - ["costsCount"] = function(t) - if t.costCollectibles then return #t.costCollectibles; end + ["info"] = function(t) + return C_TransmogSets_GetSetInfo(t.setID) or {}; end, - ["collectibleAsFaction"] = function(t) - return app.CollectibleReputations; + ["text"] = function(t) + return t.info.label; end, - ["collectibleAsFactionOrQuest"] = function(t) - return app.CollectibleReputations or t.collectibleAsQuest; + ["icon"] = function(t) + return t.achievementID and select(10, GetAchievementInfo(t.achievementID)); end, - ["collectibleAsTransmog"] = function(t) - return app.CollectibleTransmog; + ["link"] = function(t) + return t.achievementID and GetAchievementLink(t.achievementID); end, - ["collectibleAsQuest"] = app.CollectibleAsQuest, - ["collectedAsQuest"] = IsQuestFlaggedCompletedForObject, - ["lockedAsQuest"] = function(t) - return app.LockedAsQuest(t); + ["achievementID"] = function(t) + local achievementID = t.altAchID and app.FactionID == Enum.FlightPathFaction.Horde and t.altAchID or t.achID; + if achievementID then + rawset(t, "achievementID", achievementID); + return achievementID; + end end, - ["collectedAsFaction"] = function(t) - if t.factionID then - if t.repeatable then - -- This is used by reputation tokens. (turn in items) - -- quick cache checks - if app.CurrentCharacter.Factions[t.factionID] then return 1; end - if app.AccountWideReputations and ATTAccountWideData.Factions[t.factionID] then return 2; end - - -- use the extended faction logic from the associated Faction for consistency - local cachedFaction = app.SearchForObject("factionID", t.factionID); - if cachedFaction then return cachedFaction.collected; end +}; +app.BaseGearSetHeader = app.BaseObjectFields(fields, "BaseGearSetHeader"); +app.CreateGearSetHeader = function(id, t) + return setmetatable(constructor(id, t, "setID"), app.BaseGearSetHeader); +end - -- otherwise move on to the basic logic - if select(3, GetFactionInfoByID(t.factionID)) == 8 then - app.CurrentCharacter.Factions[t.factionID] = 1; - ATTAccountWideData.Factions[t.factionID] = 1; - return 1; - end - else - -- This is used for the Grand Commendations unlocking Bonus Reputation - if ATTAccountWideData.FactionBonus[t.factionID] then return 1; end - if select(15, GetFactionInfoByID(t.factionID)) then - ATTAccountWideData.FactionBonus[t.factionID] = 1; - return 1; - end - end - end +local fields = { + ["key"] = function(t) + return "setID"; end, - ["collectedAsFactionOrQuest"] = function(t) - return t.collectedAsFaction or t.collectedAsQuest; + ["info"] = function(t) + return C_TransmogSets_GetSetInfo(t.setID) or {}; end, - ["collectedAsTransmog"] = function(t) - return ATTAccountWideData.Sources[rawget(t, "s")]; + ["text"] = function(t) + return t.info.description; end, - ["savedAsQuest"] = function(t) - return IsQuestFlaggedCompleted(t.questID); + ["icon"] = function(t) + return t.achievementID and select(10, GetAchievementInfo(t.achievementID)); end, - ["costTotal"] = function(t) - return t.collectibleAsCost and 1 or 0; + ["link"] = function(t) + return t.achievementID and GetAchievementLink(t.achievementID); end, - ["costProgress"] = function(t) - return t.collectedAsCost and 1 or 0; + ["achievementID"] = function(t) + local achievementID = t.altAchID and app.FactionID == Enum.FlightPathFaction.Horde and t.altAchID or t.achID; + if achievementID then + rawset(t, "achievementID", achievementID); + return achievementID; + end end, }; -app.BaseItem = app.BaseObjectFields(itemFields, "BaseItem"); - -local fields = RawCloneData(itemFields); -fields.collectible = itemFields.collectibleAsAchievement; -fields.collected = itemFields.collectedAsAchievement; -app.BaseItemWithAchievementID = app.BaseObjectFields(fields, "BaseItemWithAchievementID"); - -local fields = RawCloneData(itemFields); -fields.collectible = itemFields.collectibleAsFaction; -fields.collected = itemFields.collectedAsFaction; -app.BaseItemWithFactionID = app.BaseObjectFields(fields, "BaseItemWithFactionID"); - -local fields = RawCloneData(itemFields); -fields.collectible = itemFields.collectibleAsQuest; -fields.collected = itemFields.collectedAsQuest; -fields.trackable = itemFields.trackableAsQuest; -fields.saved = itemFields.savedAsQuest; -fields.locked = itemFields.lockedAsQuest; -app.BaseItemWithQuestID = app.BaseObjectFields(fields, "BaseItemWithQuestID"); - -local fields = RawCloneData(itemFields); -fields.collectible = itemFields.collectibleAsFactionOrQuest; -fields.collected = itemFields.collectedAsFactionOrQuest; -fields.trackable = itemFields.trackableAsQuest; -fields.saved = itemFields.savedAsQuest; -fields.locked = itemFields.lockedAsQuest; -app.BaseItemWithQuestIDAndFactionID = app.BaseObjectFields(fields, "BaseItemWithQuestIDAndFactionID"); - -local fields = RawCloneData(itemFields); -fields.collectible = function(t) - return app.CollectibleTransmog; +app.BaseGearSetSubHeader = app.BaseObjectFields(fields, "BaseGearSetSubHeader"); +app.CreateGearSetSubHeader = function(id, t) + return setmetatable(constructor(id, t, "setID"), app.BaseGearSetSubHeader); end -fields.collected = function(t) - if t.itemID then - if GetItemCount(t.itemID, true) > 0 then - app.CurrentCharacter.CommonItems[t.itemID] = 1; - ATTAccountWideData.CommonItems[t.itemID] = 1; - return 1; - elseif app.CurrentCharacter.CommonItems[t.itemID] == 1 then - app.CurrentCharacter.CommonItems[t.itemID] = nil; - ATTAccountWideData.CommonItems[t.itemID] = nil; - for guid,characterData in pairs(ATTCharacterData) do - if characterData.CommonItems and characterData.CommonItems[t.itemID] then - ATTAccountWideData.CommonItems[t.itemID] = 1; +end)(); + +-- Holiday Lib +(function() +local function GetHolidayCache() + local cache = GetTempDataMember("HOLIDAY_CACHE"); + if not cache then + cache = {}; + SetTempDataMember("HOLIDAY_CACHE", cache); + SetDataMember("HOLIDAY_CACHE", cache); + local date = C_DateAndTime.GetCurrentCalendarTime(); + if date.month > 8 then + C_Calendar.SetAbsMonth(date.month - 8, date.year); + else + C_Calendar.SetAbsMonth(date.month + 4, date.year - 1); + end + --local date = C_Calendar.GetDate(); + for month=1,12,1 do + -- We kick off the search from January 1 at the start of the year using SetAbsMonth/GetMonthInfo. All successive functions are built from the returns of these. + local absMonth = C_Calendar.SetAbsMonth(month, date.year); + local monthInfo = C_Calendar.GetMonthInfo(absMonth); + for day=1,monthInfo.numDays,1 do + local numEvents = C_Calendar.GetNumDayEvents(0, day); + if numEvents > 0 then + for index=1,numEvents,1 do + local event = C_Calendar.GetDayEvent(0, day, index); + if event then -- If this is nil, then attempting to index it on the same line will toss an error. + if event.calendarType == "HOLIDAY" and (not event.sequenceType or event.sequenceType == "" or event.sequenceType == "START") then + if event.iconTexture then + local t = cache[event.iconTexture]; + if not t then + t = { + ["name"] = event.title, + ["icon"] = event.iconTexture, + ["times"] = {}, + }; + cache[event.iconTexture] = t; + elseif event.iconTexture == 235465 then + -- Harvest Festival and Pilgrims Bounty use the same icon... + t = { + ["name"] = event.title, + ["icon"] = event.iconTexture, + ["times"] = {}, + }; + cache[235466] = t; + end + tinsert(t.times, + { + ["start"] = time({ + year=event.startTime.year, + month=event.startTime.month, + day=event.startTime.monthDay, + hour=event.startTime.hour, + minute=event.startTime.minute, + }), + ["end"] = time({ + year=event.endTime.year, + month=event.endTime.month, + day=event.endTime.monthDay, + hour=event.endTime.hour, + minute=event.endTime.minute, + }), + ["startTime"] = event.startTime, + ["endTime"] = event.endTime, + }); + end + end + end + end end end end - if ATTAccountWideData.CommonItems[t.itemID] then - return 2; - end end + return cache; end -app.BaseCommonItem = app.BaseObjectFields(fields, "BaseCommonItem"); - --- Appearance Lib (Item Source) -local fields = RawCloneData(itemFields); -fields.key = function(t) return "s"; end; --- TODO: if PL filter is ever a thing investigate https://wowpedia.fandom.com/wiki/API_C_TransmogCollection.PlayerCanCollectSource -fields.collectible = itemFields.collectibleAsTransmog; -fields.collected = itemFields.collectedAsTransmog; -app.BaseItemSource = app.BaseObjectFields(fields, "BaseItemSource"); - -app.CreateItemSource = function(sourceID, itemID, t) - t = setmetatable(constructor(sourceID, t, "s"), app.BaseItemSource); - t.itemID = itemID; - return t; -end -app.CreateItem = function(id, t) - if t then - if rawget(t, "s") then - return setmetatable(constructor(id, t, "itemID"), app.BaseItemSource); - elseif rawget(t, "factionID") then - if rawget(t, "questID") then - return setmetatable(constructor(id, t, "itemID"), app.BaseItemWithQuestIDAndFactionID); - else - return setmetatable(constructor(id, t, "itemID"), app.BaseItemWithFactionID); - end - elseif rawget(t, "questID") then - return setmetatable(constructor(id, t, "itemID"), app.BaseItemWithQuestID); - elseif rawget(t, "achID") then - rawset(t, "achievementID", app.FactionID == Enum.FlightPathFaction.Horde and rawget(t, "altAchID") or rawget(t, "achID")); - return setmetatable(constructor(id, t, "itemID"), app.BaseItemWithAchievementID); +local texcoord = { 0.0, 0.7109375, 0.0, 0.7109375 }; +local fields = { + ["key"] = function(t) + return "holidayID"; + end, + ["info"] = function(t) + local info = GetHolidayCache()[t.holidayID]; + if info then + rawset(t, "info", info); + return info; end - end - return setmetatable(constructor(id, t, "itemID"), app.BaseItem); -end - --- Runeforge Legendary Lib -(function() --- copy base Item fields -local fields = RawCloneData(itemFields); --- Runeforge Legendary differences -local C_LegendaryCrafting_GetRuneforgePowerInfo = C_LegendaryCrafting.GetRuneforgePowerInfo; -fields.key = function(t) return "runeforgePowerID"; end; -fields.collectible = function(t) return app.CollectibleRuneforgeLegendaries; end; -fields.collectibleAsCost = app.ReturnFalse; -fields.collected = function(t) - local rfID = t.runeforgePowerID; - -- account-wide collected - if ATTAccountWideData.RuneforgeLegendaries[rfID] then return 1; end - -- fresh collected - local state = (C_LegendaryCrafting_GetRuneforgePowerInfo(rfID) or app.EmptyTable).state; - if state == 0 then - ATTAccountWideData.RuneforgeLegendaries[rfID] = 1; - return 1; - end -end; -fields.lvl = function(t) return 60; end; -app.BaseRuneforgeLegendary = app.BaseObjectFields(fields, "BaseRuneforgeLegendary"); -app.CreateRuneforgeLegendary = function(id, t) - return setmetatable(constructor(id, t, "runeforgePowerID"), app.BaseRuneforgeLegendary); -end -end)(); - --- Conduit Lib -(function() -local C_Soulbinds_GetConduitCollectionData = C_Soulbinds.GetConduitCollectionData; --- copy base Item fields -local fields = RawCloneData(itemFields); --- Conduit differences -fields.key = function(t) return "conduitID"; end; -fields.collectible = function(t) return app.CollectibleConduits; end; -fields.collectibleAsCost = app.ReturnFalse; -fields.collected = function(t) - local cID = t.conduitID; - -- character collected - if app.CurrentCharacter.Conduits[cID] then return 1; end - -- account-wide collected - if app.AccountWideConduits and ATTAccountWideData.Conduits[cID] then return 2; end - -- fresh collected - local state = C_Soulbinds_GetConduitCollectionData(cID); - if state ~= nil then - app.CurrentCharacter.Conduits[cID] = 1; - ATTAccountWideData.Conduits[cID] = 1; - return 1; - end -end; -fields.lvl = function(t) return 60; end; -app.BaseConduit = app.BaseObjectFields(fields, "BaseConduit"); -app.CreateConduit = function(id, t) - return setmetatable(constructor(id, t, "conduitID"), app.BaseConduit); + return {}; + end, + ["name"] = function(t) + return t.info.name; + end, + ["text"] = function(t) + return t.info.name; + end, + ["icon"] = function(t) + -- Use the custom icon if defined + if L["HOLIDAY_ID_ICONS"][t.holidayID] then + rawset(t, "icon", L["HOLIDAY_ID_ICONS"][t.holidayID]); + return rawget(t, "icon"); + end + return t.holidayID == 235466 and 235465 or t.holidayID; + end, + ["texcoord"] = function(t) + return not rawget(t, "icon") and texcoord; + end, +}; +app.BaseHoliday = app.BaseObjectFields(fields, "BaseHoliday"); +app.CreateHoliday = function(id, t) + return setmetatable(constructor(id, t, "holidayID"), app.BaseHoliday); end end)(); --- Heirloom Lib +-- Illusion Lib +-- TODO: add caching for consistency/move to sub-item lib? (function() -local C_Heirloom_GetHeirloomInfo = C_Heirloom.GetHeirloomInfo; -local C_Heirloom_GetHeirloomLink = C_Heirloom.GetHeirloomLink; -local C_Heirloom_PlayerHasHeirloom = C_Heirloom.PlayerHasHeirloom; -local C_Heirloom_GetHeirloomMaxUpgradeLevel = C_Heirloom.GetHeirloomMaxUpgradeLevel; -local heirloomIDs = {}; local fields = { ["key"] = function(t) - return "heirloomUnlockID"; + return "illusionID"; + end, + ["filterID"] = function(t) + return 103; end, ["text"] = function(t) - return L["HEIRLOOM_TEXT"]; + if t.itemID then + local name, link = GetItemInfo(t.itemID); + if link then + rawset(t, "name", name); + name = "|cffff80ff[" .. name .. "]|r"; + rawset(t, "link", link); + rawset(t, "text", name); + return name; + end + end + return t.silentLink; + end, + ["name"] = function(t) + return t.text; end, ["icon"] = function(t) - return "Interface/ICONS/Achievement_GuildPerk_WorkingOvertime_Rank2"; + return "Interface/ICONS/INV_Enchant_Disenchant"; end, - ["description"] = function(t) - return L["HEIRLOOM_TEXT_DESC"]; + ["link"] = function(t) + if t.itemID then + local name, link = GetItemInfo(t.itemID); + if link then + rawset(t, "name", name); + name = "|cffff80ff[" .. name .. "]|r"; + rawset(t, "link", link); + rawset(t, "text", name); + return link; + end + end end, ["collectible"] = function(t) - return app.CollectibleHeirlooms; + return app.CollectibleIllusions; end, - ["saved"] = function(t) - return C_Heirloom_PlayerHasHeirloom(t.heirloomUnlockID); + ["collected"] = function(t) + return ATTAccountWideData.Illusions[t.illusionID]; + end, + ["silentLink"] = function(t) + --[[ 9.1 TEST + local _, hyperlink = C_TransmogCollection.GetIllusionStrings(t.illusionID); + return hyperlink; + --]] + return select(3, C_TransmogCollection_GetIllusionSourceInfo(t.illusionID)); end, - ["trackable"] = app.ReturnTrue, }; -fields.collected = fields.saved; -app.BaseHeirloomUnlocked = app.BaseObjectFields(fields, "BaseHeirloomUnlocked"); +app.BaseIllusion = app.BaseObjectFields(fields, "BaseIllusion"); +app.CreateIllusion = function(id, t) + return setmetatable(constructor(id, t, "illusionID"), app.BaseIllusion); +end +end)(); -local armorTextures = { - "Interface/ICONS/INV_Icon_HeirloomToken_Armor01", - "Interface/ICONS/INV_Icon_HeirloomToken_Armor02", - "Interface/ICONS/Inv_leather_draenordungeon_c_01shoulder", - "Interface/ICONS/inv_mail_draenorquest90_b_01shoulder", - "Interface/ICONS/inv_leather_warfrontsalliance_c_01_shoulder" -}; -local weaponTextures = { - "Interface/ICONS/INV_Icon_HeirloomToken_Weapon01", - "Interface/ICONS/INV_Icon_HeirloomToken_Weapon02", - "Interface/ICONS/inv_weapon_shortblade_112", - "Interface/ICONS/inv_weapon_shortblade_111", - "Interface/ICONS/inv_weapon_shortblade_102", -}; -local isWeapon = { 20, 29, 28, 21, 22, 23, 24, 25, 26, 50, 57, 34, 35, 27, 33, 32, 31 }; +-- Instance Lib +(function() +local cache = app.CreateCache("instanceID"); +local function CacheInfo(t, field) + local _t, id = cache.GetCached(t); + local name, lore, _, _, _, icon, _, link = EJ_GetInstanceInfo(id); + _t.name = name; + _t.lore = lore; + _t.icon = icon; + _t.link = link; + if field then return _t[field]; end +end local fields = { ["key"] = function(t) - return "heirloomLevelID"; - end, - ["level"] = function(t) - return 1; - end, - ["text"] = function(t) - return "Upgrade Level " .. t.level; + return "instanceID"; end, ["icon"] = function(t) - return t.isWeapon and weaponTextures[t.level] or armorTextures[t.level]; + return cache.GetCachedField(t, "icon", CacheInfo); end, - ["description"] = function(t) - return L["HEIRLOOMS_UPGRADES_DESC"]; + ["name"] = function(t) + return cache.GetCachedField(t, "name", CacheInfo); end, - ["collectible"] = function(t) - return app.CollectibleHeirlooms and app.CollectibleHeirloomUpgrades; + ["lore"] = function(t) + return cache.GetCachedField(t, "lore", CacheInfo); end, - ["saved"] = function(t) - local itemID = t.heirloomLevelID; - if itemID then - if t.level <= (ATTAccountWideData.HeirloomRanks[itemID] or 0) then return true; end - local level = select(5, C_Heirloom_GetHeirloomInfo(itemID)); - if level then - ATTAccountWideData.HeirloomRanks[itemID] = level; - if t.level <= level then return true; end - end - end + ["link"] = function(t) + return cache.GetCachedField(t, "link", CacheInfo); end, - ["trackable"] = app.ReturnTrue, - ["isWeapon"] = function(t) - if t.f and contains(isWeapon, t.f) then - rawset(t, "isWeapon", true); - return true; + ["back"] = function(t) + if app.CurrentMapID == t.mapID or (t.maps and contains(t.maps, app.CurrentMapID)) then + return 1; end - rawset(t, "isWeapon", false); - return false; end, -}; -fields.collected = fields.saved; -app.BaseHeirloomLevel = app.BaseObjectFields(fields, "BaseHeirloomLevel"); - --- copy base Item fields --- TODO: heirlooms need to cache item information as well -local fields = RawCloneData(itemFields); -fields.b = function(t) return 2; end -fields.filterID = function(t) return 109; end -fields.icon = function(t) return select(4, C_Heirloom_GetHeirloomInfo(t.itemID)) or select(5, GetItemInfoInstant(t.itemID)); end -fields.link = function(t) return C_Heirloom_GetHeirloomLink(t.itemID) or select(2, GetItemInfo(t.itemID)); end -fields.collectibleAsCost = app.ReturnFalse; -fields.collectible = function(t) - if t.factionID then return app.CollectibleReputations; end - return t.s and app.CollectibleTransmog; - end -fields.collected = function(t) - if t.factionID then - if t.repeatable then - return (app.CurrentCharacter.Factions[t.factionID] and 1) - or (ATTAccountWideData.Factions[t.factionID] and 2); - else - -- This is used for the Grand Commendations unlocking Bonus Reputation - if ATTAccountWideData.FactionBonus[t.factionID] then return 1; end - if select(15, GetFactionInfoByID(t.factionID)) then - ATTAccountWideData.FactionBonus[t.factionID] = 1; - return 1; - end - end - end - if t.s and ATTAccountWideData.Sources[t.s] then return 1; end - if t.itemID and C_Heirloom_PlayerHasHeirloom(t.itemID) then return 1; end - end -fields.saved = function(t) - return t.collected == 1; - end -fields.isWeapon = function(t) - if t.f and contains(isWeapon, t.f) then - rawset(t, "isWeapon", true); - return true; - end - rawset(t, "isWeapon", false); - return false; - end -fields.g = function(t) - -- unlocking the heirloom is the only thing contained in the heirloom - if C_Heirloom_GetHeirloomMaxUpgradeLevel(t.itemID) then - rawset(t, "g", { setmetatable({ ["heirloomUnlockID"] = t.itemID, ["u"] = t.u, ["f"] = t.f }, app.BaseHeirloomUnlocked) }); - return rawget(t, "g"); - end - end - -app.BaseHeirloom = app.BaseObjectFields(fields, "BaseHeirloom"); -app.CreateHeirloom = function(id, t) - tinsert(heirloomIDs, id); - return setmetatable(constructor(id, t, "itemID"), app.BaseHeirloom); -end - --- Will retrieve all the cached entries by itemID for existing heirlooms and generate their --- upgrade levels into the respective upgrade tokens -app.CacheHeirlooms = function() - -- app.PrintDebug("CacheHeirlooms",#heirloomIDs) - if #heirloomIDs < 1 then return; end - - -- setup the armor tokens which will contain the upgrades for the heirlooms - local armorTokens = { - app.CreateItem(187997), -- Eternal Heirloom Armor Casing - app.CreateItem(167731), -- Battle-Hardened Heirloom Armor Casing - app.CreateItem(151614), -- Weathered Heirloom Armor Casing - app.CreateItem(122340), -- Timeworn Heirloom Armor Casing - app.CreateItem(122338), -- Ancient Heirloom Armor Casing - }; - local weaponTokens = { - app.CreateItem(187998), -- Eternal Heirloom Scabbard - app.CreateItem(167732), -- Battle-Hardened Heirloom Scabbard - app.CreateItem(151615), -- Weathered Heirloom Scabbard - app.CreateItem(122341), -- Timeworn Heirloom Scabbard - app.CreateItem(122339), -- Ancient Heirloom Scabbard - }; - - -- cache the heirloom upgrade tokens - for i,item in ipairs(armorTokens) do - item.g = {}; - end - for i,item in ipairs(weaponTokens) do - item.g = {}; - end - -- for each cached heirloom, push a copy of itself with respective upgrade level under the respective upgrade token - local heirloom, upgrades, isWeapon; - local uniques = {}; - for _,itemID in ipairs(heirloomIDs) do - if not uniques[itemID] then - uniques[itemID] = true; - - heirloom = app.SearchForObject("itemID", itemID); - if heirloom then - upgrades = C_Heirloom_GetHeirloomMaxUpgradeLevel(itemID); - if upgrades then - isWeapon = heirloom.isWeapon; - - local heirloomHeader; - for i=1,upgrades,1 do - -- Create a non-collectible version of the heirloom item itself to hold the upgrade within the token - heirloomHeader = CloneData(heirloom); - heirloomHeader.collectible = false; - -- put the upgrade object into the header heirloom object - heirloomHeader.g = { setmetatable({ ["level"] = i, ["heirloomLevelID"] = itemID, ["u"] = heirloom.u, ["f"] = heirloom.f }, app.BaseHeirloomLevel) }; - - -- add the header into the appropriate upgrade token - if isWeapon then - tinsert(weaponTokens[upgrades + 1 - i].g, heirloomHeader); - else - tinsert(armorTokens[upgrades + 1 - i].g, heirloomHeader); - end - end - end - end + ["saved"] = function(t) + return t.locks; + end, + ["locks"] = function(t) + local locks = app.CurrentCharacter.Lockouts[t.name]; + if locks then + rawset(t, "locks", locks); + return locks; end - end + end, + ["isLockoutShared"] = app.ReturnFalse, +}; +app.BaseInstance = app.BaseObjectFields(fields, "BaseInstance"); +app.CreateInstance = function(id, t) + return setmetatable(constructor(id, t, "instanceID"), app.BaseInstance); +end +end)(); - -- build groups for each upgrade token - -- and copy the set of upgrades into the cached versions of the upgrade tokens so they therefore exist in the main list - -- where the sources of the upgrade tokens exist - local cachedTokenGroups; - for i,item in ipairs(armorTokens) do - cachedTokenGroups = app.SearchForField("itemID", item.itemID); - for _,token in ipairs(cachedTokenGroups) do - -- ensure the tokens do not have a modID attached - token.modID = nil; - if not token.sym then - for _,heirloom in ipairs(item.g) do - NestObject(token, heirloom, true); - end - BuildGroups(token, token.g); - end +-- Item Lib +(function() +-- TODO: Once Item information is stored in a single source table, this mechanism can reference that instead of using a cache table here +local cache = app.CreateCache("modItemID"); +-- Consolidated function to handle how many retries for information an Item may have +local function HandleItemRetries(t) + local _t, id = cache.GetCached(t); + local retries = rawget(_t, "retries"); + if retries then + if retries > app.MaximumItemInfoRetries then + local itemName = "Item #" .. tostring(id) .. "*"; + rawset(_t, "title", L["FAILED_ITEM_INFO"]); + rawset(_t, "link", nil); + rawset(_t, "s", nil); + -- print("itemRetriesMax",itemName,rawget(t, "retries")) + -- save the "name" field in the source group to prevent further requests to the cache + rawset(t, "name", itemName); + return itemName; + else + rawset(_t, "retries", retries + 1); end + else + rawset(_t, "retries", 1); end - for i,item in ipairs(weaponTokens) do - cachedTokenGroups = app.SearchForField("itemID", item.itemID); - for _,token in ipairs(cachedTokenGroups) do - -- ensure the tokens do not have a modID attached - token.modID = nil; - if not token.sym then - for _,heirloom in ipairs(item.g) do - NestObject(token, heirloom, true); - end - BuildGroups(token, token.g); - end +end +-- Consolidated function to cache available Item information +local function RawSetItemInfoFromLink(t, link) + local name, link, quality, _, _, _, _, _, _, icon, _, _, _, b = GetItemInfo(link); + if link then + --[[ -- Debug Prints + local _t, id = cache.GetCached(t); + print("rawset item info",id,link,name,quality,b) + --]] + t = cache.GetCached(t); + rawset(t, "retries", nil); + rawset(t, "name", name); + rawset(t, "link", link); + rawset(t, "icon", icon); + rawset(t, "q", quality); + if quality > 6 then + -- heirlooms return as 1 but are technically BoE for our concern + rawset(t, "b", 2); + else + rawset(t, "b", b); end + return link; + else + HandleItemRetries(t); end - - wipe(heirloomIDs); end -end)(); - --- Toy Lib -(function() --- copy base Item fields -local fields = RawCloneData(itemFields); -fields.filterID = function(t) - return 102; +local function default_link(t) + -- item already has a pre-determined itemLink so use that + if t.rawlink then return RawSetItemInfoFromLink(t, t.rawlink); end + -- need to 'create' a valid accurate link for this item + local itemLink = t.itemID; + if itemLink then + local bonusID = t.bonusID; + local modID = t.modID; + if not bonusID or bonusID < 1 then + bonusID = nil; + end + if not modID or modID < 1 then + modID = nil; + end + if bonusID and modID then + itemLink = sformat("item:%d:::::::::::%d:1:%d:", itemLink, modID, bonusID); + elseif bonusID then + itemLink = sformat("item:%d::::::::::::1:%d:", itemLink, bonusID); + elseif modID then + -- bonusID 3524 seems to imply "use ModID to determine SourceID" since without it, everything with ModID resolves as the base SourceID from links + itemLink = sformat("item:%d:::::::::::%d:1:3524:", itemLink, modID); + else + itemLink = sformat("item:%d:::::::::::::", itemLink); + end + -- save this link so it doesn't need to be built again + rawset(t, "rawlink", itemLink); + return RawSetItemInfoFromLink(t, itemLink); end -fields.collectible = function(t) - return app.CollectibleToys; +end +local function default_icon(t) + return t.itemID and select(5, GetItemInfoInstant(t.itemID)) or "Interface\\Icons\\INV_Misc_QuestionMark"; +end +local function default_specs(t) + return GetFixedItemSpecInfo(t.itemID); +end +local function default_costCollectibles(t) + local results, id; + local modItemID = t.modItemID; + -- Search by modItemID if possible for accuracy + if modItemID and modItemID ~= t.itemID then + id = modItemID; + results = app.SearchForField("itemIDAsCost", id); + -- if app.DEBUG_PRINT then print("itemIDAsCost.modItemID",id,results and #results) end end -fields.collected = function(t) - return ATTAccountWideData.Toys[t.itemID]; + -- If no results, search by itemID + modID only if different + if not results then + id = GetGroupItemIDWithModID(nil, t.itemID, t.modID); + if id ~= modItemID then + results = app.SearchForField("itemIDAsCost", id); + -- if app.DEBUG_PRINT then print("itemIDAsCost.modID",id,results and #results) end + end end -fields.tsm = function(t) - return sformat("i:%d", t.itemID); + -- If no results, search by plain itemID only + if not results and t.itemID then + id = t.itemID; + results = app.SearchForField("itemIDAsCost", id); end -fields.isToy = app.ReturnTrue; -fields.toyID = function(t) - return t.itemID; + if results and #results > 0 then + -- not sure we need to copy these into another table + -- app.PrintDebug("default_costCollectibles",t.hash,id,#results) + return results; end - -app.BaseToy = app.BaseObjectFields(fields, "BaseToy"); -app.CreateToy = function(id, t) - return setmetatable(constructor(id, t, "itemID"), app.BaseToy); + return app.EmptyTable; end -end)(); - -local HarvestedItemDatabase; -local C_Item_GetItemInventoryTypeByID = C_Item.GetItemInventoryTypeByID; -local itemHarvesterFields = RawCloneData(itemFields); -itemHarvesterFields.visible = app.ReturnTrue; -itemHarvesterFields.collectible = app.ReturnTrue; -itemHarvesterFields.collected = app.ReturnFalse; -itemHarvesterFields.text = function(t) - -- delayed localization since ATT's globals don't exist when this logic is processed on load - if not HarvestedItemDatabase then - HarvestedItemDatabase = LocalizeGlobal("AllTheThingsHarvestItems", true); - end - local link = t.link; - if link then - local itemName, itemLink, itemQuality, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, - itemEquipLoc, itemTexture, sellPrice, classID, subclassID, bindType, expacID, setID, isCraftingReagent - = GetItemInfo(link); - if itemName then - local spellName, spellID; - -- Recipe or Mount, grab the spellID if possible - if classID == LE_ITEM_CLASS_RECIPE or (classID == LE_ITEM_CLASS_MISCELLANEOUS and subclassID == LE_ITEM_MISCELLANEOUS_MOUNT) then - spellName, spellID = GetItemSpell(t.itemID); - -- print("Recipe/Mount",classID,subclassID,spellName,spellID); - if spellName == "Learning" then spellID = nil; end -- RIP. - end - setmetatable(t, app.BaseItemTooltipHarvester); - local info = { - ["name"] = itemName, - ["itemID"] = t.itemID, - ["equippable"] = itemEquipLoc and itemEquipLoc ~= "" and true or false, - ["class"] = classID, - ["subclass"] = subclassID, - ["inventoryType"] = C_Item_GetItemInventoryTypeByID(t.itemID), - ["b"] = bindType, - ["q"] = itemQuality, - ["iLvl"] = itemLevel, - ["spellID"] = spellID, - }; - if itemMinLevel and itemMinLevel > 0 then - info.lvl = itemMinLevel; - end - if info.inventoryType == 0 then - info.inventoryType = nil; - end - if not app.IsBoP(info) then - info.b = nil; - end - if info.q and info.q < 1 then - info.q = nil; - end - if info.iLvl and info.iLvl < 2 then - info.iLvl = nil; +local itemFields = { + ["key"] = function(t) + return "itemID"; + end, + ["_cache"] = function(t) + return cache; + end, + ["text"] = function(t) + return t.link or t.name; + end, + ["icon"] = function(t) + return cache.GetCachedField(t, "icon", default_icon); + end, + ["link"] = function(t) + return cache.GetCachedField(t, "link", default_link); + end, + ["name"] = function(t) + return cache.GetCachedField(t, "name"); + end, + ["specs"] = function(t) + return cache.GetCachedField(t, "specs", default_specs); + end, + ["retries"] = function(t) + return cache.GetCachedField(t, "retries"); + end, + ["q"] = function(t) + return cache.GetCachedField(t, "q"); + end, + ["b"] = function(t) + return cache.GetCachedField(t, "b") or 2; + end, + ["title"] = function(t) + return cache.GetCachedField(t, "title"); + end, + ["f"] = function(t) + -- Unknown item type after Parser, so make sure we save the filter for later references + rawset(t, "f", -1); + return rawget(t, "f"); + end, + ["tsm"] = function(t) + local itemLink = t.itemID; + if itemLink then + local bonusID = t.bonusID; + if bonusID and bonusID > 0 then + return sformat("i:%d:0:1:%d", itemLink, bonusID); + --elseif t.modID then + -- NOTE: At this time, TSM3 does not support modID. (RIP) + --return sformat("i:%d:%d:1:3524", itemLink, t.modID); end - -- can debug output for tooltip harvesting - -- if t.itemID == 141038 then - -- info._debug = true; - -- end - t.itemType = itemType; - t.itemSubType = itemSubType; - t.info = info; - t.retries = nil; - HarvestedItemDatabase[t.itemID] = info; - return link; + return sformat("i:%d", itemLink); end - end - - local name = t.name; - -- retries exceeded, so check the raw .name on the group (gets assigned when retries exceeded during cache attempt) - if name then rawset(t, "collected", true); end - return name; -end -app.BaseItemHarvester = app.BaseObjectFields(itemHarvesterFields, "BaseItemHarvester"); - -local ItemHarvester = CreateFrame("GameTooltip", "ATTItemHarvester", UIParent, "GameTooltipTemplate"); -local itemTooltipHarvesterFields = RawCloneData(itemHarvesterFields); -itemTooltipHarvesterFields.text = function(t) - local link = t.link; - if link then - ItemHarvester:SetOwner(UIParent,"ANCHOR_NONE") - ItemHarvester:SetHyperlink(link); - -- a way to capture when the tooltip is giving information about something that is NOT the current ItemID - local isSubItem, craftName; - local lineCount = ItemHarvester:NumLines(); - if ATTItemHarvesterTextLeft1:GetText() and ATTItemHarvesterTextLeft1:GetText() ~= RETRIEVING_DATA and lineCount > 0 then - -- local debugPrint = t.info._debug; - -- if debugPrint then print("Item Info:",t.info.itemID) end - for index=1,lineCount,1 do - local line = _G["ATTItemHarvesterTextLeft" .. index] or _G["ATTItemHarvesterText" .. index]; - if line then - local text = line:GetText(); - if text then - -- sub items within recipe tooltips show this text, need to wait until it loads - if text == RETRIEVING_ITEM_INFO then - t.info.retries = (t.info.retries or 0) + 1; - -- 30 attempts to load the sub-item, otherwise just continue parsing tooltip without it - if t.info.retries < 30 then - return RETRIEVING_DATA; - end - app.PrintDebug("Failed loading sub-item for",t.info.itemID) - end - -- pull the "Recipe Type: Recipe Name" out if it matches - if index == 1 then - -- if debugPrint then - -- print("line match",text:match("^[^:]+:%s*([^:]+)$")) - -- end - craftName = text:match("^[^:]+:%s*([^:]+)$"); - if craftName then - -- whitespace search... recipes have whitespace and then a sub-item - craftName = "^%s+"; - end - -- use this name to check that the Item it creates may be listed underneath, by finding whitespace after a matching recipe name - elseif craftName and text:match(craftName) then - -- if debugPrint then - -- print("subitem",t.info.itemID,craftName) - -- end - isSubItem = true; - -- leave the sub-item info when reaching the 'Requires' portion of the parent item tooltip - elseif isSubItem and text:match("^Requires") then - -- if debugPrint then - -- print("leaving subitem",t.info.itemID,craftName) - -- end - -- leaving the sub-item tooltip when encountering 'Requires ' - isSubItem = nil; - end - - if not isSubItem then - -- if debugPrint then print(text) end - if string.find(text, "Classes: ") then - local classes = {}; - local _,list = strsplit(":", text); - for i,s in ipairs({strsplit(",", list)}) do - tinsert(classes, app.ClassDB[strtrim(s)]); - end - if #classes > 0 then - t.info.classes = classes; - end - elseif string.find(text, "Races: ") then - local _,list = strsplit(":", text); - local raceNames = {strsplit(",", list)}; - if raceNames then - local races = {}; - for _,s in ipairs(raceNames) do - local race = app.RaceDB[strtrim(s)]; - if not race then - print("Unknown Race",t.info.itemID,strtrim(s)) - elseif type(race) == "number" then - tinsert(races, race); - else -- Pandaren - for _,panda in pairs(race) do - tinsert(races, panda); - end - end - end - if #races > 0 then - t.info.races = races; - end - else - print("Empty Races",t.info.itemID) - end - elseif string.find(text, " Only") then - local faction,list,c = strsplit(" ", text); - if not c then - faction = strtrim(faction); - if faction == "Alliance" then - t.info.races = app.FACTION_RACES[1]; - elseif faction == "Horde" then - t.info.races = app.FACTION_RACES[2]; - else - print("Unknown Faction",t.info.itemID,faction); - end - end - elseif string.find(text, "Requires") and not string.find(text, "Level") and not string.find(text, "Riding") then - local c = strsub(text, 1, 1); - if c ~= " " and c ~= "\t" and c ~= "\n" and c ~= "\r" then - text = strsub(strtrim(text), 9); - if string.find(text, "-") then - local faction,replevel = strsplit("-", text); - t.info.minReputation = { app.GetFactionIDByName(faction), app.GetFactionStandingThresholdFromString(replevel) }; - else - if string.find(text, "%(") then - if t.info.requireSkill then - -- If non-specialization skill is already assigned, skip this part. - text = nil; - else - text = strsplit("(", text); - end - end - if text then - local spellName = strtrim(text); - if string.find(spellName, "Outland ") then spellName = strsub(spellName, 9); - elseif string.find(spellName, "Northrend ") then spellName = strsub(spellName, 11); - elseif string.find(spellName, "Cataclysm ") then spellName = strsub(spellName, 11); - elseif string.find(spellName, "Pandaria ") then spellName = strsub(spellName, 10); - elseif string.find(spellName, "Draenor ") then spellName = strsub(spellName, 9); - elseif string.find(spellName, "Legion ") then spellName = strsub(spellName, 8); - elseif string.find(spellName, "Kul Tiran ") then spellName = strsub(spellName, 11); - elseif string.find(spellName, "Zandalari ") then spellName = strsub(spellName, 11); - elseif string.find(spellName, "Shadowlands ") then spellName = strsub(spellName, 13); - elseif string.find(spellName, "Classic ") then spellName = strsub(spellName, 9); end - if spellName == "Herbalism" then spellName = "Herb Gathering"; end - spellName = strtrim(spellName); - local spellID = app.SpellNameToSpellID[spellName]; - if spellID then - local skillID = app.SpellIDToSkillID[spellID]; - if skillID then - t.info.requireSkill = skillID; - elseif spellName == "Pick Pocket" then - -- Do nothing, for now. - elseif spellName == "Warforged Nightmare" then - -- Do nothing, for now. - else - print("Unknown Skill",t.info.itemID, text, "'" .. spellName .. "'"); - end - elseif spellName == "Previous Rank" then - -- Do nothing - elseif spellName == "" then - -- Do nothing - elseif spellName == "Brewfest" then - -- Do nothing, yet. - elseif spellName == "Call of the Scarab" then - -- Do nothing, yet. - elseif spellName == "Children's Week" then - -- Do nothing, yet. - elseif spellName == "Darkmoon Faire" then - -- Do nothing, yet. - elseif spellName == "Day of the Dead" then - -- Do nothing, yet. - elseif spellName == "Feast of Winter Veil" then - -- Do nothing, yet. - elseif spellName == "Hallow's End" then - -- Do nothing, yet. - elseif spellName == "Love is in the Air" then - -- Do nothing, yet. - elseif spellName == "Lunar Festival" then - -- Do nothing, yet. - elseif spellName == "Midsummer Fire Festival" then - -- Do nothing, yet. - elseif spellName == "Moonkin Festival" then - -- Do nothing, yet. - elseif spellName == "Noblegarden" then - -- Do nothing, yet. - elseif spellName == "Pilgrim's Bounty" then - -- Do nothing, yet. - elseif spellName == "Un'Goro Madness" then - -- Do nothing, yet. - elseif spellName == "Thousand Boat Bash" then - -- Do nothing, yet. - elseif spellName == "Glowcap Festival" then - -- Do nothing, yet. - elseif spellName == "Battle Pet Training" then - -- Do nothing. - elseif spellName == "Lockpicking" then - -- Do nothing. - elseif spellName == "Luminous Luminaries" then - -- Do nothing. - elseif spellName == "Pick Pocket" then - -- Do nothing. - elseif spellName == "WoW's 14th Anniversary" then - -- Do nothing. - elseif spellName == "WoW's 13th Anniversary" then - -- Do nothing. - elseif spellName == "WoW's 12th Anniversary" then - -- Do nothing. - elseif spellName == "WoW's 11th Anniversary" then - -- Do nothing. - elseif spellName == "WoW's 10th Anniversary" then - -- Do nothing. - elseif spellName == "WoW's Anniversary" then - -- Do nothing. - elseif spellName == "level 1 to 29" then - -- Do nothing. - elseif spellName == "level 1 to 39" then - -- Do nothing. - elseif spellName == "level 1 to 44" then - -- Do nothing. - elseif spellName == "level 1 to 49" then - -- Do nothing. - elseif spellName == "Unknown" then - -- Do nothing. - elseif spellName == "Open" then - -- Do nothing. - elseif string.find(spellName, " specialization") then - -- Do nothing. - elseif string.find(spellName, ": ") then - -- Do nothing. - else - print("Unknown Spell",t.info.itemID, text, "'" .. spellName .. "'"); - end - end - end - end - end - end - end + end, + ["repeatable"] = function(t) + return rawget(t, "isDaily") or rawget(t, "isWeekly") or rawget(t, "isMonthly") or rawget(t, "isYearly") or rawget(t, "isWorldQuest"); + end, + ["modItemID"] = function(t) + rawset(t, "modItemID", GetGroupItemIDWithModID(t) or t.itemID); + return rawget(t, "modItemID"); + end, + ["indicatorIcon"] = app.GetQuestIndicator, + ["trackableAsQuest"] = app.ReturnTrue, + ["collectibleAsAchievement"] = function(t) + return app.CollectibleAchievements; + end, + ["costCollectibles"] = function(t) + return cache.GetCachedField(t, "costCollectibles", default_costCollectibles); + end, + ["collectibleAsCost"] = app.CollectibleAsCost, + -- ["collectedAsCost"] = app.CollectedAsCost, + ["costsCount"] = function(t) + if t.costCollectibles then return #t.costCollectibles; end + end, + ["collectibleAsFaction"] = function(t) + return app.CollectibleReputations; + end, + ["collectibleAsFactionOrQuest"] = function(t) + return app.CollectibleReputations or t.collectibleAsQuest; + end, + ["collectibleAsTransmog"] = function(t) + return app.CollectibleTransmog; + end, + ["collectibleAsQuest"] = app.CollectibleAsQuest, + ["collectedAsQuest"] = IsQuestFlaggedCompletedForObject, + ["lockedAsQuest"] = app.LockedAsQuest, + ["collectedAsFaction"] = function(t) + if t.factionID then + if t.repeatable then + -- This is used by reputation tokens. (turn in items) + -- quick cache checks + if app.CurrentCharacter.Factions[t.factionID] then return 1; end + if app.AccountWideReputations and ATTAccountWideData.Factions[t.factionID] then return 2; end + + -- use the extended faction logic from the associated Faction for consistency + local cachedFaction = app.SearchForObject("factionID", t.factionID); + if cachedFaction then return cachedFaction.collected; end + + -- otherwise move on to the basic logic + if select(3, GetFactionInfoByID(t.factionID)) == 8 then + app.CurrentCharacter.Factions[t.factionID] = 1; + ATTAccountWideData.Factions[t.factionID] = 1; + return 1; + end + else + -- This is used for the Grand Commendations unlocking Bonus Reputation + if ATTAccountWideData.FactionBonus[t.factionID] then return 1; end + if select(15, GetFactionInfoByID(t.factionID)) then + ATTAccountWideData.FactionBonus[t.factionID] = 1; + return 1; end end - -- if debugPrint then print("---") end - t.info.retries = nil; - rawset(t, "text", link); - rawset(t, "collected", true); end - ItemHarvester:Hide(); - return link; - end -end -app.BaseItemTooltipHarvester = app.BaseObjectFields(itemTooltipHarvesterFields, "BaseItemTooltipHarvester"); -app.CreateItemHarvester = function(id, t) - return setmetatable(constructor(id, t, "itemID"), app.BaseItemHarvester); -end + end, + ["collectedAsFactionOrQuest"] = function(t) + return t.collectedAsFaction or t.collectedAsQuest; + end, + ["collectedAsTransmog"] = function(t) + return ATTAccountWideData.Sources[rawget(t, "s")]; + end, + ["savedAsQuest"] = function(t) + return IsQuestFlaggedCompleted(t.questID); + end, + ["costTotal"] = function(t) + return t.collectibleAsCost and 1 or 0; + end, + ["costProgress"] = function(t) + return t.collectedAsCost and 1 or 0; + end, +}; +app.BaseItem = app.BaseObjectFields(itemFields, "BaseItem"); --- Imports the raw information from the rawlink into the specified group -app.ImportRawLink = function(group, rawlink) - rawlink = string.match(rawlink, "item[%-?%d:]+"); - if rawlink and group then - group.rawlink = rawlink; - local _, linkItemID, enchantId, gemId1, gemId2, gemId3, gemId4, suffixId, uniqueId, linkLevel, specializationID, upgradeId, modID, bonusCount, bonusID1 = strsplit(":", rawlink); - if linkItemID then - -- app.PrintDebug("ImportRawLink",rawlink,linkItemID,modID,bonusCount,bonusID1); - -- set raw fields in the group based on the link - group.itemID = tonumber(linkItemID); - group.modID = modID and tonumber(modID) or nil; - -- only set the bonusID if there is actually bonusIDs indicated - if (tonumber(bonusCount) or 0) > 0 then - -- Don't use bonusID 3524 as an actual bonusID - local b = bonusID1 and tonumber(bonusID1) or nil; - if b ~= 3524 then - group.bonusID = b; +local fields = RawCloneData(itemFields); +fields.collectible = itemFields.collectibleAsAchievement; +fields.collected = itemFields.collectedAsAchievement; +app.BaseItemWithAchievementID = app.BaseObjectFields(fields, "BaseItemWithAchievementID"); + +local fields = RawCloneData(itemFields); +fields.collectible = itemFields.collectibleAsFaction; +fields.collected = itemFields.collectedAsFaction; +app.BaseItemWithFactionID = app.BaseObjectFields(fields, "BaseItemWithFactionID"); + +local fields = RawCloneData(itemFields); +fields.collectible = itemFields.collectibleAsQuest; +fields.collected = itemFields.collectedAsQuest; +fields.trackable = itemFields.trackableAsQuest; +fields.saved = itemFields.savedAsQuest; +fields.locked = itemFields.lockedAsQuest; +app.BaseItemWithQuestID = app.BaseObjectFields(fields, "BaseItemWithQuestID"); + +local fields = RawCloneData(itemFields); +fields.collectible = itemFields.collectibleAsFactionOrQuest; +fields.collected = itemFields.collectedAsFactionOrQuest; +fields.trackable = itemFields.trackableAsQuest; +fields.saved = itemFields.savedAsQuest; +fields.locked = itemFields.lockedAsQuest; +app.BaseItemWithQuestIDAndFactionID = app.BaseObjectFields(fields, "BaseItemWithQuestIDAndFactionID"); + +local fields = RawCloneData(itemFields); +fields.collectible = function(t) + return app.CollectibleTransmog; +end +fields.collected = function(t) + if t.itemID then + if GetItemCount(t.itemID, true) > 0 then + app.CurrentCharacter.CommonItems[t.itemID] = 1; + ATTAccountWideData.CommonItems[t.itemID] = 1; + return 1; + elseif app.CurrentCharacter.CommonItems[t.itemID] == 1 then + app.CurrentCharacter.CommonItems[t.itemID] = nil; + ATTAccountWideData.CommonItems[t.itemID] = nil; + for guid,characterData in pairs(ATTCharacterData) do + if characterData.CommonItems and characterData.CommonItems[t.itemID] then + ATTAccountWideData.CommonItems[t.itemID] = 1; end end - group.modItemID = nil; - -- does this link also have a sourceID? - local s = GetSourceID(rawlink); - -- print("s",s) - if s then group.s = s; end - -- if app.DEBUG_PRINT then app.PrintTable(group) end end - end -end --- Refines a set of items down to the most-accurate matches to the provided modItemID --- The sets of items will be returned based on their respective match depth to the given modItemID --- Ex: { [1] = { { ItemID }, { ItemID2 } }, [2] = { { ModID } }, [3] = { { BonusID } } } -app.GroupBestMatchingItems = function(items, modItemID) - if not items or #items == 0 then return; end - -- print("refining",#items,"by depth to",modItemID) - -- local i, m, b = GetItemIDAndModID(modItemID); - local refinedBuckets, GetDepth, depth = {}, app.ItemMatchDepth; - for _,item in ipairs(items) do - depth = GetDepth(item, modItemID); - if depth then - -- print("added refined item",depth,item.modItemID,item.key,item.key and item[item.key]) - if refinedBuckets[depth] then tinsert(refinedBuckets[depth], item) - else refinedBuckets[depth] = { item }; end + if ATTAccountWideData.CommonItems[t.itemID] then + return 2; end end - return refinedBuckets; end --- Returns the depth at which a given Item matches the provided modItemID --- 1 = ItemID, 2 = ModID, 3 = BonusID -app.ItemMatchDepth = function(item, modItemID) - if not item or not item.itemID then return; end - local i, m, b = GetItemIDAndModID(modItemID); - local depth = 0; - if item.itemID == i then - depth = depth + 1; - if item.modID == m then - depth = depth + 1; - if item.bonusID == b then - depth = depth + 1; +app.BaseCommonItem = app.BaseObjectFields(fields, "BaseCommonItem"); + +-- Appearance Lib (Item Source) +local fields = RawCloneData(itemFields); +fields.key = function(t) return "s"; end; +-- TODO: if PL filter is ever a thing investigate https://wowpedia.fandom.com/wiki/API_C_TransmogCollection.PlayerCanCollectSource +fields.collectible = itemFields.collectibleAsTransmog; +fields.collected = itemFields.collectedAsTransmog; +app.BaseItemSource = app.BaseObjectFields(fields, "BaseItemSource"); + +app.CreateItemSource = function(sourceID, itemID, t) + t = setmetatable(constructor(sourceID, t, "s"), app.BaseItemSource); + t.itemID = itemID; + return t; +end +app.CreateItem = function(id, t) + if t then + if rawget(t, "s") then + return setmetatable(constructor(id, t, "itemID"), app.BaseItemSource); + elseif rawget(t, "factionID") then + if rawget(t, "questID") then + return setmetatable(constructor(id, t, "itemID"), app.BaseItemWithQuestIDAndFactionID); + else + return setmetatable(constructor(id, t, "itemID"), app.BaseItemWithFactionID); end + elseif rawget(t, "questID") then + return setmetatable(constructor(id, t, "itemID"), app.BaseItemWithQuestID); + elseif rawget(t, "achID") then + rawset(t, "achievementID", app.FactionID == Enum.FlightPathFaction.Horde and rawget(t, "altAchID") or rawget(t, "achID")); + return setmetatable(constructor(id, t, "itemID"), app.BaseItemWithAchievementID); end end - return depth; + return setmetatable(constructor(id, t, "itemID"), app.BaseItem); +end + +-- Runeforge Legendary Lib +(function() +-- copy base Item fields +local fields = RawCloneData(itemFields); +-- Runeforge Legendary differences +local C_LegendaryCrafting_GetRuneforgePowerInfo = C_LegendaryCrafting.GetRuneforgePowerInfo; +fields.key = function(t) return "runeforgePowerID"; end; +fields.collectible = function(t) return app.CollectibleRuneforgeLegendaries; end; +fields.collectibleAsCost = app.ReturnFalse; +fields.collected = function(t) + local rfID = t.runeforgePowerID; + -- account-wide collected + if ATTAccountWideData.RuneforgeLegendaries[rfID] then return 1; end + -- fresh collected + local state = (C_LegendaryCrafting_GetRuneforgePowerInfo(rfID) or app.EmptyTable).state; + if state == 0 then + ATTAccountWideData.RuneforgeLegendaries[rfID] = 1; + return 1; + end +end; +fields.lvl = function(t) return 60; end; +app.BaseRuneforgeLegendary = app.BaseObjectFields(fields, "BaseRuneforgeLegendary"); +app.CreateRuneforgeLegendary = function(id, t) + return setmetatable(constructor(id, t, "runeforgePowerID"), app.BaseRuneforgeLegendary); +end +end)(); + +-- Conduit Lib +(function() +local C_Soulbinds_GetConduitCollectionData = C_Soulbinds.GetConduitCollectionData; +-- copy base Item fields +local fields = RawCloneData(itemFields); +-- Conduit differences +fields.key = function(t) return "conduitID"; end; +fields.collectible = function(t) return app.CollectibleConduits; end; +fields.collectibleAsCost = app.ReturnFalse; +fields.collected = function(t) + local cID = t.conduitID; + -- character collected + if app.CurrentCharacter.Conduits[cID] then return 1; end + -- account-wide collected + if app.AccountWideConduits and ATTAccountWideData.Conduits[cID] then return 2; end + -- fresh collected + local state = C_Soulbinds_GetConduitCollectionData(cID); + if state ~= nil then + app.CurrentCharacter.Conduits[cID] = 1; + ATTAccountWideData.Conduits[cID] = 1; + return 1; + end +end; +fields.lvl = function(t) return 60; end; +app.BaseConduit = app.BaseObjectFields(fields, "BaseConduit"); +app.CreateConduit = function(id, t) + return setmetatable(constructor(id, t, "conduitID"), app.BaseConduit); end end)(); --- Map Lib +-- Heirloom Lib (function() -local C_Map_GetMapLevels = C_Map.GetMapLevels; -local C_Map_GetBestMapForUnit = C_Map.GetBestMapForUnit; -app.GetCurrentMapID = function() - local uiMapID = C_Map_GetBestMapForUnit("player"); - if uiMapID then - local map = C_Map_GetMapInfo(uiMapID); - if map then - local ZONE_TEXT_TO_MAP_ID = app.L["ZONE_TEXT_TO_MAP_ID"]; - local real = GetRealZoneText(); - local otherMapID = real and ZONE_TEXT_TO_MAP_ID[real]; - if otherMapID then - uiMapID = otherMapID; - else - local zone = GetSubZoneText(); - if zone then - otherMapID = ZONE_TEXT_TO_MAP_ID[zone]; - if otherMapID then uiMapID = otherMapID; end - end - end - end - -- print("Current UI Map ID: ", uiMapID); - -- if entering an instance, clear the search Cache so that proper difficulty tooltips are re-generated - if IsInInstance() then wipe(searchCache); end - app.CurrentMapID = uiMapID; - end - return uiMapID; -end -app.GetMapName = function(mapID) - if mapID and mapID > 0 then - local info = C_Map_GetMapInfo(mapID); - return (info and info.name) or ("Map ID #" .. mapID); - else - return "Map ID #???"; - end -end -local mapFields = { +local C_Heirloom_GetHeirloomInfo = C_Heirloom.GetHeirloomInfo; +local C_Heirloom_GetHeirloomLink = C_Heirloom.GetHeirloomLink; +local C_Heirloom_PlayerHasHeirloom = C_Heirloom.PlayerHasHeirloom; +local C_Heirloom_GetHeirloomMaxUpgradeLevel = C_Heirloom.GetHeirloomMaxUpgradeLevel; +local heirloomIDs = {}; +local fields = { ["key"] = function(t) - return "mapID"; + return "heirloomUnlockID"; end, - ["name"] = function(t) - return t.creatureID and app.NPCNameFromID[t.creatureID] or app.GetMapName(t.mapID); + ["text"] = function(t) + return L["HEIRLOOM_TEXT"]; end, ["icon"] = function(t) - return t.creatureID and L["HEADER_ICONS"][t.creatureID] or app.asset("Category_Zones"); + return "Interface/ICONS/Achievement_GuildPerk_WorkingOvertime_Rank2"; end, - ["back"] = function(t) - if app.CurrentMapID == t.mapID or (t.maps and contains(t.maps, app.CurrentMapID)) then - return 1; - end + ["description"] = function(t) + return L["HEIRLOOM_TEXT_DESC"]; end, - ["lvl"] = function(t) - return select(1, C_Map_GetMapLevels(t.mapID)); + ["collectible"] = function(t) + return app.CollectibleHeirlooms; end, - ["iconForAchievement"] = function(t) - return t.achievementID and select(10, GetAchievementInfo(t.achievementID)) or app.asset("Category_Zones"); + ["saved"] = function(t) + return C_Heirloom_PlayerHasHeirloom(t.heirloomUnlockID); end, - ["linkForAchievement"] = function(t) - return GetAchievementLink(t.achievementID); + ["trackable"] = app.ReturnTrue, +}; +fields.collected = fields.saved; +app.BaseHeirloomUnlocked = app.BaseObjectFields(fields, "BaseHeirloomUnlocked"); + +local armorTextures = { + "Interface/ICONS/INV_Icon_HeirloomToken_Armor01", + "Interface/ICONS/INV_Icon_HeirloomToken_Armor02", + "Interface/ICONS/Inv_leather_draenordungeon_c_01shoulder", + "Interface/ICONS/inv_mail_draenorquest90_b_01shoulder", + "Interface/ICONS/inv_leather_warfrontsalliance_c_01_shoulder" +}; +local weaponTextures = { + "Interface/ICONS/INV_Icon_HeirloomToken_Weapon01", + "Interface/ICONS/INV_Icon_HeirloomToken_Weapon02", + "Interface/ICONS/inv_weapon_shortblade_112", + "Interface/ICONS/inv_weapon_shortblade_111", + "Interface/ICONS/inv_weapon_shortblade_102", +}; +local isWeapon = { 20, 29, 28, 21, 22, 23, 24, 25, 26, 50, 57, 34, 35, 27, 33, 32, 31 }; +local fields = { + ["key"] = function(t) + return "heirloomLevelID"; + end, + ["level"] = function(t) + return 1; + end, + ["text"] = function(t) + return "Upgrade Level " .. t.level; + end, + ["icon"] = function(t) + return t.isWeapon and weaponTextures[t.level] or armorTextures[t.level]; + end, + ["description"] = function(t) + return L["HEIRLOOMS_UPGRADES_DESC"]; + end, + ["collectible"] = function(t) + return app.CollectibleHeirlooms and app.CollectibleHeirloomUpgrades; + end, + ["saved"] = function(t) + local itemID = t.heirloomLevelID; + if itemID then + if t.level <= (ATTAccountWideData.HeirloomRanks[itemID] or 0) then return true; end + local level = select(5, C_Heirloom_GetHeirloomInfo(itemID)); + if level then + ATTAccountWideData.HeirloomRanks[itemID] = level; + if t.level <= level then return true; end + end + end + end, + ["trackable"] = app.ReturnTrue, + ["isWeapon"] = function(t) + if t.f and contains(isWeapon, t.f) then + rawset(t, "isWeapon", true); + return true; + end + rawset(t, "isWeapon", false); + return false; end, }; -app.BaseMap = app.BaseObjectFields(mapFields, "BaseMap"); +fields.collected = fields.saved; +app.BaseHeirloomLevel = app.BaseObjectFields(fields, "BaseHeirloomLevel"); -local fields = RawCloneData(mapFields); -fields.icon = mapFields.iconForAchievement; -fields.link = mapFields.linkForAchievement; -app.BaseMapWithAchievementID = app.BaseObjectFields(fields, "BaseMapWithAchievementID"); -app.CreateMap = function(id, t) - t = constructor(id, t, "mapID"); - if rawget(t, "achID") then - rawset(t, "achievementID", app.FactionID == Enum.FlightPathFaction.Horde and rawget(t, "altAchID") or rawget(t, "achID")); - t = setmetatable(t, app.BaseMapWithAchievementID); - else - t = setmetatable(t, app.BaseMap); +-- copy base Item fields +-- TODO: heirlooms need to cache item information as well +local fields = RawCloneData(itemFields); +fields.b = function(t) return 2; end +fields.filterID = function(t) return 109; end +fields.icon = function(t) return select(4, C_Heirloom_GetHeirloomInfo(t.itemID)) or select(5, GetItemInfoInstant(t.itemID)); end +fields.link = function(t) return C_Heirloom_GetHeirloomLink(t.itemID) or select(2, GetItemInfo(t.itemID)); end +fields.collectibleAsCost = app.ReturnFalse; +fields.collectible = function(t) + if t.factionID then return app.CollectibleReputations; end + return t.s and app.CollectibleTransmog; end - return t; +fields.collected = function(t) + if t.factionID then + if t.repeatable then + return (app.CurrentCharacter.Factions[t.factionID] and 1) + or (ATTAccountWideData.Factions[t.factionID] and 2); + else + -- This is used for the Grand Commendations unlocking Bonus Reputation + if ATTAccountWideData.FactionBonus[t.factionID] then return 1; end + if select(15, GetFactionInfoByID(t.factionID)) then + ATTAccountWideData.FactionBonus[t.factionID] = 1; + return 1; + end + end + end + if t.s and ATTAccountWideData.Sources[t.s] then return 1; end + if t.itemID and C_Heirloom_PlayerHasHeirloom(t.itemID) then return 1; end + end +fields.saved = function(t) + return t.collected == 1; + end +fields.isWeapon = function(t) + if t.f and contains(isWeapon, t.f) then + rawset(t, "isWeapon", true); + return true; + end + rawset(t, "isWeapon", false); + return false; + end +fields.g = function(t) + -- unlocking the heirloom is the only thing contained in the heirloom + if C_Heirloom_GetHeirloomMaxUpgradeLevel(t.itemID) then + rawset(t, "g", { setmetatable({ ["heirloomUnlockID"] = t.itemID, ["u"] = t.u, ["f"] = t.f }, app.BaseHeirloomUnlocked) }); + return rawget(t, "g"); + end + end + +app.BaseHeirloom = app.BaseObjectFields(fields, "BaseHeirloom"); +app.CreateHeirloom = function(id, t) + tinsert(heirloomIDs, id); + return setmetatable(constructor(id, t, "itemID"), app.BaseHeirloom); end -app.CreateMapWithStyle = function(id) - local mapObject = app.CreateMap(id, { progress = 0, total = 0 }); - for _,data in ipairs(fieldCache["mapID"][id] or {}) do - if data.mapID and data.icon then - mapObject.text = data.text; - mapObject.icon = data.icon; - mapObject.lvl = data.lvl; - mapObject.lore = data.lore; - mapObject.description = data.description; - break; + +-- Will retrieve all the cached entries by itemID for existing heirlooms and generate their +-- upgrade levels into the respective upgrade tokens +app.CacheHeirlooms = function() + -- app.PrintDebug("CacheHeirlooms",#heirloomIDs) + if #heirloomIDs < 1 then return; end + + -- setup the armor tokens which will contain the upgrades for the heirlooms + local armorTokens = { + app.CreateItem(187997), -- Eternal Heirloom Armor Casing + app.CreateItem(167731), -- Battle-Hardened Heirloom Armor Casing + app.CreateItem(151614), -- Weathered Heirloom Armor Casing + app.CreateItem(122340), -- Timeworn Heirloom Armor Casing + app.CreateItem(122338), -- Ancient Heirloom Armor Casing + }; + local weaponTokens = { + app.CreateItem(187998), -- Eternal Heirloom Scabbard + app.CreateItem(167732), -- Battle-Hardened Heirloom Scabbard + app.CreateItem(151615), -- Weathered Heirloom Scabbard + app.CreateItem(122341), -- Timeworn Heirloom Scabbard + app.CreateItem(122339), -- Ancient Heirloom Scabbard + }; + + -- cache the heirloom upgrade tokens + for i,item in ipairs(armorTokens) do + item.g = {}; + end + for i,item in ipairs(weaponTokens) do + item.g = {}; + end + -- for each cached heirloom, push a copy of itself with respective upgrade level under the respective upgrade token + local heirloom, upgrades, isWeapon; + local uniques = {}; + for _,itemID in ipairs(heirloomIDs) do + if not uniques[itemID] then + uniques[itemID] = true; + + heirloom = app.SearchForObject("itemID", itemID); + if heirloom then + upgrades = C_Heirloom_GetHeirloomMaxUpgradeLevel(itemID); + if upgrades then + isWeapon = heirloom.isWeapon; + + local heirloomHeader; + for i=1,upgrades,1 do + -- Create a non-collectible version of the heirloom item itself to hold the upgrade within the token + heirloomHeader = CloneData(heirloom); + heirloomHeader.collectible = false; + -- put the upgrade object into the header heirloom object + heirloomHeader.g = { setmetatable({ ["level"] = i, ["heirloomLevelID"] = itemID, ["u"] = heirloom.u, ["f"] = heirloom.f }, app.BaseHeirloomLevel) }; + + -- add the header into the appropriate upgrade token + if isWeapon then + tinsert(weaponTokens[upgrades + 1 - i].g, heirloomHeader); + else + tinsert(armorTokens[upgrades + 1 - i].g, heirloomHeader); + end + end + end + end end end - if not mapObject.text then - local mapInfo = C_Map_GetMapInfo(id); - if mapInfo then - mapObject.text = mapInfo.name; + -- build groups for each upgrade token + -- and copy the set of upgrades into the cached versions of the upgrade tokens so they therefore exist in the main list + -- where the sources of the upgrade tokens exist + local cachedTokenGroups; + for i,item in ipairs(armorTokens) do + cachedTokenGroups = app.SearchForField("itemID", item.itemID); + for _,token in ipairs(cachedTokenGroups) do + -- ensure the tokens do not have a modID attached + token.modID = nil; + if not token.sym then + for _,heirloom in ipairs(item.g) do + NestObject(token, heirloom, true); + end + BuildGroups(token, token.g); + end + end + end + for i,item in ipairs(weaponTokens) do + cachedTokenGroups = app.SearchForField("itemID", item.itemID); + for _,token in ipairs(cachedTokenGroups) do + -- ensure the tokens do not have a modID attached + token.modID = nil; + if not token.sym then + for _,heirloom in ipairs(item.g) do + NestObject(token, heirloom, true); + end + BuildGroups(token, token.g); + end end end - return mapObject; -end -app.events.ZONE_CHANGED_INDOORS = function() - app.GetCurrentMapID(); -end -app.events.ZONE_CHANGED_NEW_AREA = function() - app.GetCurrentMapID(); + wipe(heirloomIDs); end -app:RegisterEvent("ZONE_CHANGED_INDOORS"); -app:RegisterEvent("ZONE_CHANGED_NEW_AREA"); end)(); --- Mount Lib +-- Toy Lib (function() -local C_MountJournal_GetMountInfoExtraByID = C_MountJournal.GetMountInfoExtraByID; -local C_MountJournal_GetMountInfoByID = C_MountJournal.GetMountInfoByID; -local C_MountJournal_GetMountIDs = C_MountJournal.GetMountIDs; -local GetSpellInfo = GetSpellInfo; -local GetSpellLink = GetSpellLink; -local IsSpellKnown = IsSpellKnown; -local SpellIDToMountID = setmetatable({}, { __index = function(t, id) - local allMountIDs = C_MountJournal_GetMountIDs(); - if allMountIDs and #allMountIDs > 0 then - for i,mountID in ipairs(allMountIDs) do - local spellID = select(2, C_MountJournal_GetMountInfoByID(mountID)); - if spellID then rawset(t, spellID, mountID); end - end - setmetatable(t, nil); - return rawget(t, id); +-- copy base Item fields +local fields = RawCloneData(itemFields); +fields.filterID = function(t) + return 102; end -end }); -local cache = app.CreateCache("spellID"); -local function CacheInfo(t, field) - local itemID = t.itemID; - local _t, id = cache.GetCached(t); - local mountID = SpellIDToMountID[id]; - if mountID then - local displayID, lore = C_MountJournal_GetMountInfoExtraByID(mountID); - _t.displayID = displayID; - _t.lore = lore; - _t.name = C_MountJournal_GetMountInfoByID(mountID); - _t.mountID = mountID; +fields.collectible = function(t) + return app.CollectibleToys; end - local name, _, icon = GetSpellInfo(id); - if name then - _t.text = "|cffb19cd9"..name.."|r"; - _t.icon = icon; +fields.collected = function(t) + return ATTAccountWideData.Toys[t.itemID]; end - if itemID then - local itemName = select(2, GetItemInfo(itemID)); - -- item info might not be available on first request, so don't cache the data - if itemName then - _t.link = itemName; - end - else - _t.link = GetSpellLink(id); +fields.tsm = function(t) + return sformat("i:%d", t.itemID); end - if field then return _t[field]; end -end -local function default_costCollectibles(t) - local id = t.itemID; - if id then - local results = app.SearchForField("itemIDAsCost", id); - if results and #results > 0 then - -- app.PrintDebug("default_costCollectibles",t.hash,id,#results) - return results; - end +fields.isToy = app.ReturnTrue; +fields.toyID = function(t) + return t.itemID; end - return app.EmptyTable; -end -local mountFields = { - ["key"] = function(t) - return "spellID"; - end, - ["_cache"] = function(t) - return cache; - end, - ["text"] = function(t) - return cache.GetCachedField(t, "text", CacheInfo); - end, - ["icon"] = function(t) - return cache.GetCachedField(t, "icon", CacheInfo); - end, - ["link"] = function(t) - return cache.GetCachedField(t, "link", CacheInfo); - end, - ["filterID"] = function(t) - return 100; - end, - ["collectible"] = function(t) - return app.CollectibleMounts; - end, - ["costCollectibles"] = function(t) - return cache.GetCachedField(t, "costCollectibles", default_costCollectibles); - end, - ["collectibleAsCost"] = app.CollectibleAsCost, - ["collectedAsCost"] = app.CollectedAsCost, - ["collected"] = function(t) - if ATTAccountWideData.Spells[t.spellID] then return 1; end - if IsSpellKnown(t.spellID) or (t.questID and IsQuestFlaggedCompleted(t.questID)) then - ATTAccountWideData.Spells[t.spellID] = 1; - return 1; - end - end, - ["costTotal"] = function(t) - return t.collectibleAsCost and 1 or 0; - end, - ["costProgress"] = function(t) - return t.collectedAsCost and 1 or 0; - end, - ["b"] = function(t) - return (t.parent and t.parent.b) or 1; - end, - ["mountID"] = function(t) - return cache.GetCachedField(t, "mountID", CacheInfo); - end, - ["lore"] = function(t) - return cache.GetCachedField(t, "lore", CacheInfo); - end, - ["displayID"] = function(t) - return cache.GetCachedField(t, "displayID", CacheInfo); - end, - ["name"] = function(t) - return cache.GetCachedField(t, "name", CacheInfo); - end, - ["tsm"] = function(t) - if t.itemID then return sformat("i:%d", t.itemID); end - if t.parent and t.parent.itemID then return sformat("i:%d", t.parent.itemID); end - end, -}; -app.BaseMount = app.BaseObjectFields(mountFields, "BaseMount"); -local fields = RawCloneData(mountFields); -app.BaseMountWithItemID = app.BaseObjectFields(fields, "BaseMountWithItemID"); -app.CreateMount = function(id, t) - -- if t and rawget(t, "itemID") then - -- return setmetatable(constructor(id, t, "spellID"), app.BaseMountWithItemID); - -- else - return setmetatable(constructor(id, t, "spellID"), app.BaseMount); - -- end +app.BaseToy = app.BaseObjectFields(fields, "BaseToy"); +app.CreateToy = function(id, t) + return setmetatable(constructor(id, t, "itemID"), app.BaseToy); end +end)(); --- Refresh a specific Mount or all Mounts if not provided with a specific ID -local RefreshMounts = function(newMountID) - local collectedSpells, newMounts = ATTAccountWideData.Spells; - -- Think learning multiple mounts at once or multiple mounts without leaving combat - -- would fail to update all the mounts, so probably just best to check all mounts if this is triggered - -- plus it's not laggy now to do that so it should be fine - - for i,mountID in ipairs(C_MountJournal.GetMountIDs()) do - local _, spellID, _, _, _, _, _, _, _, _, isCollected = C_MountJournal_GetMountInfoByID(mountID); - if spellID and isCollected then - if not collectedSpells[spellID] then - collectedSpells[spellID] = 1; - if not newMounts then newMounts = { spellID } - else tinsert(newMounts, spellID); end +local HarvestedItemDatabase; +local C_Item_GetItemInventoryTypeByID = C_Item.GetItemInventoryTypeByID; +local itemHarvesterFields = RawCloneData(itemFields); +itemHarvesterFields.visible = app.ReturnTrue; +itemHarvesterFields.collectible = app.ReturnTrue; +itemHarvesterFields.collected = app.ReturnFalse; +itemHarvesterFields.text = function(t) + -- delayed localization since ATT's globals don't exist when this logic is processed on load + if not HarvestedItemDatabase then + HarvestedItemDatabase = LocalizeGlobal("AllTheThingsHarvestItems", true); + end + local link = t.link; + if link then + local itemName, itemLink, itemQuality, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, + itemEquipLoc, itemTexture, sellPrice, classID, subclassID, bindType, expacID, setID, isCraftingReagent + = GetItemInfo(link); + if itemName then + local spellName, spellID; + -- Recipe or Mount, grab the spellID if possible + if classID == LE_ITEM_CLASS_RECIPE or (classID == LE_ITEM_CLASS_MISCELLANEOUS and subclassID == LE_ITEM_MISCELLANEOUS_MOUNT) then + spellName, spellID = GetItemSpell(t.itemID); + -- print("Recipe/Mount",classID,subclassID,spellName,spellID); + if spellName == "Learning" then spellID = nil; end -- RIP. + end + setmetatable(t, app.BaseItemTooltipHarvester); + local info = { + ["name"] = itemName, + ["itemID"] = t.itemID, + ["equippable"] = itemEquipLoc and itemEquipLoc ~= "" and true or false, + ["class"] = classID, + ["subclass"] = subclassID, + ["inventoryType"] = C_Item_GetItemInventoryTypeByID(t.itemID), + ["b"] = bindType, + ["q"] = itemQuality, + ["iLvl"] = itemLevel, + ["spellID"] = spellID, + }; + if itemMinLevel and itemMinLevel > 0 then + info.lvl = itemMinLevel; + end + if info.inventoryType == 0 then + info.inventoryType = nil; + end + if not app.IsBoP(info) then + info.b = nil; + end + if info.q and info.q < 1 then + info.q = nil; end + if info.iLvl and info.iLvl < 2 then + info.iLvl = nil; + end + -- can debug output for tooltip harvesting + -- if t.itemID == 141038 then + -- info._debug = true; + -- end + t.itemType = itemType; + t.itemSubType = itemSubType; + t.info = info; + t.retries = nil; + HarvestedItemDatabase[t.itemID] = info; + return link; end end - UpdateRawIDs("spellID", newMounts); - if #newMounts > 0 then - app:PlayRareFindSound(); - app:TakeScreenShot(); - end -end -app.events.NEW_MOUNT_ADDED = function(newMountID, ...) - -- print("NEW_MOUNT_ADDED", newMountID, ...) - AfterCombatCallback(RefreshMounts, newMountID); + + local name = t.name; + -- retries exceeded, so check the raw .name on the group (gets assigned when retries exceeded during cache attempt) + if name then rawset(t, "collected", true); end + return name; end -app:RegisterEvent("NEW_MOUNT_ADDED"); -end)(); +app.BaseItemHarvester = app.BaseObjectFields(itemHarvesterFields, "BaseItemHarvester"); --- Music Rolls & Selfie Filter Lib: Music Rolls -(function() -local GetSpellLink, GetSpellInfo = GetSpellLink, GetSpellInfo; -local fields = { - ["key"] = function(t) - return "questID"; - end, - ["text"] = function(t) - return t.link; - end, - ["link"] = function(t) - local _, link, _, _, _, _, _, _, _, icon = GetItemInfo(t.itemID); - if link then - rawset(t, "link", link); - rawset(t, "icon", icon); - return link; - end - end, - ["icon"] = function(t) - local _, link, _, _, _, _, _, _, _, icon = GetItemInfo(t.itemID); - if link then - rawset(t, "link", link); - rawset(t, "icon", icon); - return icon; - end - end, - ["description"] = function(t) - -- Check to make sure music rolls are unlocked for this character. - if not IsQuestFlaggedCompleted(38356) or IsQuestFlaggedCompleted(37961) then - return L["MUSIC_ROLLS_AND_SELFIE_DESC"] .. L["MUSIC_ROLLS_AND_SELFIE_DESC_2"]; +local ItemHarvester = CreateFrame("GameTooltip", "ATTItemHarvester", UIParent, "GameTooltipTemplate"); +local itemTooltipHarvesterFields = RawCloneData(itemHarvesterFields); +itemTooltipHarvesterFields.text = function(t) + local link = t.link; + if link then + ItemHarvester:SetOwner(UIParent,"ANCHOR_NONE") + ItemHarvester:SetHyperlink(link); + -- a way to capture when the tooltip is giving information about something that is NOT the current ItemID + local isSubItem, craftName; + local lineCount = ItemHarvester:NumLines(); + if ATTItemHarvesterTextLeft1:GetText() and ATTItemHarvesterTextLeft1:GetText() ~= RETRIEVING_DATA and lineCount > 0 then + -- local debugPrint = t.info._debug; + -- if debugPrint then print("Item Info:",t.info.itemID) end + for index=1,lineCount,1 do + local line = _G["ATTItemHarvesterTextLeft" .. index] or _G["ATTItemHarvesterText" .. index]; + if line then + local text = line:GetText(); + if text then + -- sub items within recipe tooltips show this text, need to wait until it loads + if text == RETRIEVING_ITEM_INFO then + t.info.retries = (t.info.retries or 0) + 1; + -- 30 attempts to load the sub-item, otherwise just continue parsing tooltip without it + if t.info.retries < 30 then + return RETRIEVING_DATA; + end + app.PrintDebug("Failed loading sub-item for",t.info.itemID) + end + -- pull the "Recipe Type: Recipe Name" out if it matches + if index == 1 then + -- if debugPrint then + -- print("line match",text:match("^[^:]+:%s*([^:]+)$")) + -- end + craftName = text:match("^[^:]+:%s*([^:]+)$"); + if craftName then + -- whitespace search... recipes have whitespace and then a sub-item + craftName = "^%s+"; + end + -- use this name to check that the Item it creates may be listed underneath, by finding whitespace after a matching recipe name + elseif craftName and text:match(craftName) then + -- if debugPrint then + -- print("subitem",t.info.itemID,craftName) + -- end + isSubItem = true; + -- leave the sub-item info when reaching the 'Requires' portion of the parent item tooltip + elseif isSubItem and text:match("^Requires") then + -- if debugPrint then + -- print("leaving subitem",t.info.itemID,craftName) + -- end + -- leaving the sub-item tooltip when encountering 'Requires ' + isSubItem = nil; + end + + if not isSubItem then + -- if debugPrint then print(text) end + if string.find(text, "Classes: ") then + local classes = {}; + local _,list = strsplit(":", text); + for i,s in ipairs({strsplit(",", list)}) do + tinsert(classes, app.ClassDB[strtrim(s)]); + end + if #classes > 0 then + t.info.classes = classes; + end + elseif string.find(text, "Races: ") then + local _,list = strsplit(":", text); + local raceNames = {strsplit(",", list)}; + if raceNames then + local races = {}; + for _,s in ipairs(raceNames) do + local race = app.RaceDB[strtrim(s)]; + if not race then + print("Unknown Race",t.info.itemID,strtrim(s)) + elseif type(race) == "number" then + tinsert(races, race); + else -- Pandaren + for _,panda in pairs(race) do + tinsert(races, panda); + end + end + end + if #races > 0 then + t.info.races = races; + end + else + print("Empty Races",t.info.itemID) + end + elseif string.find(text, " Only") then + local faction,list,c = strsplit(" ", text); + if not c then + faction = strtrim(faction); + if faction == "Alliance" then + t.info.races = app.FACTION_RACES[1]; + elseif faction == "Horde" then + t.info.races = app.FACTION_RACES[2]; + else + print("Unknown Faction",t.info.itemID,faction); + end + end + elseif string.find(text, "Requires") and not string.find(text, "Level") and not string.find(text, "Riding") then + local c = strsub(text, 1, 1); + if c ~= " " and c ~= "\t" and c ~= "\n" and c ~= "\r" then + text = strsub(strtrim(text), 9); + if string.find(text, "-") then + local faction,replevel = strsplit("-", text); + t.info.minReputation = { app.GetFactionIDByName(faction), app.GetFactionStandingThresholdFromString(replevel) }; + else + if string.find(text, "%(") then + if t.info.requireSkill then + -- If non-specialization skill is already assigned, skip this part. + text = nil; + else + text = strsplit("(", text); + end + end + if text then + local spellName = strtrim(text); + if string.find(spellName, "Outland ") then spellName = strsub(spellName, 9); + elseif string.find(spellName, "Northrend ") then spellName = strsub(spellName, 11); + elseif string.find(spellName, "Cataclysm ") then spellName = strsub(spellName, 11); + elseif string.find(spellName, "Pandaria ") then spellName = strsub(spellName, 10); + elseif string.find(spellName, "Draenor ") then spellName = strsub(spellName, 9); + elseif string.find(spellName, "Legion ") then spellName = strsub(spellName, 8); + elseif string.find(spellName, "Kul Tiran ") then spellName = strsub(spellName, 11); + elseif string.find(spellName, "Zandalari ") then spellName = strsub(spellName, 11); + elseif string.find(spellName, "Shadowlands ") then spellName = strsub(spellName, 13); + elseif string.find(spellName, "Classic ") then spellName = strsub(spellName, 9); end + if spellName == "Herbalism" then spellName = "Herb Gathering"; end + spellName = strtrim(spellName); + local spellID = app.SpellNameToSpellID[spellName]; + if spellID then + local skillID = app.SpellIDToSkillID[spellID]; + if skillID then + t.info.requireSkill = skillID; + elseif spellName == "Pick Pocket" then + -- Do nothing, for now. + elseif spellName == "Warforged Nightmare" then + -- Do nothing, for now. + else + print("Unknown Skill",t.info.itemID, text, "'" .. spellName .. "'"); + end + elseif spellName == "Previous Rank" then + -- Do nothing + elseif spellName == "" then + -- Do nothing + elseif spellName == "Brewfest" then + -- Do nothing, yet. + elseif spellName == "Call of the Scarab" then + -- Do nothing, yet. + elseif spellName == "Children's Week" then + -- Do nothing, yet. + elseif spellName == "Darkmoon Faire" then + -- Do nothing, yet. + elseif spellName == "Day of the Dead" then + -- Do nothing, yet. + elseif spellName == "Feast of Winter Veil" then + -- Do nothing, yet. + elseif spellName == "Hallow's End" then + -- Do nothing, yet. + elseif spellName == "Love is in the Air" then + -- Do nothing, yet. + elseif spellName == "Lunar Festival" then + -- Do nothing, yet. + elseif spellName == "Midsummer Fire Festival" then + -- Do nothing, yet. + elseif spellName == "Moonkin Festival" then + -- Do nothing, yet. + elseif spellName == "Noblegarden" then + -- Do nothing, yet. + elseif spellName == "Pilgrim's Bounty" then + -- Do nothing, yet. + elseif spellName == "Un'Goro Madness" then + -- Do nothing, yet. + elseif spellName == "Thousand Boat Bash" then + -- Do nothing, yet. + elseif spellName == "Glowcap Festival" then + -- Do nothing, yet. + elseif spellName == "Battle Pet Training" then + -- Do nothing. + elseif spellName == "Lockpicking" then + -- Do nothing. + elseif spellName == "Luminous Luminaries" then + -- Do nothing. + elseif spellName == "Pick Pocket" then + -- Do nothing. + elseif spellName == "WoW's 14th Anniversary" then + -- Do nothing. + elseif spellName == "WoW's 13th Anniversary" then + -- Do nothing. + elseif spellName == "WoW's 12th Anniversary" then + -- Do nothing. + elseif spellName == "WoW's 11th Anniversary" then + -- Do nothing. + elseif spellName == "WoW's 10th Anniversary" then + -- Do nothing. + elseif spellName == "WoW's Anniversary" then + -- Do nothing. + elseif spellName == "level 1 to 29" then + -- Do nothing. + elseif spellName == "level 1 to 39" then + -- Do nothing. + elseif spellName == "level 1 to 44" then + -- Do nothing. + elseif spellName == "level 1 to 49" then + -- Do nothing. + elseif spellName == "Unknown" then + -- Do nothing. + elseif spellName == "Open" then + -- Do nothing. + elseif string.find(spellName, " specialization") then + -- Do nothing. + elseif string.find(spellName, ": ") then + -- Do nothing. + else + print("Unknown Spell",t.info.itemID, text, "'" .. spellName .. "'"); + end + end + end + end + end + end + end + end + end + -- if debugPrint then print("---") end + t.info.retries = nil; + rawset(t, "text", link); + rawset(t, "collected", true); end - return L["MUSIC_ROLLS_AND_SELFIE_DESC"]; - end, - ["filterID"] = function(t) - return 108; - end, - ["lvl"] = function(t) - return 40; - end, - ["collectible"] = function(t) - return app.CollectibleMusicRollsAndSelfieFilters; - end, - ["trackable"] = app.ReturnTrue, - ["collected"] = function(t) - if IsQuestFlaggedCompleted(t.questID) then return 1; end - if app.AccountWideMusicRollsAndSelfieFilters and ATTAccountWideData.Quests[t.questID] then return 2; end - end, - ["saved"] = function(t) - return IsQuestFlaggedCompleted(t.questID); - end, -}; -app.BaseMusicRoll = app.BaseObjectFields(fields, "BaseMusicRoll"); -app.CreateMusicRoll = function(questID, t) - return setmetatable(constructor(questID, t, "questID"), app.BaseMusicRoll); + ItemHarvester:Hide(); + return link; + end +end +app.BaseItemTooltipHarvester = app.BaseObjectFields(itemTooltipHarvesterFields, "BaseItemTooltipHarvester"); +app.CreateItemHarvester = function(id, t) + return setmetatable(constructor(id, t, "itemID"), app.BaseItemHarvester); end -local fields = { - ["key"] = function(t) - return "questID"; - end, - ["text"] = function(t) - return t.link; - end, - ["icon"] = function(t) - return select(3, GetSpellInfo(t.spellID)); - end, - ["link"] = function(t) - return select(1, GetSpellLink(t.spellID)); - end, - ["description"] = function(t) - if t.crs and #t.crs > 0 then - for i,id in ipairs(t.crs) do - return L["SELFIE_DESC"] .. (select(2, GetItemInfo(122674)) or "Selfie Camera MkII") .. L["SELFIE_DESC_2"] .. (app.NPCNameFromID[id] or "???") - .. "|r" .. (t.maps and (" in |cffff8000" .. (app.GetMapName(t.maps[1]) or "???") .. "|r.") or "."); +-- Imports the raw information from the rawlink into the specified group +app.ImportRawLink = function(group, rawlink) + rawlink = string.match(rawlink, "item[%-?%d:]+"); + if rawlink and group then + group.rawlink = rawlink; + local _, linkItemID, enchantId, gemId1, gemId2, gemId3, gemId4, suffixId, uniqueId, linkLevel, specializationID, upgradeId, modID, bonusCount, bonusID1 = strsplit(":", rawlink); + if linkItemID then + -- app.PrintDebug("ImportRawLink",rawlink,linkItemID,modID,bonusCount,bonusID1); + -- set raw fields in the group based on the link + group.itemID = tonumber(linkItemID); + group.modID = modID and tonumber(modID) or nil; + -- only set the bonusID if there is actually bonusIDs indicated + if (tonumber(bonusCount) or 0) > 0 then + -- Don't use bonusID 3524 as an actual bonusID + local b = bonusID1 and tonumber(bonusID1) or nil; + if b ~= 3524 then + group.bonusID = b; + end end + group.modItemID = nil; + -- does this link also have a sourceID? + local s = GetSourceID(rawlink); + -- print("s",s) + if s then group.s = s; end + -- if app.DEBUG_PRINT then app.PrintTable(group) end end - end, - ["collectible"] = function(t) - return app.CollectibleMusicRollsAndSelfieFilters; - end, - ["collected"] = function(t) - if IsQuestFlaggedCompleted(t.questID) then return 1; end - if app.AccountWideMusicRollsAndSelfieFilters and ATTAccountWideData.Quests[t.questID] then - return 2; - end - end, - ["trackable"] = app.ReturnTrue, - ["saved"] = function(t) - return IsQuestFlaggedCompleted(t.questID); - end, - ["lvl"] = function(t) - return 40; - end, -}; -app.BaseSelfieFilter = app.BaseObjectFields(fields, "BaseSelfieFilter"); -app.CreateSelfieFilter = function(id, t) - return setmetatable(constructor(id, t, "questID"), app.BaseSelfieFilter); + end end -end)(); - --- NPC Lib -(function() --- NPC Model Harvester (also acquires the displayID) -local npcModelHarvester = CreateFrame("DressUpModel", nil, UIParent); -npcModelHarvester:SetPoint("TOPRIGHT", UIParent, "BOTTOMRIGHT", 0, 0); -npcModelHarvester:SetSize(1, 1); -npcModelHarvester:Hide(); -app.NPCDisplayIDFromID = setmetatable({}, { __index = function(t, id) - if id > 0 then - npcModelHarvester:SetDisplayInfo(0); - npcModelHarvester:SetUnit("none"); - npcModelHarvester:SetCreature(id); - local displayID = npcModelHarvester:GetDisplayInfo(); - if displayID and displayID ~= 0 then - rawset(t, id, displayID); - return displayID; +-- Refines a set of items down to the most-accurate matches to the provided modItemID +-- The sets of items will be returned based on their respective match depth to the given modItemID +-- Ex: { [1] = { { ItemID }, { ItemID2 } }, [2] = { { ModID } }, [3] = { { BonusID } } } +app.GroupBestMatchingItems = function(items, modItemID) + if not items or #items == 0 then return; end + -- print("refining",#items,"by depth to",modItemID) + -- local i, m, b = GetItemIDAndModID(modItemID); + local refinedBuckets, GetDepth, depth = {}, app.ItemMatchDepth; + for _,item in ipairs(items) do + depth = GetDepth(item, modItemID); + if depth then + -- print("added refined item",depth,item.modItemID,item.key,item.key and item[item.key]) + if refinedBuckets[depth] then tinsert(refinedBuckets[depth], item) + else refinedBuckets[depth] = { item }; end end end -end}); -local npcFields = { - ["key"] = function(t) - return "npcID"; - end, - ["name"] = function(t) - return app.NPCNameFromID[t.npcID]; - end, - ["title"] = function(t) - return app.NPCTitlesFromID[t.npcID]; - end, - ["displayID"] = function(t) - return app.NPCDisplayIDFromID[t.npcID]; - end, - ["creatureID"] = function(t) -- TODO: Do something about this, it's silly. - return t.npcID; - end, - - ["iconAsDefault"] = function(t) - return (t.parent and t.parent.headerID == -2 and "Interface\\Icons\\INV_Misc_Coin_01") - or app.DifficultyIcons[GetRelativeValue(t, "difficultyID") or 1]; - end, - ["nameAsAchievement"] = function(t) - return app.NPCNameFromID[t.npcID] or select(2, GetAchievementInfo(t.achievementID)); - end, - ["iconAsAchievement"] = function(t) - return select(10, GetAchievementInfo(t.achievementID)) or t.iconAsDefault; - end, - ["linkAsAchievement"] = function(t) - return GetAchievementLink(t.achievementID); - end, - ["collectibleAsQuest"] = app.CollectibleAsQuest, - ["collectedAsQuest"] = IsQuestFlaggedCompletedForObject, - ["savedAsQuest"] = function(t) - return IsQuestFlaggedCompleted(t.questID) or IsAnyQuestFlaggedCompleted(t.altQuests); - end, - ["trackableAsQuest"] = app.ReturnTrue, - ["repeatableAsQuest"] = function(t) - return rawget(t, "isDaily") or rawget(t, "isWeekly") or rawget(t, "isMonthly") or rawget(t, "isYearly") or rawget(t, "isWorldQuest"); - end, - ["altcollectedAsQuest"] = function(t) - if t.altQuests then - for i,questID in ipairs(t.altQuests) do - if IsQuestFlaggedCompleted(questID) then - rawset(t, "altcollected", questID); - return questID; - end + return refinedBuckets; +end +-- Returns the depth at which a given Item matches the provided modItemID +-- 1 = ItemID, 2 = ModID, 3 = BonusID +app.ItemMatchDepth = function(item, modItemID) + if not item or not item.itemID then return; end + local i, m, b = GetItemIDAndModID(modItemID); + local depth = 0; + if item.itemID == i then + depth = depth + 1; + if item.modID == m then + depth = depth + 1; + if item.bonusID == b then + depth = depth + 1; end end - end, - ["indicatorIcon"] = function(t) - if app.CurrentVignettes["npcID"][t.npcID] then - return "Category_Secrets"; - end - end, -}; -npcFields.icon = npcFields.iconAsDefault; -app.BaseNPC = app.BaseObjectFields(npcFields, "BaseNPC"); - -local fields = RawCloneData(npcFields); -fields.icon = npcFields.iconAsAchievement; ---fields.link = npcFields.linkAsAchievement; -- Go to Broken Shore -> Command Center -> -app.BaseNPCWithAchievement = app.BaseObjectFields(fields, "BaseNPCWithAchievement"); - -local fields = RawCloneData(npcFields); -fields.altcollected = npcFields.altcollectedAsQuest; -fields.collectible = npcFields.collectibleAsQuest; -fields.collected = npcFields.collectedAsQuest; -fields.trackable = npcFields.trackableAsQuest; -fields.repeatable = npcFields.repeatableAsQuest; -fields.saved = fields.savedAsQuest; -app.BaseNPCWithQuest = app.BaseObjectFields(fields, "BaseNPCWithQuest"); - -local fields = RawCloneData(npcFields); -fields.icon = npcFields.iconAsAchievement; ---fields.link = npcFields.linkAsAchievement; -fields.altcollected = npcFields.altcollectedAsQuest; -fields.collectible = npcFields.collectibleAsQuest; -fields.collected = npcFields.collectedAsQuest; -fields.trackable = npcFields.trackableAsQuest; -fields.repeatable = npcFields.repeatableAsQuest; -fields.saved = fields.savedAsQuest; -app.BaseNPCWithAchievementAndQuest = app.BaseObjectFields(fields, "BaseNPCWithAchievementAndQuest"); + end + return depth; +end +end)(); --- Header Lib -local headerFields = { +-- Map Lib +(function() +local C_Map_GetMapLevels = C_Map.GetMapLevels; +local C_Map_GetBestMapForUnit = C_Map.GetBestMapForUnit; +app.GetCurrentMapID = function() + local uiMapID = C_Map_GetBestMapForUnit("player"); + if uiMapID then + local map = C_Map_GetMapInfo(uiMapID); + if map then + local ZONE_TEXT_TO_MAP_ID = app.L["ZONE_TEXT_TO_MAP_ID"]; + local real = GetRealZoneText(); + local otherMapID = real and ZONE_TEXT_TO_MAP_ID[real]; + if otherMapID then + uiMapID = otherMapID; + else + local zone = GetSubZoneText(); + if zone then + otherMapID = ZONE_TEXT_TO_MAP_ID[zone]; + if otherMapID then uiMapID = otherMapID; end + end + end + end + -- print("Current UI Map ID: ", uiMapID); + -- if entering an instance, clear the search Cache so that proper difficulty tooltips are re-generated + if IsInInstance() then wipe(searchCache); end + app.CurrentMapID = uiMapID; + end + return uiMapID; +end +app.GetMapName = function(mapID) + if mapID and mapID > 0 then + local info = C_Map_GetMapInfo(mapID); + return (info and info.name) or ("Map ID #" .. mapID); + else + return "Map ID #???"; + end +end +local mapFields = { ["key"] = function(t) - return "headerID"; + return "mapID"; end, ["name"] = function(t) - return L["HEADER_NAMES"][t.headerID]; + return t.creatureID and app.NPCNameFromID[t.creatureID] or app.GetMapName(t.mapID); end, ["icon"] = function(t) - return L["HEADER_ICONS"][t.headerID]; + return t.creatureID and L["HEADER_ICONS"][t.creatureID] or app.asset("Category_Zones"); end, - ["description"] = function(t) - return L["HEADER_DESCRIPTIONS"][t.headerID]; + ["back"] = function(t) + if app.CurrentMapID == t.mapID or (t.maps and contains(t.maps, app.CurrentMapID)) then + return 1; + end end, - ["nameAsAchievement"] = function(t) - return L["HEADER_NAMES"][t.headerID] or select(2, GetAchievementInfo(t.achievementID)); + ["lvl"] = function(t) + return select(1, C_Map_GetMapLevels(t.mapID)); end, - ["iconAsAchievement"] = function(t) - return L["HEADER_ICONS"][t.headerID] or select(10, GetAchievementInfo(t.achievementID)); + ["iconForAchievement"] = function(t) + return t.achievementID and select(10, GetAchievementInfo(t.achievementID)) or app.asset("Category_Zones"); end, - ["linkAsAchievement"] = function(t) + ["linkForAchievement"] = function(t) return GetAchievementLink(t.achievementID); end, - ["savedAsQuest"] = function(t) - return IsQuestFlaggedCompleted(t.questID); - end, - ["trackableAsQuest"] = app.ReturnTrue, }; -app.BaseHeader = app.BaseObjectFields(headerFields, "BaseHeader"); -local fields = RawCloneData(headerFields); -fields.name = headerFields.nameAsAchievement; -fields.icon = headerFields.iconAsAchievement; ---fields.link = headerFields.linkAsAchievement; -app.BaseHeaderWithAchievement = app.BaseObjectFields(fields, "BaseHeaderWithAchievement"); -local fields = RawCloneData(headerFields); -fields.saved = headerFields.savedAsQuest; -fields.trackable = headerFields.trackableAsQuest; -app.BaseHeaderWithQuest = app.BaseObjectFields(fields, "BaseHeaderWithQuest"); -local fields = RawCloneData(headerFields); -fields.name = headerFields.nameAsAchievement; -fields.icon = headerFields.iconAsAchievement; ---fields.link = headerFields.linkAsAchievement; -fields.saved = headerFields.savedAsQuest; -fields.trackable = headerFields.trackableAsQuest; -app.BaseHeaderWithAchievementAndQuest = app.BaseObjectFields(fields, "BaseHeaderWithAchievementAndQuest"); -app.CreateNPC = function(id, t) - if t then - -- TEMP: clean MoH tagging from random Vendors - if rawget(t, "itemID") == 137642 then - rawset(t, "itemID", nil); - -- print("ItemID",rawget(t, "itemID"),"used on NPC/Header group... Don't do that!",id); +app.BaseMap = app.BaseObjectFields(mapFields, "BaseMap"); + +local fields = RawCloneData(mapFields); +fields.icon = mapFields.iconForAchievement; +fields.link = mapFields.linkForAchievement; +app.BaseMapWithAchievementID = app.BaseObjectFields(fields, "BaseMapWithAchievementID"); +app.CreateMap = function(id, t) + t = constructor(id, t, "mapID"); + if rawget(t, "achID") then + rawset(t, "achievementID", app.FactionID == Enum.FlightPathFaction.Horde and rawget(t, "altAchID") or rawget(t, "achID")); + t = setmetatable(t, app.BaseMapWithAchievementID); + else + t = setmetatable(t, app.BaseMap); + end + return t; +end +app.CreateMapWithStyle = function(id) + local mapObject = app.CreateMap(id, { progress = 0, total = 0 }); + for _,data in ipairs(fieldCache["mapID"][id] or {}) do + if data.mapID and data.icon then + mapObject.text = data.text; + mapObject.icon = data.icon; + mapObject.lvl = data.lvl; + mapObject.lore = data.lore; + mapObject.description = data.description; + break; end - if id < 1 then - if rawget(t, "achID") then - rawset(t, "achievementID", app.FactionID == Enum.FlightPathFaction.Horde and rawget(t, "altAchID") or rawget(t, "achID")); - if rawget(t, "questID") then - return setmetatable(constructor(id, t, "headerID"), app.BaseHeaderWithAchievementAndQuest); - else - return setmetatable(constructor(id, t, "headerID"), app.BaseHeaderWithAchievement); - end - else - if rawget(t, "questID") then - return setmetatable(constructor(id, t, "headerID"), app.BaseHeaderWithQuest); - else - return setmetatable(constructor(id, t, "headerID"), app.BaseHeader); - end - end - else - if rawget(t, "achID") then - rawset(t, "achievementID", app.FactionID == Enum.FlightPathFaction.Horde and rawget(t, "altAchID") or rawget(t, "achID")); - if rawget(t, "questID") then - return setmetatable(constructor(id, t, "npcID"), app.BaseNPCWithAchievementAndQuest); - else - return setmetatable(constructor(id, t, "npcID"), app.BaseNPCWithAchievement); - end - else - if rawget(t, "questID") then - return setmetatable(constructor(id, t, "npcID"), app.BaseNPCWithQuest); - else - return setmetatable(constructor(id, t, "npcID"), app.BaseNPC); - end - end + end + + if not mapObject.text then + local mapInfo = C_Map_GetMapInfo(id); + if mapInfo then + mapObject.text = mapInfo.name; end - elseif id > 1 then - return setmetatable(constructor(id, t, "npcID"), app.BaseNPC); - else - return setmetatable(constructor(id, t, "headerID"), app.BaseHeader); end + return mapObject; +end + +app.events.ZONE_CHANGED_INDOORS = function() + app.GetCurrentMapID(); end +app.events.ZONE_CHANGED_NEW_AREA = function() + app.GetCurrentMapID(); +end +app:RegisterEvent("ZONE_CHANGED_INDOORS"); +app:RegisterEvent("ZONE_CHANGED_NEW_AREA"); end)(); --- Object Lib (as in "World Object") +-- Mount Lib (function() -local objectFields = { +local C_MountJournal_GetMountInfoExtraByID = C_MountJournal.GetMountInfoExtraByID; +local C_MountJournal_GetMountInfoByID = C_MountJournal.GetMountInfoByID; +local C_MountJournal_GetMountIDs = C_MountJournal.GetMountIDs; +local GetSpellInfo = GetSpellInfo; +local GetSpellLink = GetSpellLink; +local IsSpellKnown = IsSpellKnown; +local SpellIDToMountID = setmetatable({}, { __index = function(t, id) + local allMountIDs = C_MountJournal_GetMountIDs(); + if allMountIDs and #allMountIDs > 0 then + for i,mountID in ipairs(allMountIDs) do + local spellID = select(2, C_MountJournal_GetMountInfoByID(mountID)); + if spellID then rawset(t, spellID, mountID); end + end + setmetatable(t, nil); + return rawget(t, id); + end +end }); +local cache = app.CreateCache("spellID"); +local function CacheInfo(t, field) + local itemID = t.itemID; + local _t, id = cache.GetCached(t); + local mountID = SpellIDToMountID[id]; + if mountID then + local displayID, lore = C_MountJournal_GetMountInfoExtraByID(mountID); + _t.displayID = displayID; + _t.lore = lore; + _t.name = C_MountJournal_GetMountInfoByID(mountID); + _t.mountID = mountID; + end + local name, _, icon = GetSpellInfo(id); + if name then + _t.text = "|cffb19cd9"..name.."|r"; + _t.icon = icon; + end + if itemID then + local itemName = select(2, GetItemInfo(itemID)); + -- item info might not be available on first request, so don't cache the data + if itemName then + _t.link = itemName; + end + else + _t.link = GetSpellLink(id); + end + if field then return _t[field]; end +end +local function default_costCollectibles(t) + local id = t.itemID; + if id then + local results = app.SearchForField("itemIDAsCost", id); + if results and #results > 0 then + -- app.PrintDebug("default_costCollectibles",t.hash,id,#results) + return results; + end + end + return app.EmptyTable; +end +local mountFields = { ["key"] = function(t) - return "objectID"; - end, - ["name"] = function(t) - return app.ObjectNames[t.objectID] or ("Object ID #" .. t.objectID); + return "spellID"; end, - ["icon"] = function(t) - return app.ObjectIcons[t.objectID] or "Interface\\Icons\\INV_Misc_Bag_10"; + ["_cache"] = function(t) + return cache; end, - ["model"] = function(t) - return app.ObjectModels[t.objectID]; + ["text"] = function(t) + return cache.GetCachedField(t, "text", CacheInfo); end, - - ["nameAsAchievement"] = function(t) - return app.NPCNameFromID[t.npcID] or select(2, GetAchievementInfo(t.achievementID)); + ["icon"] = function(t) + return cache.GetCachedField(t, "icon", CacheInfo); end, - ["iconAsAchievement"] = function(t) - return select(10, GetAchievementInfo(t.achievementID)) or t.iconAsDefault; + ["link"] = function(t) + return cache.GetCachedField(t, "link", CacheInfo); end, - ["linkAsAchievement"] = function(t) - return GetAchievementLink(t.achievementID); + ["filterID"] = function(t) + return 100; end, - ["collectibleAsQuest"] = app.CollectibleAsQuest, - ["collectedAsQuest"] = IsQuestFlaggedCompletedForObject, - ["savedAsQuest"] = function(t) - return IsQuestFlaggedCompleted(t.questID); + ["collectible"] = function(t) + return app.CollectibleMounts; end, - ["trackableAsQuest"] = app.ReturnTrue, - ["repeatableAsQuest"] = function(t) - return rawget(t, "isDaily") or rawget(t, "isWeekly") or rawget(t, "isMonthly") or rawget(t, "isYearly") or rawget(t, "isWorldQuest"); + ["costCollectibles"] = function(t) + return cache.GetCachedField(t, "costCollectibles", default_costCollectibles); end, - ["altcollectedAsQuest"] = function(t) - if t.altQuests then - for i,questID in ipairs(t.altQuests) do - if IsQuestFlaggedCompleted(questID) then - rawset(t, "altcollected", questID); - return questID; - end - end + ["collectibleAsCost"] = app.CollectibleAsCost, + -- ["collectedAsCost"] = app.CollectedAsCost, + ["collected"] = function(t) + if ATTAccountWideData.Spells[t.spellID] then return 1; end + if IsSpellKnown(t.spellID) or (t.questID and IsQuestFlaggedCompleted(t.questID)) then + ATTAccountWideData.Spells[t.spellID] = 1; + return 1; end end, - ["lockedAsQuest"] = function(t) - return app.LockedAsQuest(t); + ["costTotal"] = function(t) + return t.collectibleAsCost and 1 or 0; end, - ["indicatorIcon"] = function(t) - if app.CurrentVignettes["objectID"][t.objectID] then - return "Category_Secrets"; - end + ["costProgress"] = function(t) + return t.collectedAsCost and 1 or 0; end, - - -- Generic fields (potentially replaced by specific object types) - ["trackable"] = function(t) - -- only used for generic objects with no other way of being considered trackable - if not t.g then return; end - for _,group in ipairs(t.g) do - if group.objectID and group.trackable then return true; end - end + ["b"] = function(t) + return (t.parent and t.parent.b) or 1; end, - ["repeatable"] = function(t) - -- only used for generic objects with no other way of being tracked as repeatable - if not t.g then return; end - for _,group in ipairs(t.g) do - if group.objectID and group.repeatable then return true; end - end - -- every contained sub-object is not repeatable, so the repeated object should also be marked as not repeatable + ["mountID"] = function(t) + return cache.GetCachedField(t, "mountID", CacheInfo); end, - ["saved"] = function(t) - -- only used for generic objects with no other way of being tracked as saved - if not t.g then return; end - local anySaved; - for _,group in ipairs(t.g) do - if group.objectID then - if group.saved then - anySaved = true; - else - return; - end - end - end - -- every contained sub-object is already saved, so the repeated object should also be marked as saved - return anySaved; + ["lore"] = function(t) + return cache.GetCachedField(t, "lore", CacheInfo); end, - ["coords"] = function(t) - -- only used for generic objects with no other way of being tracked as saved - if not t.g then return; end - local unsavedCoords = {}; - for _,group in ipairs(t.g) do - -- show collected coords of all sub-objects which are not saved - if group.objectID and group.coords and not group.saved then - app.ArrayAppend(unsavedCoords, group.coords); - end - end - return unsavedCoords; + ["displayID"] = function(t) + return cache.GetCachedField(t, "displayID", CacheInfo); + end, + ["name"] = function(t) + return cache.GetCachedField(t, "name", CacheInfo); + end, + ["tsm"] = function(t) + if t.itemID then return sformat("i:%d", t.itemID); end + if t.parent and t.parent.itemID then return sformat("i:%d", t.parent.itemID); end end, }; -app.BaseObject = app.BaseObjectFields(objectFields, "BaseObject"); +app.BaseMount = app.BaseObjectFields(mountFields, "BaseMount"); -local fields = RawCloneData(objectFields); -fields.icon = objectFields.iconAsAchievement; ---fields.link = objectFields.linkAsAchievement; -app.BaseObjectWithAchievement = app.BaseObjectFields(fields, "BaseObjectWithAchievement"); +local fields = RawCloneData(mountFields); +app.BaseMountWithItemID = app.BaseObjectFields(fields, "BaseMountWithItemID"); +app.CreateMount = function(id, t) + -- if t and rawget(t, "itemID") then + -- return setmetatable(constructor(id, t, "spellID"), app.BaseMountWithItemID); + -- else + return setmetatable(constructor(id, t, "spellID"), app.BaseMount); + -- end +end -local fields = RawCloneData(objectFields); -fields.altcollected = objectFields.altcollectedAsQuest; -fields.collectible = objectFields.collectibleAsQuest; -fields.collected = objectFields.collectedAsQuest; -fields.trackable = objectFields.trackableAsQuest; -fields.repeatable = objectFields.repeatableAsQuest; -fields.saved = objectFields.savedAsQuest; -fields.locked = objectFields.lockedAsQuest; -app.BaseObjectWithQuest = app.BaseObjectFields(fields, "BaseObjectWithQuest"); +-- Refresh a specific Mount or all Mounts if not provided with a specific ID +local RefreshMounts = function(newMountID) + local collectedSpells, newMounts = ATTAccountWideData.Spells; + -- Think learning multiple mounts at once or multiple mounts without leaving combat + -- would fail to update all the mounts, so probably just best to check all mounts if this is triggered + -- plus it's not laggy now to do that so it should be fine -local fields = RawCloneData(objectFields); -fields.icon = objectFields.iconAsAchievement; ---fields.link = objectFields.linkAsAchievement; -fields.altcollected = objectFields.altcollectedAsQuest; -fields.collectible = objectFields.collectibleAsQuest; -fields.collected = objectFields.collectedAsQuest; -fields.trackable = objectFields.trackableAsQuest; -fields.repeatable = objectFields.repeatableAsQuest; -fields.saved = objectFields.savedAsQuest; -fields.locked = objectFields.lockedAsQuest; -app.BaseObjectWithAchievementAndQuest = app.BaseObjectFields(fields, "BaseObjectWithAchievementAndQuest"); -app.CreateObject = function(id, t) - if t then - if rawget(t, "achID") then - rawset(t, "achievementID", app.FactionID == Enum.FlightPathFaction.Horde and rawget(t, "altAchID") or rawget(t, "achID")); - if rawget(t, "questID") then - return setmetatable(constructor(id, t, "objectID"), app.BaseObjectWithAchievementAndQuest); - else - return setmetatable(constructor(id, t, "objectID"), app.BaseObjectWithAchievement); - end - else - if rawget(t, "questID") then - return setmetatable(constructor(id, t, "objectID"), app.BaseObjectWithQuest); + for i,mountID in ipairs(C_MountJournal.GetMountIDs()) do + local _, spellID, _, _, _, _, _, _, _, _, isCollected = C_MountJournal_GetMountInfoByID(mountID); + if spellID and isCollected then + if not collectedSpells[spellID] then + collectedSpells[spellID] = 1; + if not newMounts then newMounts = { spellID } + else tinsert(newMounts, spellID); end end end end - return setmetatable(constructor(id, t, "objectID"), app.BaseObject); + UpdateRawIDs("spellID", newMounts); + if #newMounts > 0 then + app:PlayRareFindSound(); + app:TakeScreenShot(); + end +end +app.events.NEW_MOUNT_ADDED = function(newMountID, ...) + -- print("NEW_MOUNT_ADDED", newMountID, ...) + AfterCombatCallback(RefreshMounts, newMountID); end +app:RegisterEvent("NEW_MOUNT_ADDED"); end)(); --- Profession Lib +-- Music Rolls & Selfie Filter Lib: Music Rolls (function() -app.SkillIDToSpellID = { - [171] = 2259, -- Alchemy - [794] = 158762, -- Arch - [261] = 5149, -- Beast Training - [164] = 2018, -- Blacksmithing - [185] = 2550, -- Cooking - [333] = 7411, -- Enchanting - [202] = 4036, -- Engineering - [356] = 7620, -- Fishing - [129] = 3273, -- First Aid - [182] = 2366, -- Herb Gathering - [773] = 45357, -- Inscription - [755] = 25229, -- Jewelcrafting - --[2720] = 2720, -- Junkyard Tinkering [Does not have a spellID] - [165] = 2108, -- Leatherworking - [186] = 2575, -- Mining - [393] = 8613, -- Skinning - [197] = 3908, -- Tailoring - [960] = 53428, -- Runeforging - [40] = 2842, -- Poisons - [633] = 1809, -- Lockpicking - - -- Specializations - [20219] = 20219, -- Gnomish Engineering - [20222] = 20222, -- Goblin Engineering - [9788] = 9788, -- Armorsmith - [9787] = 9787, -- Weaponsmith - [17041] = 17041, -- Master Axesmith - [17040] = 17040, -- Master Hammersmith - [17039] = 17039, -- Master Swordsmith - [10656] = 10656, -- Dragonscale Leatherworking - [10658] = 10658, -- Elemental Leatherworking - [10660] = 10660, -- Tribal Leatherworking - [26801] = 26801, -- Shadoweave Tailoring - [26797] = 26797, -- Spellfire Tailoring - [26798] = 26798, -- Mooncloth Tailoring - [125589] = 125589, -- Way of the Brew - [124694] = 124694, -- Way of the Grill - [125588] = 125588, -- Way of the Oven - [125586] = 125586, -- Way of the Pot - [125587] = 125587, -- Way of the Steamer - [125584] = 125584, -- Way of the Wok -}; -app.SpellIDToSkillID = {}; -for skillID,spellID in pairs(app.SkillIDToSpellID) do - app.SpellIDToSkillID[spellID] = skillID; -end -app.SpecializationSpellIDs = setmetatable({ - [20219] = 4036, -- Gnomish Engineering - [20222] = 4036, -- Goblin Engineering - [9788] = 2018, -- Armorsmith - [9787] = 2018, -- Weaponsmith - [17041] = 2018, -- Master Axesmith - [17040] = 2018, -- Master Hammersmith - [17039] = 2018, -- Master Swordsmith - [10656] = 2108, -- Dragonscale Leatherworking - [10658] = 2108, -- Elemental Leatherworking - [10660] = 2108, -- Tribal Leatherworking - [26801] = 3908, -- Shadoweave Tailoring - [26797] = 3908, -- Spellfire Tailoring - [26798] = 3908, -- Mooncloth Tailoring - [125589] = 2550,-- Way of the Brew - [124694] = 2550,-- Way of the Grill - [125588] = 2550,-- Way of the Oven - [125586] = 2550,-- Way of the Pot - [125587] = 2550,-- Way of the Steamer - [125584] = 2550,-- Way of the Wok -}, {__index = function(t,k) return k; end}) - +local GetSpellLink, GetSpellInfo = GetSpellLink, GetSpellInfo; local fields = { ["key"] = function(t) - return "professionID"; + return "questID"; end, - --[[ - ["name"] = function(t) - if app.GetSpecializationBaseTradeSkill(t.professionID) then return select(1, GetSpellInfo(t.professionID)); end - if t.professionID == 129 then return select(1, GetSpellInfo(t.spellID)); end - return C_TradeSkillUI.GetTradeSkillDisplayName(t.professionID); + ["text"] = function(t) + return t.link; + end, + ["link"] = function(t) + local _, link, _, _, _, _, _, _, _, icon = GetItemInfo(t.itemID); + if link then + rawset(t, "link", link); + rawset(t, "icon", icon); + return link; + end end, ["icon"] = function(t) - if app.GetSpecializationBaseTradeSkill(t.professionID) then return select(3, GetSpellInfo(t.professionID)); end - if t.professionID == 129 then return select(3, GetSpellInfo(t.spellID)); end - return C_TradeSkillUI.GetTradeSkillTexture(t.professionID); + local _, link, _, _, _, _, _, _, _, icon = GetItemInfo(t.itemID); + if link then + rawset(t, "link", link); + rawset(t, "icon", icon); + return icon; + end end, - ]]-- - ["name"] = function(t) - return t.spellID ~= 2366 and select(1, GetSpellInfo(t.spellID)) or C_TradeSkillUI.GetTradeSkillDisplayName(t.professionID); + ["description"] = function(t) + -- Check to make sure music rolls are unlocked for this character. + if not IsQuestFlaggedCompleted(38356) or IsQuestFlaggedCompleted(37961) then + return L["MUSIC_ROLLS_AND_SELFIE_DESC"] .. L["MUSIC_ROLLS_AND_SELFIE_DESC_2"]; + end + return L["MUSIC_ROLLS_AND_SELFIE_DESC"]; end, - ["icon"] = function(t) - return select(3, GetSpellInfo(t.spellID)) or C_TradeSkillUI.GetTradeSkillTexture(t.professionID); + ["filterID"] = function(t) + return 108; end, - ["spellID"] = function(t) - return app.SkillIDToSpellID[t.professionID]; + ["lvl"] = function(t) + return 40; end, - ["skillID"] = function(t) - return t.professionID; + ["collectible"] = function(t) + return app.CollectibleMusicRollsAndSelfieFilters; end, - ["requireSkill"] = function(t) - return t.professionID; + ["trackable"] = app.ReturnTrue, + ["collected"] = function(t) + if IsQuestFlaggedCompleted(t.questID) then return 1; end + if app.AccountWideMusicRollsAndSelfieFilters and ATTAccountWideData.Quests[t.questID] then return 2; end end, - --[[ - ["sym"] = function(t) - return {{"selectprofession", t.professionID}, - {"not","headerID",-38}}; -- Ignore the Main Professions header that will get pulled in + ["saved"] = function(t) + return IsQuestFlaggedCompleted(t.questID); end, - --]]-- }; -app.BaseProfession = app.BaseObjectFields(fields, "BaseProfession"); -app.CreateProfession = function(id, t) - return setmetatable(constructor(id, t, "professionID"), app.BaseProfession); +app.BaseMusicRoll = app.BaseObjectFields(fields, "BaseMusicRoll"); +app.CreateMusicRoll = function(questID, t) + return setmetatable(constructor(questID, t, "questID"), app.BaseMusicRoll); end -end)(); --- PVP Ranks -(function() local fields = { ["key"] = function(t) - return "pvpRankID"; + return "questID"; end, ["text"] = function(t) - return _G["PVP_RANK_" .. (t.pvpRankID + 4) .. "_" .. (t.inverseR or 0)]; + return t.link; end, ["icon"] = function(t) - return format("%s%02d","Interface\\PvPRankBadges\\PvPRank", t.pvpRankID); + return select(3, GetSpellInfo(t.spellID)); end, - ["title"] = function(t) - return RANK .. " " .. t.pvpRankID .. DESCRIPTION_SEPARATOR .. _G["PVP_RANK_" .. (t.pvpRankID + 4) .. "_" .. ((t.inverseR == 1 and 0 or 1))] .. " (" .. (t.r == Enum.FlightPathFaction.Alliance and FACTION_HORDE or FACTION_ALLIANCE) .. ")"; + ["link"] = function(t) + return select(1, GetSpellLink(t.spellID)); end, ["description"] = function(t) - return "There are a total of 14 ranks for both factions. Each rank requires a minimum amount of Rating Points to be calculated every week, then calculated in comparison to other players on your server.\n\nEach rank grants access to different rewards, from PvP consumables to Epic Mounts that do not require Epic Riding Skill and Epic pieces of gear at the highest ranks. Each rank is also applied to your character as a Title."; + if t.crs and #t.crs > 0 then + for i,id in ipairs(t.crs) do + return L["SELFIE_DESC"] .. (select(2, GetItemInfo(122674)) or "Selfie Camera MkII") .. L["SELFIE_DESC_2"] .. (app.NPCNameFromID[id] or "???") + .. "|r" .. (t.maps and (" in |cffff8000" .. (app.GetMapName(t.maps[1]) or "???") .. "|r.") or "."); + end + end end, - ["r"] = function(t) - return t.parent.r or app.FactionID; + ["collectible"] = function(t) + return app.CollectibleMusicRollsAndSelfieFilters; end, - ["inverseR"] = function(t) - return t.r == Enum.FlightPathFaction.Alliance and 1 or 0; + ["collected"] = function(t) + if IsQuestFlaggedCompleted(t.questID) then return 1; end + if app.AccountWideMusicRollsAndSelfieFilters and ATTAccountWideData.Quests[t.questID] then + return 2; + end end, - ["lifetimeRank"] = function(t) - return select(3, GetPVPLifetimeStats()); + ["trackable"] = app.ReturnTrue, + ["saved"] = function(t) + return IsQuestFlaggedCompleted(t.questID); end, - ["collectible"] = app.ReturnFalse, - ["collected"] = function(t) - return t.lifetimeRank >= t.pvpRankID; + ["lvl"] = function(t) + return 40; end, - ["OnTooltip"] = function(t) - GameTooltip:AddDoubleLine("Your lifetime highest rank: ", _G["PVP_RANK_" .. (t.lifetimeRank) .. "_" .. (app.FactionID == 2 and 1 or 0)], 1, 1, 1, 1, 1, 1); - end }; -app.BasePVPRank = app.BaseObjectFields(fields, "BasePVPRank"); -app.CreatePVPRank = function(id, t) - return setmetatable(constructor(id, t, "pvpRankID"), app.BasePVPRank); +app.BaseSelfieFilter = app.BaseObjectFields(fields, "BaseSelfieFilter"); +app.CreateSelfieFilter = function(id, t) + return setmetatable(constructor(id, t, "questID"), app.BaseSelfieFilter); end end)(); --- Quest Lib +-- NPC Lib (function() -local C_QuestLog_GetQuestObjectives = C_QuestLog.GetQuestObjectives; -local C_QuestLog_IsOnQuest = C_QuestLog.IsOnQuest; -local C_QuestLog_IsQuestReplayable = C_QuestLog.IsQuestReplayable; -local C_QuestLog_IsQuestReplayedRecently = C_QuestLog.IsQuestReplayedRecently; -local C_QuestLog_ReadyForTurnIn = C_QuestLog.ReadyForTurnIn; -local IsSpellKnown, GetSpellInfo, math_floor = IsSpellKnown, GetSpellInfo, math.floor; - -local function QuestConsideredSaved(questID) - if app.IsInPartySync then - return C_QuestLog_IsQuestReplayedRecently(questID) or (not C_QuestLog_IsQuestReplayable(questID) and IsQuestFlaggedCompleted(questID)); - end - return IsQuestFlaggedCompleted(questID); -end - -local criteriaFuncs = { - ["lvl"] = function(v) - return app.Level >= v; - end, - ["label_lvl"] = L["LOCK_CRITERIA_LEVEL_LABEL"], - ["text_lvl"] = function(v) - return v; - end, - - ["questID"] = function(v) - return QuestConsideredSaved(v); - end, - ["label_questID"] = L["LOCK_CRITERIA_QUEST_LABEL"], - ["text_questID"] = function(v) - local questObj = app.SearchForObject("questID", v); - return sformat("[%d] %s", v, questObj and questObj.text or "???"); - end, - - ["spellID"] = function(v) - return IsSpellKnown(v) or app.CurrentCharacter.Spells[v]; - end, - ["label_spellID"] = L["LOCK_CRITERIA_SPELL_LABEL"], - ["text_spellID"] = function(v) - return select(1, GetSpellInfo(v)); - end, - - ["factionID"] = function(v) - -- v = factionID.standingRequiredToLock - local factionID = math_floor(v + 0.00001); - local lockStanding = math_floor((v - factionID) * 10 + 0.00001); - local standing = select(3, GetFactionInfoByID(factionID)) or 1; - -- app.PrintDebug(sformat("Check Faction %s Standing (%d) is locked @ (%d)", factionID, standing, lockStanding)) - return standing >= lockStanding; - end, - ["label_factionID"] = L["LOCK_CRITERIA_FACTION_LABEL"], - ["text_factionID"] = function(v) - -- v = factionID.standingRequiredToLock - local factionID = math_floor(v + 0.00001); - local lockStanding = math_floor((v - factionID) * 10 + 0.00001); - local name, _, standing = GetFactionInfoByID(factionID); - return sformat(L["LOCK_CRITERIA_FACTION_FORMAT"], app.GetFactionStandingText(lockStanding), name, app.GetFactionStandingText(standing)); - end, -}; -app.QuestLockCriteriaFunctions = criteriaFuncs; -local function LockedAsQuest(t) - local questID = t.questID; - if not IsQuestFlaggedCompleted(questID) then - local lockCriteria = t.lc; - if lockCriteria then - local criteriaRequired = lockCriteria[1]; - local critKey, critFunc, nonQuestLock; - local i, limit = 2, #lockCriteria; - while i < limit do - critKey = lockCriteria[i]; - critFunc = criteriaFuncs[critKey]; - i = i + 1; - if critFunc then - if critFunc(lockCriteria[i]) then - criteriaRequired = criteriaRequired - 1; - if not nonQuestLock and critKey ~= "questID" then - nonQuestLock = true; - end - end - else - app.print("Unknown 'lockCriteria' key:",critKey,lockCriteria[i]); - end - -- enough criteria met to consider this quest locked - if criteriaRequired <= 0 then - -- we can rawset this since there's no real way for a player to 'remove' this lock during a session - -- and this does not come into play during party sync - rawset(t, "locked", true); - -- if this was locked due to something other than a Quest specifically, indicate it cannot be done in Party Sync - if nonQuestLock then - -- app.PrintDebug("Automatic DisablePartySync", app:Linkify(questID, app.Colors.ChatLink, "search:questID:" .. questID)) - rawset(t, "DisablePartySync", true); - end - return true; - end - i = i + 1; - end - end - -- if an alt-quest is completed, then this quest is locked - if t.altcollected then - rawset(t, "locked", t.altcollected); - return true; - end - -- determine if a 'nextQuest' exists and is completed specifically by this character, to remove availability of the breadcrumb - if t.isBreadcrumb and t.nextQuests then - local nq; - for _,questID in ipairs(t.nextQuests) do - if IsQuestFlaggedCompleted(questID) then - rawset(t, "locked", questID); - return questID; - else - -- this questID may not even be available to pick up, so try to find an object with this questID to determine if the object is complete - nq = app.SearchForObject("questID", questID); - if nq and (IsQuestFlaggedCompleted(nq.questID) or nq.altcollected or nq.locked) then - rawset(t, "locked", questID); - return questID; - end - end - end - end - end - -- rawset means that this will persist as a non-locked quest until reload, so quests that become locked while playing will not immediately update - -- maybe can revise that somehow without also having this entire logic be calculated billions of times when nothing changes.... - rawset(t, "locked", false); -end -app.LockedAsQuest = LockedAsQuest; - -local questFields = { - ["key"] = function(t) - return "questID"; - end, - ["name"] = function(t) - return app.QuestTitleFromID[t.questID]; - end, - ["objectiveInfo"] = function(t) - local questID = t.questID; - if questID then - local objectives = C_QuestLog_GetQuestObjectives(questID); - if objectives then - rawset(t, "objectiveInfo", objectives); - return objectives; - end - end - rawset(t, "objectiveInfo", app.EmptyTable) - end, - ["description"] = function(t) - -- Provide a fall-back description as to collectibility of a Quest due to granting reputation - if app.CollectibleReputations and t.maxReputation and t.collectibleAsReputation then - local factionID = t.maxReputation[1]; - return L["ITEM_GIVES_REP"] .. (select(1, GetFactionInfoByID(factionID)) or ("Faction #" .. tostring(factionID))) .. "'"; - end - end, - ["icon"] = function(t) - if t.providers then - for k,v in ipairs(t.providers) do - if v[2] > 0 then - if v[1] == "o" then - return app.ObjectIcons[v[2]] or "Interface\\Icons\\INV_Misc_Bag_10"; - elseif v[1] == "i" then - return select(5, GetItemInfoInstant(v[2])) or "Interface\\Icons\\INV_Misc_Book_09"; - end - end - end - end - if t.isWorldQuest then - return "Interface\\AddOns\\AllTheThings\\assets\\Interface_Questind"; - elseif t.repeatable then - return "Interface\\AddOns\\AllTheThings\\assets\\Interface_Questd"; - elseif t._missing then - return "Interface\\Icons\\INV_Misc_QuestionMark"; - else - return "Interface\\AddOns\\AllTheThings\\assets\\Interface_Quest"; +-- NPC Model Harvester (also acquires the displayID) +local npcModelHarvester = CreateFrame("DressUpModel", nil, UIParent); +npcModelHarvester:SetPoint("TOPRIGHT", UIParent, "BOTTOMRIGHT", 0, 0); +npcModelHarvester:SetSize(1, 1); +npcModelHarvester:Hide(); +app.NPCDisplayIDFromID = setmetatable({}, { __index = function(t, id) + if id > 0 then + npcModelHarvester:SetDisplayInfo(0); + npcModelHarvester:SetUnit("none"); + npcModelHarvester:SetCreature(id); + local displayID = npcModelHarvester:GetDisplayInfo(); + if displayID and displayID ~= 0 then + rawset(t, id, displayID); + return displayID; end + end +end}); +local npcFields = { + ["key"] = function(t) + return "npcID"; end, - ["model"] = function(t) - if t.providers then - for k,v in ipairs(t.providers) do - if v[2] > 0 then - if v[1] == "o" then - return app.ObjectModels[v[2]]; - end - end - end - end + ["name"] = function(t) + return app.NPCNameFromID[t.npcID]; end, - ["link"] = function(t) - return GetQuestLink(t.questID) or "quest:" .. t.questID; + ["title"] = function(t) + return app.NPCTitlesFromID[t.npcID]; end, - ["repeatable"] = function(t) - return rawget(t, "isDaily") or rawget(t, "isWeekly") or rawget(t, "isMonthly") or rawget(t, "isYearly") or rawget(t, "isWorldQuest"); + ["displayID"] = function(t) + return app.NPCDisplayIDFromID[t.npcID]; end, - ["collectible"] = function(t) - return app.CollectibleAsQuest(t); + ["creatureID"] = function(t) -- TODO: Do something about this, it's silly. + return t.npcID; end, - ["collected"] = function(t) - return IsQuestFlaggedCompletedForObject(t); + + ["iconAsDefault"] = function(t) + return (t.parent and t.parent.headerID == -2 and "Interface\\Icons\\INV_Misc_Coin_01") + or app.DifficultyIcons[GetRelativeValue(t, "difficultyID") or 1]; end, - ["trackable"] = app.ReturnTrue, - ["saved"] = function(t) - return QuestConsideredSaved(t.questID); + ["nameAsAchievement"] = function(t) + return app.NPCNameFromID[t.npcID] or select(2, GetAchievementInfo(t.achievementID)); end, - ["indicatorIcon"] = function(t) - return app.GetQuestIndicator(t); + ["iconAsAchievement"] = function(t) + return select(10, GetAchievementInfo(t.achievementID)) or t.iconAsDefault; end, - ["collectibleAsReputation"] = function(t) - local factionID = t.maxReputation[1]; - -- If Collectible by providing reputation towards a Faction with which the character is below the rep-granting Standing - -- and the Faction itself is Collectible & Not Collected - -- and the Quest is not locked from being completed - if app.CollectibleReputations and not t.locked then - local factionRef = app.SearchForObject("factionID", factionID); - if factionRef and not factionRef.collected and (select(6, GetFactionInfoByID(factionID)) or 0) < t.maxReputation[2] then - return true; - end - end - -- If Collectible by being a Quest - if app.CollectibleQuests or app.CollectibleQuestsLocked then - return app.CollectibleAsQuest(t); - end + ["linkAsAchievement"] = function(t) + return GetAchievementLink(t.achievementID); end, - ["collectedAsReputation"] = function(t) - -- If the Quest is completed on this character, then it doesn't matter about the faction - if IsQuestFlaggedCompleted(t.questID) then - return 1; - end - -- Check whether this Quest can provide Rep towards an incomplete Faction - if app.CollectibleReputations and t.maxReputation then - local factionID = t.maxReputation[1]; - local factionRef = app.SearchForObject("factionID", factionID); - -- Completing the quest will increase the Faction, so it is incomplete - if factionRef and not factionRef.collected and (select(6, GetFactionInfoByID(factionID)) or 0) < t.maxReputation[2] then - return false; - elseif not app.CollectibleQuests and not app.CollectibleQuestsLocked then - -- Completing the quest will not increase the Faction, but User doesn't care about Quests, then consider it 'collected' - return 2; - end - end - -- Finally, check if the quest is otherwise considered 'collected' by normal logic - return IsQuestFlaggedCompletedForObject(t); + ["collectibleAsQuest"] = app.CollectibleAsQuest, + ["collectedAsQuest"] = IsQuestFlaggedCompletedForObject, + ["savedAsQuest"] = function(t) + return IsQuestFlaggedCompleted(t.questID) or IsAnyQuestFlaggedCompleted(t.altQuests); end, - ["altcollected"] = function(t) - -- determine if an altQuest is considered completed for this quest for this character + ["trackableAsQuest"] = app.ReturnTrue, + ["repeatableAsQuest"] = function(t) + return rawget(t, "isDaily") or rawget(t, "isWeekly") or rawget(t, "isMonthly") or rawget(t, "isYearly") or rawget(t, "isWorldQuest"); + end, + ["altcollectedAsQuest"] = function(t) if t.altQuests then - for _,questID in ipairs(t.altQuests) do + for i,questID in ipairs(t.altQuests) do if IsQuestFlaggedCompleted(questID) then - -- if LOG then print(LOG,"altCollected by",questID) end rawset(t, "altcollected", questID); - return questID; - end - end - end - end, - ["missingSourceQuests"] = function(t) - if t.sourceQuests and #t.sourceQuests > 0 then - local includeBreadcrumbs = app.Settings:Get("Thing:QuestsLocked"); - local sq; - for _,sourceQuestID in ipairs(t.sourceQuests) do - if not IsQuestFlaggedCompleted(sourceQuestID) then - -- consider the breadcrumb as an actual sq since the user is tracking them - if includeBreadcrumbs then - return true; - -- otherwise incomplete breadcrumbs will not prevent picking up a quest if they are ignored - else - sq = app.SearchForObject("questID", sourceQuestID); - if sq and not sq.isBreadcrumb and not (sq.locked or sq.altcollected) then - return true; - end - end - end - end - end - end, - ["locked"] = LockedAsQuest, -}; -app.BaseQuest = app.BaseObjectFields(questFields, "BaseQuest"); - - --- These are Items rewarded by WQs which are treated as currency --- other Items which are 'costs' will not be excluded by the "WorldQuestsList:Currencies" setting -app.WorldQuestCurrencyItems = { - [163036] = true, -- Polished Pet Charms - [116415] = true, -- Shiny Pet Charms -}; --- Will attempt to populate the rewards of the quest object into itself (will become the object's OnUpdate until populated or 15 rendered frames) -app.TryPopulateQuestRewards = function(questObject) - if not questObject or not questObject.questID then return; end - if not questObject.OnUpdate then questObject.OnUpdate = app.TryPopulateQuestRewards; end - - -- track how many attempts for retrieving reward data for both types (15 frames) - questObject.missingItem = questObject.missingItem and (questObject.missingItem - 1) or 15; - questObject.missingCurr = questObject.missingCurr and (questObject.missingCurr - 1) or 15; - - -- all sub-group data will be populated via this method, so any pre-existing stuff should be cleaned out on the initial setup - if questObject.missingItem == 15 and questObject.missingCurr == 15 then - questObject.g = nil; - end - - -- app.DEBUG_PRINT = questObject.questID == 61949 and 61949; - -- if app.DEBUG_PRINT then print("TryPopulateQuestRewards",questObject.questID) end - -- print("TryPopulateQuestRewards",questObject.questID,questObject.missingItem,questObject.missingCurr) - if questObject.missingItem > 0 then - -- Get reward info - local numQuestRewards = GetNumQuestLogRewards(questObject.questID); - local skipCollectibleCurrencies = not app.Settings:GetTooltipSetting("WorldQuestsList:Currencies"); - -- numQuestRewards will often be 0 for fresh questID API calls... - -- pre-emptively call the following API method as well to get cached data earlier for the next refresh - GetQuestLogRewardInfo(1, questObject.questID); - -- if app.DEBUG_PRINT then print("TryPopulateQuestRewards:numQuestRewards",questObject.questID,numQuestRewards,questObject.missingItem) end - for j=1,numQuestRewards,1 do - local _, _, _, _, _, itemID = GetQuestLogRewardInfo(j, questObject.questID); - if itemID then - -- if app.DEBUG_PRINT then print("TryPopulateQuestRewards:found",questObject.questID,itemID) end - - QuestHarvester.AllTheThingsProcessing = true; - QuestHarvester:SetOwner(UIParent, "ANCHOR_NONE"); - QuestHarvester:SetQuestLogItem("reward", j, questObject.questID); - local link = select(2, QuestHarvester:GetItem()); - QuestHarvester.AllTheThingsProcessing = false; - QuestHarvester:Hide(); - if link then - local item = {}; - app.ImportRawLink(item, link); - -- if item.itemID == 137483 then - -- app.DEBUG_PRINT = item.itemID; - -- print("item.initial parse") - -- end - -- if app.DEBUG_PRINT then print("Parse Link", link) end - if item.itemID then - local search = SearchForLink(link); - -- search will either match through bonusID, modID, or itemID in that priority - - -- put all the item information into a basic table - -- if app.DEBUG_PRINT then print("Base Item") app.PrintTable(item) end - -- block the group from being collectible as a cost if the option is not enabled for various 'currency' items - if skipCollectibleCurrencies and app.WorldQuestCurrencyItems[item.itemID] then - item.collectibleAsCost = false; - end - if search then - -- if app.DEBUG_PRINT then print("Initial search",#search,link) end - -- find the specific item which the link represents (not sure all of this is necessary with improved search) - local exactItemID = GetGroupItemIDWithModID(item); - local subItems = {}; - local refinedMatches = app.GroupBestMatchingItems(search, exactItemID); - if refinedMatches then - -- move from depth 3 to depth 1 to find the set of items which best matches for the root - for depth=3,1,-1 do - if refinedMatches[depth] then - -- if app.DEBUG_PRINT then print("refined",depth,#refinedMatches[depth]) end - for _,o in ipairs(refinedMatches[depth]) do - MergeProperties(item, o, true); - NestObjects(item, o.g); -- no clone since item is cloned later - end - end - end - -- any matches with depth 0 will be nested - if refinedMatches[0] then - -- if app.DEBUG_PRINT then print("refined",0,#refinedMatches[0]) end - app.ArrayAppend(subItems, refinedMatches[0]); -- no clone since item is cloned later - end - end - -- then pull in any other sub-items which were not the item itself - NestObjects(item, subItems); -- no clone since item is cloned later - end - - -- at least one reward exists, so clear the missing data - questObject.missingItem = 0; - -- don't let cached groups pollute potentially inaccurate raw Data - item.link = nil; - -- if app.DEBUG_PRINT then print("Final Item") app.PrintTable(item) end - NestObject(questObject, item, true); - end - -- if app.DEBUG_PRINT then app.DEBUG_PRINT = nil; end - end - end - end - end - - -- Add info for currency rewards as containers for their respective collectibles - if questObject.missingCurr > 0 then - local numCurrencies = GetNumQuestLogRewardCurrencies(questObject.questID); - local skipCollectibleCurrencies = not app.Settings:GetTooltipSetting("WorldQuestsList:Currencies"); - -- pre-emptively call the following API method as well to get cached data earlier for the next refresh - GetQuestLogRewardCurrencyInfo(1, questObject.questID); - -- numCurrencies will often be 0 for fresh questID API calls... - local currencyID; - for j=1,numCurrencies,1 do - currencyID = select(4, GetQuestLogRewardCurrencyInfo(j, questObject.questID)); - if currencyID then - -- if app.DEBUG_PRINT then print("TryPopulateQuestRewards_currencies:found",questObject.questID,currencyID,questObject.missingCurr) end - - currencyID = tonumber(currencyID); - local item = { ["currencyID"] = currencyID }; - -- block the group from being collectible as a cost if the option is not enabled - if skipCollectibleCurrencies then - item.collectibleAsCost = false; - end - _cache = SearchForField("currencyID", currencyID); - if _cache then - for _,data in ipairs(_cache) do - -- cache record is the item itself - if GroupMatchesParams(data, "currencyID", currencyID) then - MergeProperties(item, data); - -- cache record is associated with the item - else - NestObject(item, data); -- no clone since item is cloned later - end - end + return questID; end - questObject.missingCurr = 0; - NestObject(questObject, item, true); end end - end + end, + ["indicatorIcon"] = function(t) + if app.CurrentVignettes["npcID"][t.npcID] then + return "Category_Secrets"; + end + end, +}; +npcFields.icon = npcFields.iconAsDefault; +app.BaseNPC = app.BaseObjectFields(npcFields, "BaseNPC"); - -- done attempting to populate the quest object - if questObject.missingItem < 1 and questObject.missingCurr < 1 then - -- if app.DEBUG_PRINT then print("TryPopulateQuestRewards:populated",questObject.questID) end - questObject.OnUpdate = nil; - questObject.doUpdate = true; +local fields = RawCloneData(npcFields); +fields.icon = npcFields.iconAsAchievement; +--fields.link = npcFields.linkAsAchievement; -- Go to Broken Shore -> Command Center -> +app.BaseNPCWithAchievement = app.BaseObjectFields(fields, "BaseNPCWithAchievement"); - -- Troublesome scenarios to test when changing this logic: - -- BFA emissaries - -- BFA Azerite armor caches - -- Argus Rare WQ's + Rare Alt quest +local fields = RawCloneData(npcFields); +fields.altcollected = npcFields.altcollectedAsQuest; +fields.collectible = npcFields.collectibleAsQuest; +fields.collected = npcFields.collectedAsQuest; +fields.trackable = npcFields.trackableAsQuest; +fields.repeatable = npcFields.repeatableAsQuest; +fields.saved = fields.savedAsQuest; +app.BaseNPCWithQuest = app.BaseObjectFields(fields, "BaseNPCWithQuest"); - -- Finally ensure that any cached entries for the quest are copied into this version of the object - -- Needs to be SearchForField as non-quests can be pulled too - local cachedQuests = SearchForField("questID", questObject.questID); - if cachedQuests then - -- special care for API provided items - local apiItems = {}; - if questObject.g then - for _,item in ipairs(questObject.g) do - if item.itemID then - apiItems[item.itemID] = item; - end +local fields = RawCloneData(npcFields); +fields.icon = npcFields.iconAsAchievement; +--fields.link = npcFields.linkAsAchievement; +fields.altcollected = npcFields.altcollectedAsQuest; +fields.collectible = npcFields.collectibleAsQuest; +fields.collected = npcFields.collectedAsQuest; +fields.trackable = npcFields.trackableAsQuest; +fields.repeatable = npcFields.repeatableAsQuest; +fields.saved = fields.savedAsQuest; +app.BaseNPCWithAchievementAndQuest = app.BaseObjectFields(fields, "BaseNPCWithAchievementAndQuest"); + +-- Header Lib +local headerFields = { + ["key"] = function(t) + return "headerID"; + end, + ["name"] = function(t) + return L["HEADER_NAMES"][t.headerID]; + end, + ["icon"] = function(t) + return L["HEADER_ICONS"][t.headerID]; + end, + ["description"] = function(t) + return L["HEADER_DESCRIPTIONS"][t.headerID]; + end, + ["nameAsAchievement"] = function(t) + return L["HEADER_NAMES"][t.headerID] or select(2, GetAchievementInfo(t.achievementID)); + end, + ["iconAsAchievement"] = function(t) + return L["HEADER_ICONS"][t.headerID] or select(10, GetAchievementInfo(t.achievementID)); + end, + ["linkAsAchievement"] = function(t) + return GetAchievementLink(t.achievementID); + end, + ["savedAsQuest"] = function(t) + return IsQuestFlaggedCompleted(t.questID); + end, + ["trackableAsQuest"] = app.ReturnTrue, +}; +app.BaseHeader = app.BaseObjectFields(headerFields, "BaseHeader"); +local fields = RawCloneData(headerFields); +fields.name = headerFields.nameAsAchievement; +fields.icon = headerFields.iconAsAchievement; +--fields.link = headerFields.linkAsAchievement; +app.BaseHeaderWithAchievement = app.BaseObjectFields(fields, "BaseHeaderWithAchievement"); +local fields = RawCloneData(headerFields); +fields.saved = headerFields.savedAsQuest; +fields.trackable = headerFields.trackableAsQuest; +app.BaseHeaderWithQuest = app.BaseObjectFields(fields, "BaseHeaderWithQuest"); +local fields = RawCloneData(headerFields); +fields.name = headerFields.nameAsAchievement; +fields.icon = headerFields.iconAsAchievement; +--fields.link = headerFields.linkAsAchievement; +fields.saved = headerFields.savedAsQuest; +fields.trackable = headerFields.trackableAsQuest; +app.BaseHeaderWithAchievementAndQuest = app.BaseObjectFields(fields, "BaseHeaderWithAchievementAndQuest"); +app.CreateNPC = function(id, t) + if t then + -- TEMP: clean MoH tagging from random Vendors + if rawget(t, "itemID") == 137642 then + rawset(t, "itemID", nil); + -- print("ItemID",rawget(t, "itemID"),"used on NPC/Header group... Don't do that!",id); + end + if id < 1 then + if rawget(t, "achID") then + rawset(t, "achievementID", app.FactionID == Enum.FlightPathFaction.Horde and rawget(t, "altAchID") or rawget(t, "achID")); + if rawget(t, "questID") then + return setmetatable(constructor(id, t, "headerID"), app.BaseHeaderWithAchievementAndQuest); + else + return setmetatable(constructor(id, t, "headerID"), app.BaseHeaderWithAchievement); + end + else + if rawget(t, "questID") then + return setmetatable(constructor(id, t, "headerID"), app.BaseHeaderWithQuest); + else + return setmetatable(constructor(id, t, "headerID"), app.BaseHeader); end end - local nonItemNested = {}; - -- merge in any DB data without replacing existing data - for _,data in ipairs(cachedQuests) do - -- only merge into the quest object properties from an object in cache with this questID - if data.questID and data.questID == questObject.questID then - MergeProperties(questObject, data, true); - -- need to exclusively copy cached values for certain fields since normal merge logic will not copy them - -- ref: quest 49675/58703 - if data.u then questObject.u = data.u; end - -- merge in sourced things under this quest object - if data.g then - for _,o in ipairs(data.g) do - -- nest cached non-items - if not o.itemID then - -- if app.DEBUG_PRINT then print("nested-nonItem",o.hash) end - tinsert(nonItemNested, o); - -- cached items need to merge with corresponding API item based on simple itemID - elseif apiItems[o.itemID] then - -- if app.DEBUG_PRINT then print("nested-merged",o.hash) end - MergeProperties(apiItems[o.itemID], o, true); - -- if it is not a WQ or is a 'raid' (world boss) - elseif questObject.isRaid or not questObject.isWorldQuest then - -- otherwise just get nested - -- if app.DEBUG_PRINT then print("nested-item",o.hash) end - tinsert(nonItemNested, o); - end - end - end - -- otherwise if this is a non-quest object flagged with this questID so it should be added under the quest - elseif data.key ~= "questID" then - tinsert(nonItemNested, data); + else + if rawget(t, "achID") then + rawset(t, "achievementID", app.FactionID == Enum.FlightPathFaction.Horde and rawget(t, "altAchID") or rawget(t, "achID")); + if rawget(t, "questID") then + return setmetatable(constructor(id, t, "npcID"), app.BaseNPCWithAchievementAndQuest); + else + return setmetatable(constructor(id, t, "npcID"), app.BaseNPCWithAchievement); + end + else + if rawget(t, "questID") then + return setmetatable(constructor(id, t, "npcID"), app.BaseNPCWithQuest); + else + return setmetatable(constructor(id, t, "npcID"), app.BaseNPC); end end - NestObjects(questObject, nonItemNested, true); end + elseif id > 1 then + return setmetatable(constructor(id, t, "npcID"), app.BaseNPC); + else + return setmetatable(constructor(id, t, "headerID"), app.BaseHeader); + end +end +end)(); - -- Check for provider info - -- TODO: don't think this is necessary? it's trying to pull things listed under a creature when that creature is sourced under a 'Rares' header - -- if questObject.qgs and #questObject.qgs == 1 then - -- _cache = SearchForField("creatureID", questObject.qgs[1]); - -- if _cache then - -- for _,data in ipairs(_cache) do - -- if GetRelativeField(data, "headerID", -16) then -- Rares only! - -- print("merge creature data",data.hash,"=>",questObject.questID) - -- MergeProperties(questObject, data, true); - -- NestObjects(questObject, data.g, true); - -- end - -- end - -- end - -- end - - -- Build out purchases if specified - -- if app.Settings:GetTooltipSetting("WorldQuestsList:Currencies") then - -- FillPurchases(questObject); - -- end +-- Object Lib (as in "World Object") +(function() +local objectFields = { + ["key"] = function(t) + return "objectID"; + end, + ["name"] = function(t) + return app.ObjectNames[t.objectID] or ("Object ID #" .. t.objectID); + end, + ["icon"] = function(t) + return app.ObjectIcons[t.objectID] or "Interface\\Icons\\INV_Misc_Bag_10"; + end, + ["model"] = function(t) + return app.ObjectModels[t.objectID]; + end, - -- Resolve all symbolic links now that the quest contains items - FillSymLinks(questObject, true); + ["nameAsAchievement"] = function(t) + return app.NPCNameFromID[t.npcID] or select(2, GetAchievementInfo(t.achievementID)); + end, + ["iconAsAchievement"] = function(t) + return select(10, GetAchievementInfo(t.achievementID)) or t.iconAsDefault; + end, + ["linkAsAchievement"] = function(t) + return GetAchievementLink(t.achievementID); + end, + ["collectibleAsQuest"] = app.CollectibleAsQuest, + ["collectedAsQuest"] = IsQuestFlaggedCompletedForObject, + ["savedAsQuest"] = function(t) + return IsQuestFlaggedCompleted(t.questID); + end, + ["trackableAsQuest"] = app.ReturnTrue, + ["repeatableAsQuest"] = function(t) + return rawget(t, "isDaily") or rawget(t, "isWeekly") or rawget(t, "isMonthly") or rawget(t, "isYearly") or rawget(t, "isWorldQuest"); + end, + ["altcollectedAsQuest"] = function(t) + if t.altQuests then + for i,questID in ipairs(t.altQuests) do + if IsQuestFlaggedCompleted(questID) then + rawset(t, "altcollected", questID); + return questID; + end + end + end + end, + ["lockedAsQuest"] = app.LockedAsQuest, + ["indicatorIcon"] = function(t) + if app.CurrentVignettes["objectID"][t.objectID] then + return "Category_Secrets"; + end + end, - -- Special logic for Torn Invitation... maybe can clean up sometime - if questObject.g and #questObject.g > 0 then - for _,item in ipairs(questObject.g) do - if item.g then - for k,o in ipairs(item.g) do - if o.itemID == 140495 then -- Torn Invitation - local searchResults = app.SearchForField("questID", 44058); -- Volpin the Elusive - NestObjects(o, searchResults, true); - end - end + -- Generic fields (potentially replaced by specific object types) + ["trackable"] = function(t) + -- only used for generic objects with no other way of being considered trackable + if not t.g then return; end + for _,group in ipairs(t.g) do + if group.objectID and group.trackable then return true; end + end + end, + ["repeatable"] = function(t) + -- only used for generic objects with no other way of being tracked as repeatable + if not t.g then return; end + for _,group in ipairs(t.g) do + if group.objectID and group.repeatable then return true; end + end + -- every contained sub-object is not repeatable, so the repeated object should also be marked as not repeatable + end, + ["saved"] = function(t) + -- only used for generic objects with no other way of being tracked as saved + if not t.g then return; end + local anySaved; + for _,group in ipairs(t.g) do + if group.objectID then + if group.saved then + anySaved = true; + else + return; end end end - BuildGroups(questObject, questObject.g); - else - -- print("set questObject.doUpdate",questObject.questID) - questObject.doUpdate = true; - end - - -- app.DEBUG_PRINT = nil; -end --- Given an Object, will return the indicator (asset name) if this Object should show one based on it being tied to a QuestID -app.GetQuestIndicator = function(t) - if t.questID then - if C_QuestLog_IsOnQuest(t.questID) then - return (C_QuestLog_ReadyForTurnIn(t.questID) and "Interface_Questin") - or "Interface_Questin_grey"; - elseif ATTAccountWideData.OneTimeQuests[t.questID] == false then - return "Interface_Quest_Arrow"; + -- every contained sub-object is already saved, so the repeated object should also be marked as saved + return anySaved; + end, + ["coords"] = function(t) + -- only used for generic objects with no other way of being tracked as saved + if not t.g then return; end + local unsavedCoords = {}; + for _,group in ipairs(t.g) do + -- show collected coords of all sub-objects which are not saved + if group.objectID and group.coords and not group.saved then + app.ArrayAppend(unsavedCoords, group.coords); + end end - end -end - -local fields = RawCloneData(questFields); -fields.collectible = questFields.collectibleAsReputation; -fields.collected = questFields.collectedAsReputation; -app.BaseQuestWithReputation = app.BaseObjectFields(fields, "BaseQuestWithReputation"); -app.CreateQuest = function(id, t) - if t and rawget(t, "maxReputation") then - return setmetatable(constructor(id, t, "questID"), app.BaseQuestWithReputation); - end - return setmetatable(constructor(id, t, "questID"), app.BaseQuest); -end -app.CreateQuestWithFactionData = function(t) - local questData = app.FactionID == Enum.FlightPathFaction.Horde and t.hqd or t.aqd; - for key,value in pairs(questData) do t[key] = value; end - return setmetatable(t, app.BaseQuest); -end --- Causes a group to remain visible if it is replayable, regardless of collection status -app.ShowIfReplayableQuest = function(data) - data.visible = C_QuestLog_IsQuestReplayable(data.questID) or app.CollectedItemVisibilityFilter(data); - return true; -end - --- Quest Objective Lib --- Not used in Retail anymore --- local fields = { --- ["key"] = function(t) --- return "objectiveID"; --- end, --- ["name"] = function(t) --- local objInfo = t.parent.objectiveInfo; --- if objInfo then --- local objective = objInfo[t.objectiveID]; --- if objective then return objective.text; end --- end --- return L["QUEST_OBJECTIVE_INVALID"]; --- end, --- ["icon"] = function(t) --- if t.providers then --- for k,v in ipairs(t.providers) do --- if v[2] > 0 then --- if v[1] == "o" then --- return app.ObjectIcons[v[2]] or "Interface\\Worldmap\\Gear_64Grey"; --- elseif v[1] == "i" then --- return select(5, GetItemInfoInstant(v[2])) or "Interface\\Worldmap\\Gear_64Grey"; --- end --- end --- end --- end --- if t.spellID then return select(3, GetSpellInfo(t.spellID)); end --- return t.parent.icon or "Interface\\Worldmap\\Gear_64Grey"; --- end, --- ["model"] = function(t) --- if t.providers then --- for k,v in ipairs(t.providers) do --- if v[2] > 0 then --- if v[1] == "o" then --- return app.ObjectModels[v[2]]; --- end --- end --- end --- end --- end, --- ["objectiveID"] = function(t) --- return 1; --- end, --- ["questID"] = function(t) --- return t.parent.questID; --- end, --- ["isDaily"] = function(t) --- return t.parent.isDaily; --- end, --- ["isWeekly"] = function(t) --- return t.parent.isWeekly; --- end, --- ["isMonthly"] = function(t) --- return t.parent.isMonthly; --- end, --- ["isYearly"] = function(t) --- return t.parent.isYearly; --- end, --- ["isWorldQuest"] = function(t) --- return t.parent.isWorldQuest; --- end, --- ["repeatable"] = function(t) --- return t.parent.repeatable; --- end, --- ["collectible"] = function(t) --- return t.questID and C_QuestLog_IsOnQuest(t.questID); --- end, --- ["trackable"] = app.ReturnTrue, --- ["collected"] = function(t) --- -- If the parent is collected, return immediately. --- local collected = t.parent.collected; --- if collected then return collected; end + return unsavedCoords; + end, +}; +app.BaseObject = app.BaseObjectFields(objectFields, "BaseObject"); --- -- Check to see if the objective was completed. --- local objInfo = t.parent.objectiveInfo; --- if objInfo then --- local objective = objInfo[t.objectiveID]; --- if objective then return objective.finished and 1; end --- end --- end, --- ["saved"] = function(t) --- -- If the parent is saved, return immediately. --- if t.parent.saved then return true; end +local fields = RawCloneData(objectFields); +fields.icon = objectFields.iconAsAchievement; +--fields.link = objectFields.linkAsAchievement; +app.BaseObjectWithAchievement = app.BaseObjectFields(fields, "BaseObjectWithAchievement"); --- -- Check to see if the objective was completed. --- local objInfo = t.parent.objectiveInfo; --- if objInfo then --- local objective = objInfo[t.objectiveID]; --- if objective then return objective.finished and 1; end --- end --- end, --- }; --- app.BaseQuestObjective = app.BaseObjectFields(fields, "BaseQuestObjective"); --- app.CreateQuestObjective = function(id, t) --- return setmetatable(constructor(id, t, "objectiveID"), app.BaseQuestObjective); --- end ---]] +local fields = RawCloneData(objectFields); +fields.altcollected = objectFields.altcollectedAsQuest; +fields.collectible = objectFields.collectibleAsQuest; +fields.collected = objectFields.collectedAsQuest; +fields.trackable = objectFields.trackableAsQuest; +fields.repeatable = objectFields.repeatableAsQuest; +fields.saved = objectFields.savedAsQuest; +fields.locked = objectFields.lockedAsQuest; +app.BaseObjectWithQuest = app.BaseObjectFields(fields, "BaseObjectWithQuest"); --- Vignette Lib -(function() --- Vignettes copy Quest fields -local fields = RawCloneData(questFields); -local function BuildTextFromNPCIDs(t, npcIDs) - if not npcIDs or #npcIDs == 0 then app.report("Invalid Vignette! "..(t.hash or "[NOHASH]")) end - local retry, name; - local textTbl = {}; - for i,npcID in ipairs(npcIDs) do - name = app.NPCNameFromID[npcID]; - retry = retry or not name or name == RETRIEVING_DATA; - if not retry then - textTbl[i * 2 - 1] = name; - if i > 1 then - textTbl[(i - 1) * 2] = ", "; +local fields = RawCloneData(objectFields); +fields.icon = objectFields.iconAsAchievement; +--fields.link = objectFields.linkAsAchievement; +fields.altcollected = objectFields.altcollectedAsQuest; +fields.collectible = objectFields.collectibleAsQuest; +fields.collected = objectFields.collectedAsQuest; +fields.trackable = objectFields.trackableAsQuest; +fields.repeatable = objectFields.repeatableAsQuest; +fields.saved = objectFields.savedAsQuest; +fields.locked = objectFields.lockedAsQuest; +app.BaseObjectWithAchievementAndQuest = app.BaseObjectFields(fields, "BaseObjectWithAchievementAndQuest"); +app.CreateObject = function(id, t) + if t then + if rawget(t, "achID") then + rawset(t, "achievementID", app.FactionID == Enum.FlightPathFaction.Horde and rawget(t, "altAchID") or rawget(t, "achID")); + if rawget(t, "questID") then + return setmetatable(constructor(id, t, "objectID"), app.BaseObjectWithAchievementAndQuest); + else + return setmetatable(constructor(id, t, "objectID"), app.BaseObjectWithAchievement); end - end - end - if retry then return RETRIEVING_DATA; end - name = table.concat(textTbl); - rawset(t, "name", name); - return name; -end --- Custom Vignette fields -fields.name = function(t) - if t.qgs or t.crs then - return BuildTextFromNPCIDs(t, t.qgs or t.crs); - elseif t.qg or t.creatureID then - return BuildTextFromNPCIDs(t, { t.qg or t.creatureID }); - end - return BuildTextFromNPCIDs(t); -end; -fields.icon = function(t) return "Interface\\Icons\\INV_Misc_Head_Dragon_Black"; end; -fields.isVignette = app.ReturnTrue; -app.BaseVignette = app.BaseObjectFields(fields, "BaseVignette"); -app.CreateVignette = function(id, t) - return setmetatable(constructor(id, t, "questID"), app.BaseVignette); -end -end)(); - --- Will print a warning message and play a warning sound if the given QuestID being completed will prevent being able to complete a breadcrumb --- (as far as ATT is capable of knowing) -app.CheckForBreadcrumbPrevention = function(title, questID) - local nextQuests = app.SearchForField("nextQuests", questID); - if nextQuests then - local warning; - for _,group in pairs(nextQuests) do - if not group.collected and app.RecursiveGroupRequirementsFilter(group) then - app.print(sformat(L["QUEST_PREVENTS_BREADCRUMB_COLLECTION_FORMAT"], title, app:Linkify(questID, app.Colors.ChatLink, "search:questID:"..questID), group.text or RETRIEVING_DATA, app:Linkify(group.questID, app.Colors.Locked, "search:questID:"..group.questID))); - warning = true; + else + if rawget(t, "questID") then + return setmetatable(constructor(id, t, "objectID"), app.BaseObjectWithQuest); end end - if warning then app:PlayRemoveSound(); end end + return setmetatable(constructor(id, t, "objectID"), app.BaseObject); end - -app:RegisterEvent("QUEST_SESSION_JOINED"); end)(); -local function QueryCompletedQuests() - local t = CompletedQuests; - local freshCompletes = C_QuestLog_GetAllCompletedQuestIDs(); - -- print("total completed quests new/previous",#freshCompletes,rawget(t, "_TOTAL") or 0) - local oldReportSetting = app.Settings:GetTooltipSetting("Report:CompletedQuests"); - -- check if Blizzard is being dumb / should we print a summary instead of individual lines - local questDiff = #freshCompletes - (rawget(t, "_TOTAL") or 0); - if app.IsReady then - if oldReportSetting and questDiff > 50 then - print(questDiff,"Quests Completed"); - elseif oldReportSetting and questDiff < -50 then - print(questDiff,"Quests Unflagged"); - end - end - questDiff = math.abs(questDiff); - if questDiff > 50 then - app.Settings:SetTooltipSetting("Report:CompletedQuests", false); - end - local completedKeys = {}; - -- allow individual prints - for _,v in ipairs(freshCompletes) do - t[v] = true; - completedKeys[v] = true; - end - -- check for 'unflagged' questIDs (this seems to basically not impact lag at all... i hope) - for q,_ in pairs(t) do - if not completedKeys[q] and q ~= "_TOTAL" then - t[q] = nil; -- delete the key - t[q] = false; -- trigger the metatable function - end - end - if questDiff > 50 then - app.Settings:SetTooltipSetting("Report:CompletedQuests", oldReportSetting); - end +-- Profession Lib +(function() +app.SkillIDToSpellID = { + [171] = 2259, -- Alchemy + [794] = 158762, -- Arch + [261] = 5149, -- Beast Training + [164] = 2018, -- Blacksmithing + [185] = 2550, -- Cooking + [333] = 7411, -- Enchanting + [202] = 4036, -- Engineering + [356] = 7620, -- Fishing + [129] = 3273, -- First Aid + [182] = 2366, -- Herb Gathering + [773] = 45357, -- Inscription + [755] = 25229, -- Jewelcrafting + --[2720] = 2720, -- Junkyard Tinkering [Does not have a spellID] + [165] = 2108, -- Leatherworking + [186] = 2575, -- Mining + [393] = 8613, -- Skinning + [197] = 3908, -- Tailoring + [960] = 53428, -- Runeforging + [40] = 2842, -- Poisons + [633] = 1809, -- Lockpicking + + -- Specializations + [20219] = 20219, -- Gnomish Engineering + [20222] = 20222, -- Goblin Engineering + [9788] = 9788, -- Armorsmith + [9787] = 9787, -- Weaponsmith + [17041] = 17041, -- Master Axesmith + [17040] = 17040, -- Master Hammersmith + [17039] = 17039, -- Master Swordsmith + [10656] = 10656, -- Dragonscale Leatherworking + [10658] = 10658, -- Elemental Leatherworking + [10660] = 10660, -- Tribal Leatherworking + [26801] = 26801, -- Shadoweave Tailoring + [26797] = 26797, -- Spellfire Tailoring + [26798] = 26798, -- Mooncloth Tailoring + [125589] = 125589, -- Way of the Brew + [124694] = 124694, -- Way of the Grill + [125588] = 125588, -- Way of the Oven + [125586] = 125586, -- Way of the Pot + [125587] = 125587, -- Way of the Steamer + [125584] = 125584, -- Way of the Wok +}; +app.SpellIDToSkillID = {}; +for skillID,spellID in pairs(app.SkillIDToSpellID) do + app.SpellIDToSkillID[spellID] = skillID; end -local function RefreshQuestCompletionState(questID) - -- print("QuestRefresh",questID) - if questID then - CompletedQuests[questID] = true; - else - QueryCompletedQuests(); - end +app.SpecializationSpellIDs = setmetatable({ + [20219] = 4036, -- Gnomish Engineering + [20222] = 4036, -- Goblin Engineering + [9788] = 2018, -- Armorsmith + [9787] = 2018, -- Weaponsmith + [17041] = 2018, -- Master Axesmith + [17040] = 2018, -- Master Hammersmith + [17039] = 2018, -- Master Swordsmith + [10656] = 2108, -- Dragonscale Leatherworking + [10658] = 2108, -- Elemental Leatherworking + [10660] = 2108, -- Tribal Leatherworking + [26801] = 3908, -- Shadoweave Tailoring + [26797] = 3908, -- Spellfire Tailoring + [26798] = 3908, -- Mooncloth Tailoring + [125589] = 2550,-- Way of the Brew + [124694] = 2550,-- Way of the Grill + [125588] = 2550,-- Way of the Oven + [125586] = 2550,-- Way of the Pot + [125587] = 2550,-- Way of the Steamer + [125584] = 2550,-- Way of the Wok +}, {__index = function(t,k) return k; end}) - local completedQuestHelper = app.QuestCompletionHelper; - for questID,_ in pairs(DirtyQuests) do - completedQuestHelper(tonumber(questID)); - end - -- soft update if any quests were even completed to ensure visible changes occur - if DirtyQuests.DIRTY then - app:UpdateWindows(); - end - -- re-register the criteria update event - app:RegisterEvent("CRITERIA_UPDATE"); - wipe(DirtyQuests); - wipe(npcQuestsCache); +local fields = { + ["key"] = function(t) + return "professionID"; + end, + --[[ + ["name"] = function(t) + if app.GetSpecializationBaseTradeSkill(t.professionID) then return select(1, GetSpellInfo(t.professionID)); end + if t.professionID == 129 then return select(1, GetSpellInfo(t.spellID)); end + return C_TradeSkillUI.GetTradeSkillDisplayName(t.professionID); + end, + ["icon"] = function(t) + if app.GetSpecializationBaseTradeSkill(t.professionID) then return select(3, GetSpellInfo(t.professionID)); end + if t.professionID == 129 then return select(3, GetSpellInfo(t.spellID)); end + return C_TradeSkillUI.GetTradeSkillTexture(t.professionID); + end, + ]]-- + ["name"] = function(t) + return t.spellID ~= 2366 and select(1, GetSpellInfo(t.spellID)) or C_TradeSkillUI.GetTradeSkillDisplayName(t.professionID); + end, + ["icon"] = function(t) + return select(3, GetSpellInfo(t.spellID)) or C_TradeSkillUI.GetTradeSkillTexture(t.professionID); + end, + ["spellID"] = function(t) + return app.SkillIDToSpellID[t.professionID]; + end, + ["skillID"] = function(t) + return t.professionID; + end, + ["requireSkill"] = function(t) + return t.professionID; + end, + --[[ + ["sym"] = function(t) + return {{"selectprofession", t.professionID}, + {"not","headerID",-38}}; -- Ignore the Main Professions header that will get pulled in + end, + --]]-- +}; +app.BaseProfession = app.BaseObjectFields(fields, "BaseProfession"); +app.CreateProfession = function(id, t) + return setmetatable(constructor(id, t, "professionID"), app.BaseProfession); end -app.RefreshQuestInfo = function(questID) - -- print("RefreshQuestInfo",questID) - -- unregister criteria update until the quest refresh actually completes - app:UnregisterEvent("CRITERIA_UPDATE"); - if questID then - RefreshQuestCompletionState(questID); - else - AfterCombatOrDelayedCallback(RefreshQuestCompletionState, 1); +end)(); + +-- PVP Ranks +(function() +local fields = { + ["key"] = function(t) + return "pvpRankID"; + end, + ["text"] = function(t) + return _G["PVP_RANK_" .. (t.pvpRankID + 4) .. "_" .. (t.inverseR or 0)]; + end, + ["icon"] = function(t) + return format("%s%02d","Interface\\PvPRankBadges\\PvPRank", t.pvpRankID); + end, + ["title"] = function(t) + return RANK .. " " .. t.pvpRankID .. DESCRIPTION_SEPARATOR .. _G["PVP_RANK_" .. (t.pvpRankID + 4) .. "_" .. ((t.inverseR == 1 and 0 or 1))] .. " (" .. (t.r == Enum.FlightPathFaction.Alliance and FACTION_HORDE or FACTION_ALLIANCE) .. ")"; + end, + ["description"] = function(t) + return "There are a total of 14 ranks for both factions. Each rank requires a minimum amount of Rating Points to be calculated every week, then calculated in comparison to other players on your server.\n\nEach rank grants access to different rewards, from PvP consumables to Epic Mounts that do not require Epic Riding Skill and Epic pieces of gear at the highest ranks. Each rank is also applied to your character as a Title."; + end, + ["r"] = function(t) + return t.parent.r or app.FactionID; + end, + ["inverseR"] = function(t) + return t.r == Enum.FlightPathFaction.Alliance and 1 or 0; + end, + ["lifetimeRank"] = function(t) + return select(3, GetPVPLifetimeStats()); + end, + ["collectible"] = app.ReturnFalse, + ["collected"] = function(t) + return t.lifetimeRank >= t.pvpRankID; + end, + ["OnTooltip"] = function(t) + GameTooltip:AddDoubleLine("Your lifetime highest rank: ", _G["PVP_RANK_" .. (t.lifetimeRank) .. "_" .. (app.FactionID == 2 and 1 or 0)], 1, 1, 1, 1, 1, 1); end +}; +app.BasePVPRank = app.BaseObjectFields(fields, "BasePVPRank"); +app.CreatePVPRank = function(id, t) + return setmetatable(constructor(id, t, "pvpRankID"), app.BasePVPRank); end +end)(); -- Race Lib (function() @@ -13339,14 +13341,15 @@ function app.IsBoP(group) end function app.FilterGroupsByLevel(group) -- after 9.0, transition to a req lvl range, either min, or min + max - if group.lvl then + local lvl = group.lvl; + if lvl then local minlvl; local maxlvl; - if type(group.lvl) == "table" then - minlvl = group.lvl[1]; - maxlvl = group.lvl[2]; + if type(lvl) == "table" then + minlvl = lvl[1]; + maxlvl = lvl[2]; else - minlvl = group.lvl; + minlvl = lvl; end if maxlvl then @@ -13481,8 +13484,9 @@ function app.FilterItemClass_RequiredSkill(item) end end function app.FilterItemClass_RequireFaction(item) - if item.minReputation and app.IsFactionExclusive(item.minReputation[1]) then - if item.minReputation[2] > (select(6, GetFactionInfoByID(item.minReputation[1])) or 0) then + local minReputation = item.minReputation; + if minReputation and app.IsFactionExclusive(minReputation[1]) then + if minReputation[2] > (select(6, GetFactionInfoByID(minReputation[1])) or 0) then --print("Filtering Out", item.key, item[item.key], item.text, item.minReputation[1], app.CreateFaction(item.minReputation[1]).text); return false; else @@ -13493,9 +13497,10 @@ function app.FilterItemClass_RequireFaction(item) end end function app.FilterItemClass_CustomCollect(item) - if item.customCollect then + local customCollect = item.customCollect; + if customCollect then local customCollects = app.CurrentCharacter.CustomCollects; - for _,c in ipairs(item.customCollect) do + for _,c in ipairs(customCollect) do if not customCollects[c] then return false; end @@ -13796,8 +13801,9 @@ end app.RecursiveGroupRequirementsFilter = function(group) -- if not app.VerifyRecursion(group) then return; end if app.GroupRequirementsFilter(group) and app.GroupFilter(group) then - if group.sourceParent or group.parent then - return app.RecursiveGroupRequirementsFilter(group.sourceParent or group.parent) + local filterParent = group.sourceParent or group.parent; + if filterParent then + return app.RecursiveGroupRequirementsFilter(filterParent) end return true; -- elseif app.DEBUG_PRINT then @@ -13938,20 +13944,27 @@ UpdateGroup = function(parent, group, window) if valid then -- if app.DEBUG_PRINT then print("UpdateGroup.GroupRequirementsFilter",group.key,group.key and group[group.key],group.__type) end -- if app.DEBUG_PRINT then print("UpdateGroup.GroupFilter",group.key,group.key and group[group.key],group.__type) end - -- Set total/progress for this object using its cost information if any - group.total = (group.costTotal or 0) + (group.customTotal or 0); - group.progress = group.total > 0 and ((group.costProgress or 0) + (group.customProgress or 0)) or 0; + -- Set total/progress for this object using its cost/custom information if any + local costTotal = group.costTotal or 0; + local costProgress = costTotal > 0 and group.costProgress or 0; + local customTotal = group.customTotal or 0; + local customProgress = customTotal > 0 and group.customProgress or 0; + local total, progress = costTotal + customTotal, costProgress + customProgress; -- if app.DEBUG_PRINT then print("UpdateGroup.Initial",group.key,group.key and group[group.key],group.progress,group.total,group.__type) end -- If this item is collectible, then mark it as such. if group.collectible then -- An item is a special case where it may have both an appearance and a set of items - group.progress = group.progress + (group.collected and 1 or 0); - group.total = group.total + 1; + progress = progress + (group.collected and 1 or 0); + total = total + 1; -- if app.DEBUG_PRINT then print("UpdateGroup.Collectible",group.progress,group.total,group.__type) end end + -- Set the total/progress on the group + group.progress = progress; + group.total = total; + -- Check if this is a group if group.g then -- if app.DEBUG_PRINT then print("UpdateGroup.g",group.progress,group.total,group.__type) end @@ -14033,12 +14046,12 @@ app.SetGroupVisibility = SetGroupVisibility; local function TopLevelUpdateGroup(group, window) group.total = 0; group.progress = 0; - if app.ItemBindFilter ~= app.NoFilter and app.ItemBindFilter(group) then - local oldItemBindFilter = app.ItemBindFilter; + local ItemBindFilter = app.ItemBindFilter; + if ItemBindFilter ~= app.NoFilter and ItemBindFilter(group) then app.ItemBindFilter = app.NoFilter; UpdateGroups(group, group.g, window); -- reapply the previous BoE filter - app.ItemBindFilter = oldItemBindFilter; + app.ItemBindFilter = ItemBindFilter; else UpdateGroups(group, group.g, window); end @@ -14310,35 +14323,11 @@ function app.GetNumberOfItemsUntilNextPercentage(progress, total) end end end --- A set of quests which indicate a needed refresh to the Custom Collect status of the character -app.CustomCollectQuests = { - [56775] = 1, -- New Player Experience Starting Quest - [59926] = 1, -- New Player Experience Starting Quest - [58911] = 1, -- New Player Experience Ending Quest - [60359] = 1, -- New Player Experience Ending Quest - [62713] = 1, -- Shadowlands - SL_SKIP (Threads of Fate) - [65076] = 1, -- Shadowlands - Covenant - Kyrian - [65077] = 1, -- Shadowlands - Covenant - Venthyr - [65078] = 1, -- Shadowlands - Covenant - Night Fae - [65079] = 1, -- Shadowlands - Covenant - Necrolord -}; -function app.QuestCompletionHelper(questID) - if questID then - -- Only increase progress for Quests as Collectible users. - if app.CollectibleQuests or app.CollectibleQuestsLocked then - -- Search ATT for the related quests. - local searchResults = SearchForField("questID", questID); - UpdateSearchResults(searchResults, true); - end - -- Certain quests being completed should trigger a refresh of the Custom Collect status of the character (i.e. Covenant Switches, Threads of Fate, etc.) - if app.CustomCollectQuests[questID] then - Callback(app.RefreshCustomCollectibility); - end - end -end + +(function() -- receives a key and a function which returns the value to be set for -- that key based on the current value and current character -app.SetCustomCollectibility = function(key, func) +local function SetCustomCollectibility(key, func) -- print("SetCustomCollectibility",key); local result = func(); if result ~= nil then @@ -14347,7 +14336,7 @@ app.SetCustomCollectibility = function(key, func) else -- failed attempt to set the CC, try next frame -- print("Failed") - Callback(app.SetCustomCollectibility, key, func); + Callback(SetCustomCollectibility, key, func); end end -- determines whether an object may be considered collectible for the current character based on the 'customCollect' value(s) @@ -14374,7 +14363,7 @@ app.RefreshCustomCollectibility = function() -- do one-time per character custom visibility check(s) -- Exile's Reach (New Player Experience) - app.SetCustomCollectibility("NPE", function() + SetCustomCollectibility("NPE", function() -- settings override if app.Settings:Get("CC:NPE") then return true; end -- needs mapID to check this @@ -14398,7 +14387,7 @@ app.RefreshCustomCollectibility = function() return false; end); -- Shadowlands Skip - app.SetCustomCollectibility("SL_SKIP", function() + SetCustomCollectibility("SL_SKIP", function() -- settings override if app.Settings:Get("CC:SL_SKIP") then return true; end -- check if quest #62713 is completed. appears to be a HQT concerning whether the character has chosen to skip the SL Storyline @@ -14409,30 +14398,32 @@ app.RefreshCustomCollectibility = function() -- print("Current Covenant",SLCovenantId); -- Show all Covenants if not yet selected -- Shadowlands Covenant: Kyrian - app.SetCustomCollectibility("SL_COV_KYR", function() + SetCustomCollectibility("SL_COV_KYR", function() -- settings override if app.Settings:Get("CC:SL_COV_KYR") then return true; end return SLCovenantId == 1 or SLCovenantId == 0; end); -- Shadowlands Covenant: Venthyr - app.SetCustomCollectibility("SL_COV_VEN", function() + SetCustomCollectibility("SL_COV_VEN", function() -- settings override if app.Settings:Get("CC:SL_COV_VEN") then return true; end return SLCovenantId == 2 or SLCovenantId == 0; end); -- Shadowlands Covenant: Night Fae - app.SetCustomCollectibility("SL_COV_NFA", function() + SetCustomCollectibility("SL_COV_NFA", function() -- settings override if app.Settings:Get("CC:SL_COV_NFA") then return true; end return SLCovenantId == 3 or SLCovenantId == 0; end); -- Shadowlands Covenant: Necrolord - app.SetCustomCollectibility("SL_COV_NEC", function() + SetCustomCollectibility("SL_COV_NEC", function() -- settings override if app.Settings:Get("CC:SL_COV_NEC") then return true; end return SLCovenantId == 4 or SLCovenantId == 0; end); end +end)(); + local function MinimapButtonOnClick(self, button) if button == "RightButton" then app.Settings:Open(); @@ -14598,7 +14589,6 @@ local function CreateMinimapButton() end app.CreateMinimapButton = CreateMinimapButton; - -- Panel Class Library (function() -- Shared Panel Functions @@ -15383,7 +15373,7 @@ local function StartMovingOrSizing(self, fromChild) end end end -app.StoreWindowPosition = function(self) +local StoreWindowPosition = function(self) if AllTheThingsProfiles then if self.isLocked or self.lockPersistable then local key = app.Settings:GetProfile(); @@ -16051,7 +16041,7 @@ RowOnEnter = function (self) GameTooltip:AddDoubleLine(L["CRITERIA_FOR"], GetAchievementLink(reference.achievementID)); end if app.Settings:GetTooltipSetting("Progress") then - local right = (app.Settings:GetTooltipSetting("ShowIconOnly") and GetProgressTextForRow or GetProgressTextForTooltip)(reference); + local right = GetProgressTextForTooltip(reference, app.Settings:GetTooltipSetting("ShowIconOnly")); if right and right ~= "" and right ~= "---" then GameTooltipTextRight1:SetText(right); GameTooltipTextRight1:Show(); @@ -16617,7 +16607,7 @@ local function ProcessGroup(data, object) if sortInfo then app.SortGroup(object, sortInfo[1], sortInfo[2], sortInfo[3], sortInfo[4]); end - for j, group in ipairs(object.g) do + for _,group in ipairs(object.g) do ProcessGroup(data, group); end end @@ -16702,13 +16692,13 @@ local backdrop = { insets = { left = 4, right = 4, top = 4, bottom = 4 } }; -- allows resetting a given ATT window -function app:ResetWindow(suffix) +local function ResetWindow(suffix) app.Windows[suffix] = nil; app.print("Reset Window",suffix); end function app:GetWindow(suffix, parent, onUpdate) if app.GetCustomWindowParam(suffix, "reset") then - app:ResetWindow(suffix); + ResetWindow(suffix); end local window = app.Windows[suffix]; if not window then @@ -16722,7 +16712,7 @@ function app:GetWindow(suffix, parent, onUpdate) window.BaseUpdate = UpdateWindow; window.Update = onUpdate or app:CustomWindowUpdate(suffix) or UpdateWindow; window.SetVisible = SetVisible; - window.StorePosition = app.StoreWindowPosition; + window.StorePosition = StoreWindowPosition; window:SetScript("OnMouseWheel", OnScrollBarMouseWheel); window:SetScript("OnMouseDown", StartMovingOrSizing); @@ -16877,7 +16867,6 @@ app.DynamicCategory_Nested = function(self) -- make sure these things are cached so they can be updated when collected app.CacheFields(self); end - function app:GetDataCache() local dynamicSetting = app.Settings:Get("Dynamic:Style") or 0; -- copy the function which will handle the desired Dynamic style for this session @@ -17852,7 +17841,7 @@ app._RefreshData = function() app:GetDataCache(); -- Refresh all Quests without callback - RefreshQuestCompletionState(); + app.RefreshQuestInfo(); -- Reapply custom collects app.RefreshCustomCollectibility(); @@ -20179,7 +20168,7 @@ customWindowUpdates["Sync"] = function(self) if self:IsVisible() then if not self.initialized then self.initialized = true; - + local function OnRightButtonDeleteCharacter(row, button) if button == "RightButton" then app:ShowPopupDialog("CHARACTER DATA: " .. (row.ref.text or RETRIEVING_DATA) .. "\n \nAre you sure you want to delete this?", @@ -20232,11 +20221,11 @@ customWindowUpdates["Sync"] = function(self) GameTooltip:AddLine("Right Click to Delete this Linked Account", 1, 0.8, 0.8); end end - + local syncHeader; syncHeader = { ['text'] = "Account Management", - ['icon'] = "Interface\\Icons\\Achievement_Dungeon_HEROIC_GloryoftheRaider", + ['icon'] = "Interface\\Icons\\Achievement_Dungeon_HEROIC_GloryoftheRaider", ["description"] = "This list shows you all of the functionality related to syncing account data.", ['visible'] = true, ['expanded'] = true, @@ -20277,7 +20266,7 @@ customWindowUpdates["Sync"] = function(self) })); end end - + if #data.g < 1 then table.insert(data.g, { ['text'] = "No characters found.", @@ -20289,11 +20278,11 @@ customWindowUpdates["Sync"] = function(self) BuildGroups(data, data.g); return app.AlwaysShowUpdate(data); end, - ['visible'] = true, + ['visible'] = true, ['expanded'] = true, ['g'] = {}, }, - + -- Linked Accounts Section { ['text'] = "Linked Accounts", @@ -20307,7 +20296,7 @@ customWindowUpdates["Sync"] = function(self) charactersByName[character.name] = character; end end - + for playerName,allowed in pairs(AllTheThingsAD.LinkedAccounts) do local character = charactersByName[playerName]; if character then @@ -20342,7 +20331,7 @@ customWindowUpdates["Sync"] = function(self) }); end end - + if #data.g < 1 then table.insert(data.g, { ['text'] = "No linked accounts found.", @@ -20353,7 +20342,7 @@ customWindowUpdates["Sync"] = function(self) BuildGroups(data, data.g); return app.AlwaysShowUpdate(data); end, - ['visible'] = true, + ['visible'] = true, ['expanded'] = true, ['g'] = {}, }, @@ -20362,14 +20351,14 @@ customWindowUpdates["Sync"] = function(self) return b.text > a.text; end, }; - + self.Reset = function() self.data = syncHeader; self:Update(true); end self:Reset(); end - + -- Update the groups without forcing Debug Mode. if self.data.OnUpdate then self.data.OnUpdate(self.data, self); end BuildGroups(self.data, self.data.g); @@ -22476,7 +22465,7 @@ app.InitDataCoroutine = function() end -- Mark all previously completed quests. - QueryCompletedQuests(); + app.QueryCompletedQuests(); -- Current character collections shouldn't use '2' ever... so clear any 'inaccurate' data local currentQuestsCache = currentCharacter.Quests; From bed9c2b1891ff46517d49e243d02df37ffa88ad0 Mon Sep 17 00:00:00 2001 From: ImUnicke Date: Wed, 25 May 2022 21:03:55 -0600 Subject: [PATCH 5/7] More clean up --- AllTheThings.lua | 39 +++++++++++++++++---------------------- src/base.lua | 6 +++--- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/AllTheThings.lua b/AllTheThings.lua index 87cd6f8a8be..720882077d8 100644 --- a/AllTheThings.lua +++ b/AllTheThings.lua @@ -6,7 +6,6 @@ local app = select(2, ...); local L = app.L; -local auctionFrame = CreateFrame("Frame"); local window; -- Assign the FactionID. @@ -213,9 +212,6 @@ end local Callback = app.Callback; -- Triggers a timer callback method to run after the provided number of seconds with the provided params; the method can only be set to run once per delay local function DelayedCallback(method, delaySec, ...) - if not app.__callbacks then - app.__callbacks = {}; - end if not app.__callbacks[method] then app.__callbacks[method] = ... and {...} or true; -- print("DelayedCallback:",method, ...) @@ -238,12 +234,6 @@ end -- Triggers a timer callback method to run on the next game frame or following combat if in combat currently with the provided params; the method can only be set to run once per frame local function AfterCombatCallback(method, ...) if not InCombatLockdown() then Callback(method, ...); return; end - if not app.__callbacks then - app.__callbacks = {}; - end - if not app.__combatcallbacks then - app.__combatcallbacks = {}; - end if not app.__callbacks[method] then app.__callbacks[method] = ... and {...} or true; -- If in combat, register to trigger on leave combat @@ -2906,17 +2896,6 @@ local function ExpandGroupsRecursively(group, expanded, manual) end end end --- Returns true if any subgroup of the provided group is currently expanded, otherwise nil -local function HasExpandedSubgroup(group) - if group and group.g then - for _,subgroup in ipairs(group.g) do - -- dont need recursion since a group has to be expanded for a subgroup to be visible within it - if subgroup.expanded then - return true; - end - end - end -end local ResolveSymbolicLink; -- Fills & returns a group with its symlink references, along with all sub-groups recursively if specified @@ -15419,6 +15398,17 @@ local function AddQuestInfoToTooltip(tooltip, quests) end end end +-- Returns true if any subgroup of the provided group is currently expanded, otherwise nil +local function HasExpandedSubgroup(group) + if group and group.g then + for _,subgroup in ipairs(group.g) do + -- dont need recursion since a group has to be expanded for a subgroup to be visible within it + if subgroup.expanded then + return true; + end + end + end +end local RowOnEnter, RowOnLeave; local function RowOnClick(self, button) local reference = self.ref; @@ -18069,7 +18059,7 @@ customWindowUpdates["AuctionData"] = function(self) local cooldown, now = GetDataMember("AuctionScanCooldownTime", 0), time(); if cooldown - now < 0 then SetDataMember("AuctionScanCooldownTime", time() + 900); - auctionFrame:RegisterEvent("REPLICATE_ITEM_LIST_UPDATE"); + app.AuctionFrame:RegisterEvent("REPLICATE_ITEM_LIST_UPDATE"); C_AuctionHouse_ReplicateItems(); else app.print(": Throttled scan! Please wait " .. RoundNumber(cooldown - now, 0) .. " before running another. Loading last save instead..."); @@ -21764,6 +21754,10 @@ hooksecurefunc("EmbeddedItemTooltip_SetItemByQuestReward", function(self, ...) end); --hooksecurefunc("BattlePetTooltipTemplate_SetBattlePet", AttachBattlePetTooltip); -- Not ready yet. +-- Auction House Lib +(function() +local auctionFrame = CreateFrame("Frame"); +app.AuctionFrame = auctionFrame; app.ProcessAuctionData = function() -- If we have no auction data, then simply return now. if not AllTheThingsAuctionData then return end; @@ -22078,6 +22072,7 @@ app.OpenAuctionModule = function(self) end); end end +end)(); -- Creates the data structures and initial 'Default' profiles for ATT app.SetupProfiles = function() diff --git a/src/base.lua b/src/base.lua index 0088d8ebb5f..10b55337439 100644 --- a/src/base.lua +++ b/src/base.lua @@ -87,11 +87,11 @@ app.SetScript = function(self, ...) _:SetScript(scriptName, nil); end end +-- Setup the callback tables since they are heavily used +app.__callbacks = {}; +app.__combatcallbacks = {}; -- Triggers a timer callback method to run on the next game frame with the provided params; the method can only be set to run once per frame local function Callback(method, ...) - if not app.__callbacks then - app.__callbacks = {}; - end if not app.__callbacks[method] then app.__callbacks[method] = ... and {...} or true; -- print("Callback:",method, ...) From fe6c540e85b2d64bfd392d49b6b59d02048ad202 Mon Sep 17 00:00:00 2001 From: ImUnicke Date: Thu, 26 May 2022 01:23:30 -0600 Subject: [PATCH 6/7] More various clean up adjustments Fixed bug with ArrayAppend when passing in a nil --- AllTheThings.lua | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/AllTheThings.lua b/AllTheThings.lua index 720882077d8..bda4a4a7bcd 100644 --- a/AllTheThings.lua +++ b/AllTheThings.lua @@ -291,14 +291,14 @@ local contains = function(arr, value) end end local containsAny = function(arr, otherArr) - for i, v in ipairs(arr) do - for j, w in ipairs(otherArr) do + for _,v in ipairs(arr) do + for _,w in ipairs(otherArr) do if v == w then return true; end end end end local containsValue = function(dict, value) - for key,value2 in pairs(dict) do + for _,value2 in pairs(dict) do if value2 == value then return true; end end end @@ -432,7 +432,6 @@ app.SortDefaults = { -- Sorts objects first by whether they do not have sub-groups [.g] defined ["Hierarchy"] = defaultHierarchyComparison, }; --- local defaultStringComparison local function Sort(t, compare, nested) if t then if not compare then compare = defaultComparison; end @@ -447,7 +446,6 @@ end -- Safely-sorts a table using a provided comparison function and whether to propogate to nested groups -- Wrapping in a pcall since sometimes the sorted values are able to change while being within the sort method. This causes the 'invalid sort order function' error app.Sort = function(t, compare, nested) - if app.DisableSort then return; end pcall(Sort, t, compare, nested); end local sortByNameSafely = function(a, b) @@ -571,12 +569,14 @@ end app.ArrayAppend = function(a1, ...) if ... then a1 = a1 or {}; - local i, select, a = #a1 + 1, select; - for n=1,select("#", ...) do + local i, select, arrs, a = #a1 + 1, select, select("#", ...); + for n=1,arrs do a = select(n, ...); - for ai=1,#a do - a1[i] = a[ai]; - i = i + 1; + if a then + for ai=1,#a do + a1[i] = a[ai]; + i = i + 1; + end end end end @@ -4835,14 +4835,11 @@ local function FillGroupsRecursive(group, depth) end local groups; - -- Determine Cost groups - groups = app.ArrayAppend(groups, DeterminePurchaseGroups(group)); - - -- Determine Crafted groups - groups = app.ArrayAppend(groups, DetermineCraftedGroups(group)); - - -- Determine Symlink groups - groups = app.ArrayAppend(groups, DetermineSymlinkGroups(group)); + -- Determine Cost/Crafted/Symlink groups + groups = app.ArrayAppend(groups, + DeterminePurchaseGroups(group), + DetermineCraftedGroups(group), + DetermineSymlinkGroups(group)); -- Prevent repeated nesting of anything dynamically nested if groups then @@ -18608,9 +18605,7 @@ customWindowUpdates["CurrentInstance"] = function(self, force, got) or self.data.achID and app.BaseMapWithAchievementID or app.BaseMap); -- Fill up the groups that need to be filled! - app.DisableSort = 1; app.FillGroups(self.data); - app.DisableSort = nil; -- sort top level by name if not in an instance if not GetRelativeValue(self.data, "instanceID") then From beb63bacb0354ac8837f6599db78f09b5e8f2cc4 Mon Sep 17 00:00:00 2001 From: ImUnicke Date: Thu, 26 May 2022 01:39:12 -0600 Subject: [PATCH 7/7] Fixed non-callback quest refresh function --- AllTheThings.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AllTheThings.lua b/AllTheThings.lua index bda4a4a7bcd..e81d5b0cf19 100644 --- a/AllTheThings.lua +++ b/AllTheThings.lua @@ -17828,7 +17828,7 @@ app._RefreshData = function() app:GetDataCache(); -- Refresh all Quests without callback - app.RefreshQuestInfo(); + app.QueryCompletedQuests(); -- Reapply custom collects app.RefreshCustomCollectibility();