diff --git a/AllTheThings.lua b/AllTheThings.lua index 63e37ba4781..e81d5b0cf19 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. @@ -30,7 +29,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"]; @@ -214,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, ...) @@ -239,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 @@ -302,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 @@ -443,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 @@ -458,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) @@ -582,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 @@ -1517,6 +1506,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 +1521,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 +1549,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 @@ -2572,124 +2584,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 = {}; @@ -3002,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 @@ -4780,7 +4663,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 +4744,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 @@ -4951,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 @@ -5390,9 +5271,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 +5312,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 +5374,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 +5415,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 +5426,7 @@ function app:ReceiveSyncSummaryResponse(sender, summary) end end end - + if not rawMsg then rawMsg = rawData; else @@ -5553,7 +5434,7 @@ function app:ReceiveSyncSummaryResponse(sender, summary) end end end - + if rawMsg then -- Send Addon Message Back local length = string.len(rawMsg); @@ -5569,7 +5450,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 +6782,7 @@ local function RefreshCollections() end end coroutine.yield(); - + app:RecalculateAccountWideData(); -- Refresh Sources from Cache if tracking Transmog @@ -7632,7 +7513,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; @@ -7640,11 +7524,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; @@ -7654,5166 +7533,5261 @@ 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)(); + +-- Quest Lib +-- Quests first because a lot of other Thing libs use Quest logic +(function() +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 - -- Make sure this thing can actually be collectible via hierarchy - if GetRelativeValue(t, "altcollected") then - -- app.PrintDebug("CollectedAsCost:altcollected",t.hash) - return; + + 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 - -- 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}); +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 - -- app.PrintDebug("CollectedAsCost:true",t.hash) - t.collectedAsCost = nil; - return true; end -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); - --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 +-- 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 -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 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 +-- 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 - 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); + 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 - local statistic = GetStatistic(t.achievementID); - if statistic and statistic ~= '0' then - return statistic; - end - end, - ["sortProgress"] = function(t) - if t.collected then - return 1; + -- if an alt-quest is completed, then this quest is locked + if t.altcollected then + rawset(t, "locked", t.altcollected); + return true; 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); + -- 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 - 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); + 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 categoryFields = { +local questFields = { ["key"] = function(t) - return "achievementCategoryID"; + return "questID"; end, ["name"] = function(t) - return GetCategoryInfo(t.achievementCategoryID); + return QuestTitleFromID[t.questID]; end, - ["icon"] = function(t) - return app.asset("Category_Achievements"); - end, - ["parentCategoryID"] = function(t) - return select(2, GetCategoryInfo(t.achievementCategoryID)) or -1; + ["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, -}; -app.BaseAchievementCategory = app.BaseObjectFields(categoryFields, "BaseAchievementCategory"); -app.CreateAchievementCategory = function(id, t) - return setmetatable(constructor(id, t, "achievementCategoryID"), app.BaseAchievementCategory); -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); - end -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; + ["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, - ["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); + ["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 - return L["WRONG_FACTION"]; + 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, - ["description"] = function(t) - if t.encounterID then - return select(2, EJ_GetEncounterInfo(t.encounterID)); + ["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, ["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; + 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, + ["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 - 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)); + -- If Collectible by being a Quest + if app.CollectibleQuests or app.CollectibleQuestsLocked then + return app.CollectibleAsQuest(t); 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; - 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 - return displayInfos; end + -- Finally, check if the quest is otherwise considered 'collected' by normal logic + return IsQuestFlaggedCompletedForObject(t); end, - ["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)); + ["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, - ["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)); + ["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, - ["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"); - end, - ["r"] = function(t) - return GetParentAchievementInfo(t, "r"); - 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 -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; + -- 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: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 == 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; + -- 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 - 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 + end + end - HarvestedAchievementDatabase[achievementID] = info; - AllTheThingsHarvestItems = HarvestedAchievementDatabase; - setmetatable(t, app.BaseAchievement); - rawset(t, "collected", true); - return link; + -- 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 + end + questObject.missingCurr = 0; + NestObject(questObject, item, true); + end 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; + -- 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 + 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); + end + end + NestObjects(questObject, nonItemNested, true); + 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 + + -- 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 + end + end + BuildGroups(questObject, questObject.g); + else + -- print("set questObject.doUpdate",questObject.questID) + questObject.doUpdate = true; + end + + -- app.DEBUG_PRINT = nil; end -app.BaseAchievementHarvester = app.BaseObjectFields(harvesterFields, "BaseAchievementHarvester"); -app.CreateAchievementHarvester = function(id, t) - return setmetatable(constructor(id, t, "achievementID"), app.BaseAchievementHarvester); +-- 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 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; +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 +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 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 +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 - for achievementID=maxid,1,-1 do - CheckAchievementCollectionStatus(achievementID); + -- 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:RegisterEvent("ACHIEVEMENT_EARNED"); -app.events.ACHIEVEMENT_EARNED = CheckAchievementCollectionStatus; -end)(); +local function RefreshQuestCompletionState(questID) + -- print("QuestRefresh",questID) + if questID then + CompletedQuests[questID] = true; + else + QueryCompletedQuests(); + end --- Artifact Lib + for questID,_ in pairs(DirtyQuests) do + QuestCompletionHelper(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); +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 +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 + +-- -- 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 + +-- -- 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 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 -}; +-- 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 +-- 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)(); + +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); + --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 "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; + return "achievementID"; 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; + ["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) - 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)); + return t.link or t.name; end, - ["appearanceText"] = function(t) - return "|cffe6cc80" .. (t.artifactinfo[3] or "???") .. "|r"; + ["link"] = function(t) + return cache.GetCachedField(t, "link", CacheInfo); end, - ["description"] = function(t) - return t.artifactinfo[6] or L["ARTIFACT_INTRO_REWARD"]; + ["name"] = function(t) + return cache.GetCachedField(t, "name", CacheInfo); end, - ["atlas"] = function(t) - return "Forge-ColorSwatchBorder"; + ["icon"] = function(t) + return cache.GetCachedField(t, "icon", CacheInfo); end, - ["atlas-background"] = function(t) - return "Forge-ColorSwatchBackground"; + ["collectible"] = function(t) + return app.CollectibleAchievements; end, - ["atlas-border"] = function(t) - return "Forge-ColorSwatch"; + ["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, - ["atlas-color"] = function(t) - return { t.artifactinfo[9], t.artifactinfo[10], t.artifactinfo[11], 1.0 }; + ["parentCategoryID"] = function(t) + return GetAchievementCategory(t.achievementID) or -1; 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) + ["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 - return itemID; + end + local statistic = GetStatistic(t.achievementID); + if statistic and statistic ~= '0' then + return statistic; 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; + ["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.BaseArtifact = app.BaseObjectFields(fields, "BaseArtifact"); -app.CreateArtifact = function(id, t) - return setmetatable(constructor(id, t, "artifactID"), app.BaseArtifact); +app.BaseAchievement = app.BaseObjectFields(fields, "BaseAchievement"); +app.CreateAchievement = function(id, t) + return setmetatable(constructor(id, t, "achID"), app.BaseAchievement); end -end)(); --- Azerite Essence Lib -(function() -local fields = { +local categoryFields = { ["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"; + return "achievementCategoryID"; end, ["name"] = function(t) - return t.info.name; + return GetCategoryInfo(t.achievementCategoryID); end, - ["link"] = function(t) - return C_AzeriteEssence.GetEssenceHyperlink(t.azeriteEssenceID, t.rank); + ["icon"] = function(t) + return app.asset("Category_Achievements"); end, - ["rank"] = function(t) - return t.info.rank or 0; + ["parentCategoryID"] = function(t) + return select(2, GetCategoryInfo(t.achievementCategoryID)) or -1; end, }; -app.BaseAzeriteEssence = app.BaseObjectFields(fields, "BaseAzeriteEssence"); -app.CreateAzeriteEssence = function(id, t) - return setmetatable(constructor(id, t, "azeriteEssenceID"), app.BaseAzeriteEssence); +app.BaseAchievementCategory = app.BaseObjectFields(categoryFields, "BaseAchievementCategory"); +app.CreateAchievementCategory = function(id, t) + return setmetatable(constructor(id, t, "achievementCategoryID"), app.BaseAchievementCategory); 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 -local function default_link(t) - if t.itemID then - return select(2, GetItemInfo(t.itemID)); +-- 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 t.text; 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 = { +local criteriaFields = { ["key"] = function(t) - return "speciesID"; - end, - ["filterID"] = function(t) - return 101; + return "criteriaID"; end, - ["collectible"] = function(t) - return app.CollectibleBattlePets; + ["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, - ["collected"] = function(t) - if CollectedSpeciesHelper[t.speciesID] then - return 1; + ["name"] = function(t) + if t.link then return t.link; end + if t.encounterID then + return select(1, EJ_GetEncounterInfo(t.encounterID)); end - local altSpeciesID = t.altSpeciesID; - if altSpeciesID and CollectedSpeciesHelper[altSpeciesID]then - return 2; + 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, - ["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); + ["description"] = function(t) + if t.encounterID then + return select(2, EJ_GetEncounterInfo(t.encounterID)); + end end, ["link"] = function(t) - return cache.GetCachedField(t, "link", default_link); + 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, - ["tsm"] = function(t) - return sformat("p:%d:1:3", t.speciesID); + ["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, -}; -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; + ["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 - 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 -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 - end -end }); -local math_floor = math.floor; -local cache = app.CreateCache("classID"); -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 .. ")"; + ["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 - --]] - 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 "classID"; 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 .. ")"; + ["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 - 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]; + ["index"] = function(t) + return 1; end, + -- Use parent achievement if info not listed directly in the criteria ["c"] = function(t) - local c = { t.classID }; - rawset(t, "c", c); - return c; + return GetParentAchievementInfo(t, "c"); end, - ["nmc"] = function(t) - return t.classID ~= app.ClassIndex; + ["classID"] = function(t) + return GetParentAchievementInfo(t, "classID"); end, - ["classColors"] = function(t) - return RAID_CLASS_COLORS[select(2, GetClassInfo(t.classID))]; + ["races"] = function(t) + return GetParentAchievementInfo(t, "races"); + end, + ["r"] = function(t) + return GetParentAchievementInfo(t, "r"); end, }; -app.BaseCharacterClass = app.BaseObjectFields(fields, "BaseCharacterClass"); -app.CreateCharacterClass = function(id, t) - return setmetatable(constructor(id, t, "classID"), app.BaseCharacterClass); +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 -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; - end, - ["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 - 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, - ["_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; - 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); - 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.BaseCurrencyClass = app.BaseObjectFields(fields, "BaseCurrencyClass"); -app.CreateCurrencyClass = function(id, t) - return setmetatable(constructor(id, t, "currencyID"), app.BaseCurrencyClass); -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; - end - return false; -end -local fields = { - ["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; - 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); +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 - end - 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; - 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; + 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, - ["u"] = function(t) - if t.difficultyID == 24 or t.difficultyID == 33 then - return 1016; - end - end, - ["description"] = function(t) - if t.difficultyID == 24 or t.difficultyID == 33 then - return L["WE_JUST_HATE_TIMEWALKING"]; - end - end, -}; -app.BaseDifficulty = app.BaseObjectFields(fields, "BaseDifficulty"); -app.CreateDifficulty = function(id, t) - return setmetatable(constructor(id, t, "difficultyID"), app.BaseDifficulty); -end -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 + 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 -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; +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 - return displayInfos; 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 "encounterID"; + return "artifactID"; end, - ["name"] = function(t) - return cache.GetCachedField(t, "name", CacheInfo); + ["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, - ["lore"] = function(t) - return cache.GetCachedField(t, "lore", CacheInfo); + ["f"] = function(t) + return 11; end, - ["link"] = function(t) - return cache.GetCachedField(t, "link", CacheInfo); + ["collectible"] = function(t) + return app.CollectibleTransmog; end, - ["displayID"] = function(t) - return cache.GetCachedField(t, "displayID", CacheInfo); + ["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, - ["displayInfo"] = function(t) - return cache.GetCachedField(t, "displayInfo", default_displayInfo); + ["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, - ["icon"] = function(t) - return app.DifficultyIcons[GetRelativeValue(t, "difficultyID") or 1]; + ["title"] = function(t) + return t.variantText; end, - ["trackable"] = function(t) - return t.questID; + ["variantText"] = function(t) + return Colorize("Variant " .. t.artifactinfo[4], RGBToHex(t.artifactinfo[9] * 255, t.artifactinfo[10] * 255, t.artifactinfo[11] * 255)); end, - ["saved"] = function(t) - -- only consider encounters saved if saved for the current character - return IsQuestFlaggedCompleted(t.questID); + ["appearanceText"] = function(t) + return "|cffe6cc80" .. (t.artifactinfo[3] or "???") .. "|r"; end, - ["index"] = function(t) - return 1; + ["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.BaseEncounter = app.BaseObjectFields(fields, "BaseEncounter"); -app.CreateEncounter = function(id, t) - return setmetatable(constructor(id, t, "encounterID"), app.BaseEncounter); +app.BaseArtifact = app.BaseObjectFields(fields, "BaseArtifact"); +app.CreateArtifact = function(id, t) + return setmetatable(constructor(id, t, "artifactID"), app.BaseArtifact); end end)(); --- Faction Lib +-- Azerite Essence 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; +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 - 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.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); + + 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 - 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; + + if app.AccountWideAzeriteEssences and accountRank >= t.rank then + return 2; 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); - -- 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 "factionID"; - end, - ["name"] = function(t) - return cache.GetCachedField(t, "name", CacheInfo); end, - ["description"] = function(t) - return cache.GetCachedField(t, "description", CacheInfo); + ["text"] = function(t) + return t.link; end, - ["lore"] = function(t) - return cache.GetCachedField(t, "lore", CacheInfo); + ["lvl"] = function(t) + return 50; 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 t.info.icon or "Interface/ICONS/INV_Glowing Azerite Spire"; + end, + ["name"] = function(t) + return t.info.name; end, ["link"] = function(t) - return t.achievementID and GetAchievementLink(t.achievementID); + return C_AzeriteEssence.GetEssenceHyperlink(t.azeriteEssenceID, t.rank); 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; + ["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); +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 +local function default_link(t) + if t.itemID then + return select(2, GetItemInfo(t.itemID)); + end + return t.text; +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 112; + return 101; 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; + return app.CollectibleBattlePets; 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; + if CollectedSpeciesHelper[t.speciesID] then 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 + local altSpeciesID = t.altSpeciesID; + if altSpeciesID and CollectedSpeciesHelper[altSpeciesID]then return 2; 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 - end - end 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; - end - local friendID, _, _, _, _, _, _, _, nextFriendThreshold = GetFriendshipReputation(factionID); - if friendID and not nextFriendThreshold then - app.CurrentCharacter.Factions[factionID] = 1; - ATTAccountWideData.Factions[factionID] = 1; - return 1; - end + ["text"] = function(t) + return cache.GetCachedField(t, "text", CacheInfo); 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 + ["icon"] = function(t) + return cache.GetCachedField(t, "icon", CacheInfo); 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 + ["lore"] = function(t) + return cache.GetCachedField(t, "lore", CacheInfo); end, - ["reputation"] = function(t) - return select(6, GetFactionInfoByID(t.factionID)); + ["displayID"] = function(t) + return cache.GetCachedField(t, "displayID", CacheInfo); end, - ["ceiling"] = function(t) - local _, _, _, m, ma = GetFactionInfoByID(t.factionID); - return ma and m and (ma - m); + ["petTypeID"] = function(t) + return cache.GetCachedField(t, "petTypeID", CacheInfo); end, - ["standing"] = function(t) - return select(3, GetFactionInfoByID(t.factionID)) or 1; + ["name"] = function(t) + return cache.GetCachedField(t, "name", CacheInfo); end, - ["maxstanding"] = function(t) - if t.minReputation and t.minReputation[1] == t.factionID then - return app.GetFactionStanding(t.minReputation[2]); - end - return 8; + ["link"] = function(t) + return cache.GetCachedField(t, "link", default_link); end, - ["sortProgress"] = function(t) - return ((t.reputation or -42000) + 42000) / 84000; + ["tsm"] = function(t) + return sformat("p:%d:1:3", t.speciesID); end, }; -app.BaseFaction = app.BaseObjectFields(fields, "BaseFaction"); -app.CreateFaction = function(id, t) - return setmetatable(constructor(id, t, "factionID"), app.BaseFaction); +app.BaseSpecies = app.BaseObjectFields(fields, "BaseSpecies"); +app.CreateSpecies = function(id, t) + return setmetatable(constructor(id, t, "speciesID"), app.BaseSpecies); 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; + +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 - -- 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 "petAbilityID"; end, ["text"] = function(t) - return L["FILTER_ID_TYPES"][t.filterID]; - end, - ["name"] = function(t) - return t.text; + return select(2, C_PetBattles_GetAbilityInfoByID(t.petAbilityID)); end, ["icon"] = function(t) - return L["FILTER_ID_ICONS"][t.filterID]; + return select(3, C_PetBattles_GetAbilityInfoByID(t.petAbilityID)); + end, + ["description"] = function(t) + return select(5, C_PetBattles_GetAbilityInfoByID(t.petAbilityID)); end, }; -app.BaseFilter = app.BaseObjectFields(fields, "BaseFilter"); -app.CreateFilter = function(id, t) - return setmetatable(constructor(id, t, "filterID"), app.BaseFilter); +app.BasePetAbility = app.BaseObjectFields(fields, "BasePetAbility"); +app.CreatePetAbility = function(id, t) + return setmetatable(constructor(id, t, "petAbilityID"), app.BasePetAbility); end -end)(); - --- Flight Path 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 -}; -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 "petTypeID"; end, - ["name"] = function(t) - return t.info.name or L["VISIT_FLIGHT_MASTER"]; + ["text"] = function(t) + return _G["BATTLE_PET_NAME_" .. t.petTypeID]; end, ["icon"] = function(t) - 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, - ["altQuests"] = function(t) - return t.info.altQuests; - end, - ["description"] = function(t) - local description = t.info.description; - return (description and (description .."\n\n") or "") .. L["FLIGHT_PATHS_DESC"]; + return "Interface\\Icons\\Icon_PetFamily_"..PET_TYPE_SUFFIX[t.petTypeID]; end, - ["collectible"] = function(t) - return app.CollectibleFlightPaths; + ["filterID"] = function(t) + return 101; 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 - 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, - ["trackable"] = app.ReturnTrue, - ["saved"] = function(t) - return app.CurrentCharacter.FlightPaths[t.flightPathID]; + ["name"] = function(t) + return AllTheThingsAD.LocalizedCategoryNames[t.categoryID] or ("Unknown Category #" .. t.categoryID); end, - ["coord"] = function(t) - return t.info.coord; - end, - ["c"] = function(t) - return t.info.c; - end, - ["r"] = function(t) - local faction = t.info.faction; - if faction and faction > 0 then - return faction; - end - end, - ["u"] = function(t) - return t.info.u; - end, - ["crs"] = function(t) - return t.info.qg and { t.info.qg }; - end, - ["mapID"] = function(t) - return t.info.mapID; - 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; - end, - ["nmr"] = function(t) - local r = t.r; - return r and r ~= app.FactionID; - end, - ["sourceQuests"] = function(t) - return t.info.sourceQuests; + ["icon"] = function(t) + return AllTheThings.CategoryIcons[t.categoryID] or "Interface/ICONS/INV_Garrison_Blueprints1"; end, }; -app.BaseFlightPath = app.BaseObjectFields(fields, "BaseFlightPath"); -app.CreateFlightPath = function(id, t) - return setmetatable(constructor(id, t, "flightPathID"), app.BaseFlightPath); -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 +app.BaseCategory = app.BaseObjectFields(fields, "BaseCategory"); +app.CreateCategory = function(id, t) + return setmetatable(constructor(id, t, "categoryID"), app.BaseCategory); end end)(); --- Follower Lib +-- Character Class 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 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 +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 + end +end }); +local math_floor = math.floor; +local cache = app.CreateCache("classID"); 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; + -- 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 "followerID"; + return "classID"; end, ["text"] = function(t) - return cache.GetCachedField(t, "text", CacheInfo); - end, - ["name"] = function(t) - return cache.GetCachedField(t, "name", CacheInfo); + 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 cache.GetCachedField(t, "icon", CacheInfo); + -- return classIcons[t.classID]; end, - ["lvl"] = function(t) - return cache.GetCachedField(t, "lvl", CacheInfo); + ["c"] = function(t) + local c = { t.classID }; + rawset(t, "c", c); + return c; end, - ["title"] = function(t) - return cache.GetCachedField(t, "title", CacheInfo); + ["nmc"] = function(t) + return t.classID ~= app.ClassIndex; end, - ["displayID"] = function(t) - return cache.GetCachedField(t, "displayID", CacheInfo); + ["classColors"] = function(t) + return RAID_CLASS_COLORS[select(2, GetClassInfo(t.classID))]; end, - ["link"] = function(t) - if app.CurrentCharacter.Followers[t.followerID] then - return C_Garrison_GetFollowerLink(t.followerID); - else - return C_Garrison_GetFollowerLinkByID(t.followerID); +}; +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; end, - ["description"] = function(t) - return L["FOLLOWERS_COLLECTION_DESC"]; + ["icon"] = function(t) + if t.classID then return classIcons[t.classID]; end end, - ["collectible"] = function(t) - return app.CollectibleFollowers; + ["name"] = function(t) + return UnitName(t.unit); 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; - return 1; + ["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 - if app.AccountWideFollowers and ATTAccountWideData.Followers[t.followerID] then return 2; end + 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.BaseFollower = app.BaseObjectFields(fields, "BaseFollower"); -app.CreateFollower = function(id, t) - return setmetatable(constructor(id, t, "followerID"), app.BaseFollower); +app.BaseUnit = app.BaseObjectFields(unitFields, "BaseUnit"); +app.CreateUnit = function(unit, t) + return setmetatable(constructor(unit, t, "unit"), app.BaseUnit); end end)(); --- Garrison Lib +-- Currency Lib (function() -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, _, 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; +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 - if field then return _t[field]; end + return app.EmptyTable; end - local fields = { ["key"] = function(t) - return "buildingID"; + return "currencyID"; end, - ["text"] = function(t) - return t.link or cache.GetCachedField(t, "text", CacheInfo); + ["_cache"] = function(t) + return cache; end, - ["link"] = function(t) - return cache.GetCachedField(t, "link", CacheInfo); + ["text"] = function(t) + return cache.GetCachedField(t, "text", default_text); end, - ["name"] = function(t) - return cache.GetCachedField(t, "name", CacheInfo); + ["info"] = function(t) + return cache.GetCachedField(t, "info", default_info); end, - ["lore"] = function(t) - return cache.GetCachedField(t, "lore", CacheInfo); + ["link"] = function(t) + return cache.GetCachedField(t, "link", default_link); end, ["icon"] = function(t) - return cache.GetCachedField(t, "icon", CacheInfo); + local info = t.info; + return info and info.iconFileID; end, - ["filterID"] = function(t) - return t.itemID and 200; + ["name"] = function(t) + local info = t.info; + return info and info.name or ("Currency #" .. t.currencyID); end, - ["collectible"] = function(t) - return t.itemID and app.CollectibleRecipes; + ["costCollectibles"] = function(t) + return cache.GetCachedField(t, "costCollectibles", default_costCollectibles); 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; - end - if app.AccountWideRecipes and ATTAccountWideData.Buildings[id] then return 2; 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.BaseGarrisonBuilding = app.BaseObjectFields(fields, "BaseGarrisonBuilding"); -app.CreateGarrisonBuilding = function(id, t) - return setmetatable(constructor(id, t, "buildingID"), app.BaseGarrisonBuilding); +app.BaseCurrencyClass = app.BaseObjectFields(fields, "BaseCurrencyClass"); +app.CreateCurrencyClass = function(id, t) + return setmetatable(constructor(id, t, "currencyID"), app.BaseCurrencyClass); 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; + end + return false; +end local fields = { ["key"] = function(t) - return "missionID"; + return "deaths"; end, ["text"] = function(t) - return C_Garrison_GetMissionName(t.missionID); + return "Total Deaths"; 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 fields = { - ["key"] = function(t) - return "talentID"; - end, - ["info"] = function(t) - return C_Garrison_GetTalentInfo(t.talentID) or {}; + return app.asset("Category_Deaths"); end, - ["text"] = function(t) - return t.info.name; + ["progress"] = function(t) + return math.min(1000, app.AccountWideDeaths and ATTAccountWideData.Deaths or app.CurrentCharacter.Deaths); end, - ["icon"] = function(t) - return t.info.icon or "Interface/ICONS/INV_Icon_Mission_Complete_Order"; + ["total"] = function(t) + return 1000; end, ["description"] = function(t) - return t.info.description; + return "The ATT Gods must be sated. Go forth and attempt to level, mortal!\n\n 'Live! Die! Live Again!'\n"; end, - ["trackable"] = app.ReturnTrue, - ["saved"] = function(t) - return IsQuestFlaggedCompleted(t.questID) or t.info.researched; + ["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.BaseGarrisonTalent = app.BaseObjectFields(fields, "BaseGarrisonTalent"); -app.CreateGarrisonTalent = function(id, t) - return setmetatable(constructor(id, t, "talentID"), app.BaseGarrisonTalent); +app.BaseDeathClass = app.BaseObjectFields(fields, "BaseDeathClass"); +app.CreateDeathClass = function() + return setmetatable({}, app.BaseDeathClass); end end)(); --- Gear Set Lib +-- Difficulty 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; +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 "setID"; - end, - ["info"] = function(t) - return C_TransmogSets_GetSetInfo(t.setID) or {}; + return "difficultyID"; end, ["text"] = function(t) - return t.info.name; + return L["CUSTOM_DIFFICULTIES"][t.difficultyID] or GetDifficultyInfo(t.difficultyID) or "Unknown Difficulty"; 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; - end - end - 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; + return app.DifficultyIcons[t.difficultyID]; end, - ["header"] = function(t) - return t.info.label; + ["saved"] = function(t) + return t.locks; end, - ["subheader"] = function(t) - return t.info.description; + ["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 + end + end end, - ["title"] = function(t) - return t.info.requiredFaction; + ["u"] = function(t) + if t.difficultyID == 24 or t.difficultyID == 33 then + return 1016; + end end, - ["sources"] = function(t) - local sources = C_TransmogSets_GetSetSources(t.setID); - if sources then - rawset(t, "sources", sources); - return sources; + ["description"] = function(t) + if t.difficultyID == 24 or t.difficultyID == 33 then + return L["WE_JUST_HATE_TIMEWALKING"]; end end, }; -app.BaseGearSet = app.BaseObjectFields(fields, "BaseGearSet"); -app.CreateGearSet = function(id, t) - return setmetatable(constructor(id, t, "setID"), app.BaseGearSet); +app.BaseDifficulty = app.BaseObjectFields(fields, "BaseDifficulty"); +app.CreateDifficulty = function(id, t) + return setmetatable(constructor(id, t, "difficultyID"), app.BaseDifficulty); end +end)(); -local fields = { - ["key"] = function(t) - return "s"; - 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; +-- 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, - ["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, - ["specs"] = function(t) - return t.itemID and GetFixedItemSpecInfo(t.itemID); - end, - ["invType"] = function(t) - return t.info.invType or 99; - end, -}; -app.BaseGearSource = app.BaseObjectFields(fields, "BaseGearSource"); -app.CreateGearSource = function(id) - return setmetatable({ s = id}, app.BaseGearSource); + end + return displayInfos; end - local fields = { ["key"] = function(t) - return "setID"; - end, - ["info"] = function(t) - return C_TransmogSets_GetSetInfo(t.setID) or {}; + return "encounterID"; end, - ["text"] = function(t) - return t.info.label; + ["name"] = function(t) + return cache.GetCachedField(t, "name", CacheInfo); end, - ["icon"] = function(t) - return t.achievementID and select(10, GetAchievementInfo(t.achievementID)); + ["lore"] = function(t) + return cache.GetCachedField(t, "lore", CacheInfo); end, ["link"] = function(t) - 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, -}; -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"; + return cache.GetCachedField(t, "link", CacheInfo); end, - ["info"] = function(t) - return C_TransmogSets_GetSetInfo(t.setID) or {}; + ["displayID"] = function(t) + return cache.GetCachedField(t, "displayID", CacheInfo); end, - ["text"] = function(t) - return t.info.description; + ["displayInfo"] = function(t) + return cache.GetCachedField(t, "displayInfo", default_displayInfo); end, ["icon"] = function(t) - return t.achievementID and select(10, GetAchievementInfo(t.achievementID)); + return app.DifficultyIcons[GetRelativeValue(t, "difficultyID") or 1]; end, - ["link"] = function(t) - return t.achievementID and GetAchievementLink(t.achievementID); + ["trackable"] = function(t) + return t.questID; 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 + ["saved"] = function(t) + -- only consider encounters saved if saved for the current character + return IsQuestFlaggedCompleted(t.questID); + end, + ["index"] = function(t) + return 1; end, }; -app.BaseGearSetSubHeader = app.BaseObjectFields(fields, "BaseGearSetSubHeader"); -app.CreateGearSetSubHeader = function(id, t) - return setmetatable(constructor(id, t, "setID"), app.BaseGearSetSubHeader); +app.BaseEncounter = app.BaseObjectFields(fields, "BaseEncounter"); +app.CreateEncounter = function(id, t) + return setmetatable(constructor(id, t, "encounterID"), app.BaseEncounter); end end)(); --- Holiday Lib +-- Faction 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 +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 - 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, +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.BaseHoliday = app.BaseObjectFields(fields, "BaseHoliday"); -app.CreateHoliday = function(id, t) - return setmetatable(constructor(id, t, "holidayID"), app.BaseHoliday); +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 -end)(); - --- Illusion Lib --- TODO: add caching for consistency/move to sub-item lib? -(function() -local fields = { - ["key"] = function(t) - return "illusionID"; - end, - ["filterID"] = function(t) - return 103; - 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; +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 - return t.silentLink; - end, - ["name"] = function(t) - return t.text; - end, - ["icon"] = function(t) - return "Interface/ICONS/INV_Enchant_Disenchant"; - 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 + 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; end - end, - ["collectible"] = function(t) - return app.CollectibleIllusions; - 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)); - end, -}; -app.BaseIllusion = app.BaseObjectFields(fields, "BaseIllusion"); -app.CreateIllusion = function(id, t) - return setmetatable(constructor(id, t, "illusionID"), app.BaseIllusion); + end end -end)(); - --- Instance Lib -(function() -local cache = app.CreateCache("instanceID"); +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 name, lore, _, _, _, icon, _, link = EJ_GetInstanceInfo(id); - _t.name = name; - _t.lore = lore; - _t.icon = icon; - _t.link = link; + -- 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 "instanceID"; - end, - ["icon"] = function(t) - return cache.GetCachedField(t, "icon", CacheInfo); + return "factionID"; end, ["name"] = function(t) return cache.GetCachedField(t, "name", CacheInfo); end, + ["description"] = function(t) + return cache.GetCachedField(t, "description", CacheInfo); + end, ["lore"] = function(t) return cache.GetCachedField(t, "lore", CacheInfo); 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"); + end, ["link"] = function(t) - return cache.GetCachedField(t, "link", CacheInfo); + return t.achievementID and GetAchievementLink(t.achievementID); end, - ["back"] = function(t) - if app.CurrentMapID == t.mapID or (t.maps and contains(t.maps, app.CurrentMapID)) then - return 1; + ["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, - ["saved"] = function(t) - return t.locks; + ["filterID"] = function(t) + return 112; end, - ["locks"] = function(t) - local locks = app.CurrentCharacter.Lockouts[t.name]; - if locks then - rawset(t, "locks", locks); - return locks; + ["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; 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)(); + ["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 --- 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); + -- If there's an associated achievement, return partial completion. + if t.achievementID and select(4, GetAchievementInfo(t.achievementID)) then + return 2; 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); + + -- 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 - 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, + ["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 - if not modID or modID < 1 then - modID = nil; + local friendID, _, _, _, _, _, _, _, nextFriendThreshold = GetFriendshipReputation(factionID); + if friendID and not nextFriendThreshold then + app.CurrentCharacter.Factions[factionID] = 1; + ATTAccountWideData.Factions[factionID] = 1; + return 1; 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); + 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 - 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"; -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 - -- 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 + 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 - -- If no results, search by plain itemID only - if not results and t.itemID then - id = t.itemID; - results = app.SearchForField("itemIDAsCost", id); - 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; - end - return app.EmptyTable; -end -local itemFields = { - ["key"] = function(t) - return "itemID"; end, - ["_cache"] = function(t) - return cache; + ["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, - ["text"] = function(t) - return t.link or t.name; + ["reputation"] = function(t) + return select(6, GetFactionInfoByID(t.factionID)); end, - ["icon"] = function(t) - return cache.GetCachedField(t, "icon", default_icon); + ["ceiling"] = function(t) + local _, _, _, m, ma = GetFactionInfoByID(t.factionID); + return ma and m and (ma - m); end, - ["link"] = function(t) - return cache.GetCachedField(t, "link", default_link); + ["standing"] = function(t) + return select(3, GetFactionInfoByID(t.factionID)) or 1; end, - ["name"] = function(t) - return cache.GetCachedField(t, "name"); + ["maxstanding"] = function(t) + if t.minReputation and t.minReputation[1] == t.factionID then + return app.GetFactionStanding(t.minReputation[2]); + end + return 8; end, - ["specs"] = function(t) - return cache.GetCachedField(t, "specs", default_specs); + ["sortProgress"] = function(t) + return ((t.reputation or -42000) + 42000) / 84000; end, - ["retries"] = function(t) - return cache.GetCachedField(t, "retries"); +}; +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 "filterID"; end, - ["q"] = function(t) - return cache.GetCachedField(t, "q"); + ["text"] = function(t) + return L["FILTER_ID_TYPES"][t.filterID]; end, - ["b"] = function(t) - return cache.GetCachedField(t, "b") or 2; + ["name"] = function(t) + return t.text; 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"); + ["icon"] = function(t) + return L["FILTER_ID_ICONS"][t.filterID]; 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); +}; +app.BaseFilter = app.BaseObjectFields(fields, "BaseFilter"); +app.CreateFilter = function(id, t) + return setmetatable(constructor(id, t, "filterID"), app.BaseFilter); +end +end)(); + +-- Flight Path 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 +}; +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 - return sformat("i:%d", itemLink); + -- 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, - ["repeatable"] = function(t) - return rawget(t, "isDaily") or rawget(t, "isWeekly") or rawget(t, "isMonthly") or rawget(t, "isYearly") or rawget(t, "isWorldQuest"); + ["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; end, - ["modItemID"] = function(t) - rawset(t, "modItemID", GetGroupItemIDWithModID(t) or t.itemID); - return rawget(t, "modItemID"); + ["name"] = function(t) + return t.info.name or L["VISIT_FLIGHT_MASTER"]; end, - ["indicatorIcon"] = function(t) - return app.GetQuestIndicator(t); + ["icon"] = function(t) + 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, - ["trackableAsQuest"] = app.ReturnTrue, - ["collectibleAsAchievement"] = function(t) - return app.CollectibleAchievements; + ["altQuests"] = function(t) + return t.info.altQuests; end, - ["costCollectibles"] = function(t) - return cache.GetCachedField(t, "costCollectibles", default_costCollectibles); + ["description"] = function(t) + local description = t.info.description; + return (description and (description .."\n\n") or "") .. L["FLIGHT_PATHS_DESC"]; end, - ["collectibleAsCost"] = app.CollectibleAsCost, - ["collectedAsCost"] = app.CollectedAsCost, - ["costsCount"] = function(t) - if t.costCollectibles then return #t.costCollectibles; end + ["collectible"] = function(t) + return app.CollectibleFlightPaths; end, - ["collectibleAsFaction"] = function(t) - return app.CollectibleReputations; + ["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, - ["collectibleAsFactionOrQuest"] = function(t) - return app.CollectibleReputations or t.collectibleAsQuest; + ["trackable"] = app.ReturnTrue, + ["saved"] = function(t) + return app.CurrentCharacter.FlightPaths[t.flightPathID]; end, - ["collectibleAsTransmog"] = function(t) - return app.CollectibleTransmog; + ["coord"] = function(t) + return t.info.coord; end, - ["collectibleAsQuest"] = app.CollectibleAsQuest, - ["collectedAsQuest"] = IsQuestFlaggedCompletedForObject, - ["lockedAsQuest"] = function(t) - return app.LockedAsQuest(t); + ["c"] = function(t) + return t.info.c; 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 - - -- 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 + ["r"] = function(t) + local faction = t.info.faction; + if faction and faction > 0 then + return faction; end end, - ["collectedAsFactionOrQuest"] = function(t) - return t.collectedAsFaction or t.collectedAsQuest; + ["u"] = function(t) + return t.info.u; end, - ["collectedAsTransmog"] = function(t) - return ATTAccountWideData.Sources[rawget(t, "s")]; + ["crs"] = function(t) + return t.info.qg and { t.info.qg }; end, - ["savedAsQuest"] = function(t) - return IsQuestFlaggedCompleted(t.questID); + ["mapID"] = function(t) + return t.info.mapID; end, - ["costTotal"] = function(t) - return t.collectibleAsCost and 1 or 0; + ["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, - ["costProgress"] = function(t) - return t.collectedAsCost and 1 or 0; + ["nmr"] = function(t) + local r = t.r; + return r and r ~= app.FactionID; + end, + ["sourceQuests"] = function(t) + return t.info.sourceQuests; 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.BaseFlightPath = app.BaseObjectFields(fields, "BaseFlightPath"); +app.CreateFlightPath = function(id, t) + return setmetatable(constructor(id, t, "flightPathID"), app.BaseFlightPath); 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; +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 - if ATTAccountWideData.CommonItems[t.itemID] then - return 2; - end - end -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); - 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; + UpdateRawIDs("flightPathID", newFPs); 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 +-- Follower 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; +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 -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); + if field then return _t[field]; end end -end)(); - --- Heirloom 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 "followerID"; end, ["text"] = function(t) - return L["HEIRLOOM_TEXT"]; - end, - ["icon"] = function(t) - return "Interface/ICONS/Achievement_GuildPerk_WorkingOvertime_Rank2"; - end, - ["description"] = function(t) - return L["HEIRLOOM_TEXT_DESC"]; + return cache.GetCachedField(t, "text", CacheInfo); end, - ["collectible"] = function(t) - return app.CollectibleHeirlooms; + ["name"] = function(t) + return cache.GetCachedField(t, "name", CacheInfo); end, - ["saved"] = function(t) - return C_Heirloom_PlayerHasHeirloom(t.heirloomUnlockID); + ["icon"] = function(t) + return cache.GetCachedField(t, "icon", CacheInfo); end, - ["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"; + ["lvl"] = function(t) + return cache.GetCachedField(t, "lvl", CacheInfo); end, - ["level"] = function(t) - return 1; + ["title"] = function(t) + return cache.GetCachedField(t, "title", CacheInfo); end, - ["text"] = function(t) - return "Upgrade Level " .. t.level; + ["displayID"] = function(t) + return cache.GetCachedField(t, "displayID", CacheInfo); end, - ["icon"] = function(t) - return t.isWeapon and weaponTextures[t.level] or armorTextures[t.level]; + ["link"] = function(t) + 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["HEIRLOOMS_UPGRADES_DESC"]; + return L["FOLLOWERS_COLLECTION_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 + return app.CollectibleFollowers; end, - ["trackable"] = app.ReturnTrue, - ["isWeapon"] = function(t) - if t.f and contains(isWeapon, t.f) then - rawset(t, "isWeapon", true); - return true; + ["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; + return 1; end - rawset(t, "isWeapon", false); - return false; + if app.AccountWideFollowers and ATTAccountWideData.Followers[t.followerID] then return 2; end end, }; -fields.collected = fields.saved; -app.BaseHeirloomLevel = app.BaseObjectFields(fields, "BaseHeirloomLevel"); +app.BaseFollower = app.BaseObjectFields(fields, "BaseFollower"); +app.CreateFollower = function(id, t) + return setmetatable(constructor(id, t, "followerID"), app.BaseFollower); +end +end)(); --- 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; +-- 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 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 -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"); + -- 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 - -app.BaseHeirloom = app.BaseObjectFields(fields, "BaseHeirloom"); -app.CreateHeirloom = function(id, t) - tinsert(heirloomIDs, id); - return setmetatable(constructor(id, t, "itemID"), app.BaseHeirloom); + if field then return _t[field]; end 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 +local fields = { + ["key"] = function(t) + return "buildingID"; + end, + ["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); + end, + ["lore"] = function(t) + return cache.GetCachedField(t, "lore", CacheInfo); + end, + ["icon"] = function(t) + return cache.GetCachedField(t, "icon", CacheInfo); + end, + ["filterID"] = function(t) + return t.itemID and 200; + end, + ["collectible"] = function(t) + return t.itemID and app.CollectibleRecipes; + 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; end - 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 - -- 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 +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 - wipe(heirloomIDs); +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 end)(); --- Toy Lib +-- Gear Set Lib (function() --- copy base Item fields -local fields = RawCloneData(itemFields); -fields.filterID = function(t) - return 102; - end -fields.collectible = function(t) - return app.CollectibleToys; +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 -fields.collected = function(t) - return ATTAccountWideData.Toys[t.itemID]; - end -fields.tsm = function(t) - return sformat("i:%d", t.itemID); - end -fields.isToy = app.ReturnTrue; -fields.toyID = function(t) - return t.itemID; + 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; + end + end + 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); +end -app.BaseToy = app.BaseObjectFields(fields, "BaseToy"); -app.CreateToy = function(id, t) - return setmetatable(constructor(id, t, "itemID"), app.BaseToy); +local fields = { + ["key"] = function(t) + return "s"; + 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; + end + end, + ["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, + ["specs"] = function(t) + return t.itemID and GetFixedItemSpecInfo(t.itemID); + end, + ["invType"] = function(t) + return t.info.invType or 99; + end, +}; +app.BaseGearSource = app.BaseObjectFields(fields, "BaseGearSource"); +app.CreateGearSource = function(id) + return setmetatable({ s = id}, app.BaseGearSource); 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; - 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; +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.label; + end, + ["icon"] = function(t) + return t.achievementID and select(10, GetAchievementInfo(t.achievementID)); + end, + ["link"] = function(t) + 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 + end, +}; +app.BaseGearSetHeader = app.BaseObjectFields(fields, "BaseGearSetHeader"); +app.CreateGearSetHeader = function(id, t) + return setmetatable(constructor(id, t, "setID"), app.BaseGearSetHeader); +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; +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.description; + end, + ["icon"] = function(t) + return t.achievementID and select(10, GetAchievementInfo(t.achievementID)); + end, + ["link"] = function(t) + 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, +}; +app.BaseGearSetSubHeader = app.BaseObjectFields(fields, "BaseGearSetSubHeader"); +app.CreateGearSetSubHeader = function(id, t) + return setmetatable(constructor(id, t, "setID"), app.BaseGearSetSubHeader); end -app.BaseItemHarvester = app.BaseObjectFields(itemHarvesterFields, "BaseItemHarvester"); +end)(); -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+"; +-- 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 - -- 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 + end + end + end + end + 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)(); - 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 - 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 - --- 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 +-- Illusion Lib +-- TODO: add caching for consistency/move to sub-item lib? +(function() +local fields = { + ["key"] = function(t) + return "illusionID"; + end, + ["filterID"] = function(t) + return 103; + 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 - 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 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; + return t.silentLink; + end, + ["name"] = function(t) + return t.text; + end, + ["icon"] = function(t) + return "Interface/ICONS/INV_Enchant_Disenchant"; + 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 end - end - return depth; + end, + ["collectible"] = function(t) + return app.CollectibleIllusions; + 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)); + end, +}; +app.BaseIllusion = app.BaseObjectFields(fields, "BaseIllusion"); +app.CreateIllusion = function(id, t) + return setmetatable(constructor(id, t, "illusionID"), app.BaseIllusion); end end)(); --- Map Lib +-- Instance 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 +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 mapFields = { +local fields = { ["key"] = function(t) - return "mapID"; + return "instanceID"; + end, + ["icon"] = function(t) + return cache.GetCachedField(t, "icon", CacheInfo); end, ["name"] = function(t) - return t.creatureID and app.NPCNameFromID[t.creatureID] or app.GetMapName(t.mapID); + return cache.GetCachedField(t, "name", CacheInfo); end, - ["icon"] = function(t) - return t.creatureID and L["HEADER_ICONS"][t.creatureID] or app.asset("Category_Zones"); + ["lore"] = function(t) + return cache.GetCachedField(t, "lore", CacheInfo); + end, + ["link"] = function(t) + return cache.GetCachedField(t, "link", CacheInfo); end, ["back"] = function(t) if app.CurrentMapID == t.mapID or (t.maps and contains(t.maps, app.CurrentMapID)) then return 1; end end, - ["lvl"] = function(t) - return select(1, C_Map_GetMapLevels(t.mapID)); + ["saved"] = function(t) + return t.locks; end, - ["iconForAchievement"] = function(t) - return t.achievementID and select(10, GetAchievementInfo(t.achievementID)) or app.asset("Category_Zones"); - end, - ["linkForAchievement"] = function(t) - return GetAchievementLink(t.achievementID); + ["locks"] = function(t) + local locks = app.CurrentCharacter.Lockouts[t.name]; + if locks then + rawset(t, "locks", locks); + return locks; + end end, + ["isLockoutShared"] = app.ReturnFalse, }; -app.BaseMap = app.BaseObjectFields(mapFields, "BaseMap"); +app.BaseInstance = app.BaseObjectFields(fields, "BaseInstance"); +app.CreateInstance = function(id, t) + return setmetatable(constructor(id, t, "instanceID"), app.BaseInstance); +end +end)(); -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); +-- 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 - t = setmetatable(t, app.BaseMap); + rawset(_t, "retries", 1); 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; +-- 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 - - if not mapObject.text then - local mapInfo = C_Map_GetMapInfo(id); - if mapInfo then - mapObject.text = mapInfo.name; +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 - return mapObject; end - -app.events.ZONE_CHANGED_INDOORS = function() - app.GetCurrentMapID(); +local function default_icon(t) + return t.itemID and select(5, GetItemInfoInstant(t.itemID)) or "Interface\\Icons\\INV_Misc_QuestionMark"; end -app.events.ZONE_CHANGED_NEW_AREA = function() - app.GetCurrentMapID(); +local function default_specs(t) + return GetFixedItemSpecInfo(t.itemID); end -app:RegisterEvent("ZONE_CHANGED_INDOORS"); -app:RegisterEvent("ZONE_CHANGED_NEW_AREA"); -end)(); - --- Mount 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); - 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; +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 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; + -- 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 - 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 + -- If no results, search by plain itemID only + if not results and t.itemID then + id = t.itemID; + results = app.SearchForField("itemIDAsCost", id); + 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; end return app.EmptyTable; end -local mountFields = { +local itemFields = { ["key"] = function(t) - return "spellID"; + return "itemID"; end, ["_cache"] = function(t) return cache; end, ["text"] = function(t) - return cache.GetCachedField(t, "text", CacheInfo); + return t.link or t.name; end, ["icon"] = function(t) - return cache.GetCachedField(t, "icon", CacheInfo); + return cache.GetCachedField(t, "icon", default_icon); end, ["link"] = function(t) - return cache.GetCachedField(t, "link", CacheInfo); - end, - ["filterID"] = function(t) - return 100; - end, - ["collectible"] = function(t) - return app.CollectibleMounts; + return cache.GetCachedField(t, "link", default_link); end, - ["costCollectibles"] = function(t) - return cache.GetCachedField(t, "costCollectibles", default_costCollectibles); + ["name"] = function(t) + return cache.GetCachedField(t, "name"); 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 + ["specs"] = function(t) + return cache.GetCachedField(t, "specs", default_specs); end, - ["costTotal"] = function(t) - return t.collectibleAsCost and 1 or 0; + ["retries"] = function(t) + return cache.GetCachedField(t, "retries"); end, - ["costProgress"] = function(t) - return t.collectedAsCost and 1 or 0; + ["q"] = function(t) + return cache.GetCachedField(t, "q"); 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); + return cache.GetCachedField(t, "b") or 2; end, - ["displayID"] = function(t) - return cache.GetCachedField(t, "displayID", CacheInfo); + ["title"] = function(t) + return cache.GetCachedField(t, "title"); end, - ["name"] = function(t) - return cache.GetCachedField(t, "name", CacheInfo); + ["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) - 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 -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 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); 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); -end -app:RegisterEvent("NEW_MOUNT_ADDED"); -end)(); - --- 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 + ["repeatable"] = function(t) + return rawget(t, "isDaily") or rawget(t, "isWeekly") or rawget(t, "isMonthly") or rawget(t, "isYearly") or rawget(t, "isWorldQuest"); end, - ["icon"] = function(t) - local _, link, _, _, _, _, _, _, _, icon = GetItemInfo(t.itemID); - if link then - rawset(t, "link", link); - rawset(t, "icon", icon); - return icon; - end + ["modItemID"] = function(t) + rawset(t, "modItemID", GetGroupItemIDWithModID(t) or t.itemID); + return rawget(t, "modItemID"); 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"]; - end - return L["MUSIC_ROLLS_AND_SELFIE_DESC"]; + ["indicatorIcon"] = app.GetQuestIndicator, + ["trackableAsQuest"] = app.ReturnTrue, + ["collectibleAsAchievement"] = function(t) + return app.CollectibleAchievements; end, - ["filterID"] = function(t) - return 108; + ["costCollectibles"] = function(t) + return cache.GetCachedField(t, "costCollectibles", default_costCollectibles); end, - ["lvl"] = function(t) - return 40; + ["collectibleAsCost"] = app.CollectibleAsCost, + -- ["collectedAsCost"] = app.CollectedAsCost, + ["costsCount"] = function(t) + if t.costCollectibles then return #t.costCollectibles; end end, - ["collectible"] = function(t) - return app.CollectibleMusicRollsAndSelfieFilters; + ["collectibleAsFaction"] = function(t) + return app.CollectibleReputations; 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 + ["collectibleAsFactionOrQuest"] = function(t) + return app.CollectibleReputations or t.collectibleAsQuest; end, - ["saved"] = function(t) - return IsQuestFlaggedCompleted(t.questID); + ["collectibleAsTransmog"] = function(t) + return app.CollectibleTransmog; end, -}; -app.BaseMusicRoll = app.BaseObjectFields(fields, "BaseMusicRoll"); -app.CreateMusicRoll = function(questID, t) - return setmetatable(constructor(questID, t, "questID"), app.BaseMusicRoll); -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 -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 "."); + -- 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 end end, - ["collectible"] = function(t) - return app.CollectibleMusicRollsAndSelfieFilters; + ["collectedAsFactionOrQuest"] = function(t) + return t.collectedAsFaction or t.collectedAsQuest; end, - ["collected"] = function(t) - if IsQuestFlaggedCompleted(t.questID) then return 1; end - if app.AccountWideMusicRollsAndSelfieFilters and ATTAccountWideData.Quests[t.questID] then - return 2; - end + ["collectedAsTransmog"] = function(t) + return ATTAccountWideData.Sources[rawget(t, "s")]; end, - ["trackable"] = app.ReturnTrue, - ["saved"] = function(t) + ["savedAsQuest"] = function(t) return IsQuestFlaggedCompleted(t.questID); end, - ["lvl"] = function(t) - return 40; + ["costTotal"] = function(t) + return t.collectibleAsCost and 1 or 0; + end, + ["costProgress"] = function(t) + return t.collectedAsCost and 1 or 0; end, }; -app.BaseSelfieFilter = app.BaseObjectFields(fields, "BaseSelfieFilter"); -app.CreateSelfieFilter = function(id, t) - return setmetatable(constructor(id, t, "questID"), app.BaseSelfieFilter); -end -end)(); +app.BaseItem = app.BaseObjectFields(itemFields, "BaseItem"); --- 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; +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 + end + if ATTAccountWideData.CommonItems[t.itemID] then + return 2; 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, +end +app.BaseCommonItem = app.BaseObjectFields(fields, "BaseCommonItem"); - ["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; +-- 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 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)(); + +-- Heirloom 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"; end, - ["linkAsAchievement"] = function(t) - return GetAchievementLink(t.achievementID); + ["text"] = function(t) + return L["HEIRLOOM_TEXT"]; end, - ["collectibleAsQuest"] = app.CollectibleAsQuest, - ["collectedAsQuest"] = IsQuestFlaggedCompletedForObject, - ["savedAsQuest"] = function(t) - return IsQuestFlaggedCompleted(t.questID) or IsAnyQuestFlaggedCompleted(t.altQuests); + ["icon"] = function(t) + return "Interface/ICONS/Achievement_GuildPerk_WorkingOvertime_Rank2"; 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"); + ["description"] = function(t) + return L["HEIRLOOM_TEXT_DESC"]; 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 + ["collectible"] = function(t) + return app.CollectibleHeirlooms; end, - ["indicatorIcon"] = function(t) - if app.CurrentVignettes["npcID"][t.npcID] then - return "Category_Secrets"; - end + ["saved"] = function(t) + return C_Heirloom_PlayerHasHeirloom(t.heirloomUnlockID); end, + ["trackable"] = app.ReturnTrue, }; -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"); +fields.collected = fields.saved; +app.BaseHeirloomUnlocked = app.BaseObjectFields(fields, "BaseHeirloomUnlocked"); --- Header Lib -local headerFields = { +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 "headerID"; + return "heirloomLevelID"; end, - ["name"] = function(t) - return L["HEADER_NAMES"][t.headerID]; + ["level"] = function(t) + return 1; + end, + ["text"] = function(t) + return "Upgrade Level " .. t.level; end, ["icon"] = function(t) - return L["HEADER_ICONS"][t.headerID]; + return t.isWeapon and weaponTextures[t.level] or armorTextures[t.level]; 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)); + return L["HEIRLOOMS_UPGRADES_DESC"]; end, - ["iconAsAchievement"] = function(t) - return L["HEADER_ICONS"][t.headerID] or select(10, GetAchievementInfo(t.achievementID)); + ["collectible"] = function(t) + return app.CollectibleHeirlooms and app.CollectibleHeirloomUpgrades; end, - ["linkAsAchievement"] = function(t) - return GetAchievementLink(t.achievementID); + ["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, - ["savedAsQuest"] = function(t) - return IsQuestFlaggedCompleted(t.questID); + ["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, - ["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 - 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 +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 - if rawget(t, "questID") then - return setmetatable(constructor(id, t, "npcID"), app.BaseNPCWithQuest); - else - return setmetatable(constructor(id, t, "npcID"), app.BaseNPC); + -- 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 - elseif id > 1 then - return setmetatable(constructor(id, t, "npcID"), app.BaseNPC); - else - return setmetatable(constructor(id, t, "headerID"), app.BaseHeader); + 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 -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, +-- 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 - ["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; + -- 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, - ["lockedAsQuest"] = function(t) - return app.LockedAsQuest(t); - end, - ["indicatorIcon"] = function(t) - if app.CurrentVignettes["objectID"][t.objectID] then - return "Category_Secrets"; - end - 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 + -- 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 - -- 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 + 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 - -- 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 + + wipe(heirloomIDs); +end +end)(); + +-- Toy Lib +(function() +-- copy base Item fields +local fields = RawCloneData(itemFields); +fields.filterID = function(t) + return 102; + end +fields.collectible = function(t) + return app.CollectibleToys; + end +fields.collected = function(t) + return ATTAccountWideData.Toys[t.itemID]; + end +fields.tsm = function(t) + return sformat("i:%d", t.itemID); + end +fields.isToy = app.ReturnTrue; +fields.toyID = function(t) + return t.itemID; + end + +app.BaseToy = app.BaseObjectFields(fields, "BaseToy"); +app.CreateToy = function(id, t) + return setmetatable(constructor(id, t, "itemID"), app.BaseToy); +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; + 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 + + 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 end + -- if debugPrint then print("---") end + t.info.retries = nil; + rawset(t, "text", link); + rawset(t, "collected", true); end - return unsavedCoords; - end, -}; -app.BaseObject = app.BaseObjectFields(objectFields, "BaseObject"); - -local fields = RawCloneData(objectFields); -fields.icon = objectFields.iconAsAchievement; ---fields.link = objectFields.linkAsAchievement; -app.BaseObjectWithAchievement = app.BaseObjectFields(fields, "BaseObjectWithAchievement"); - -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"); + 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 = 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); +-- 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 - else - if rawget(t, "questID") then - return setmetatable(constructor(id, t, "objectID"), app.BaseObjectWithQuest); + 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 + 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; end end end - return setmetatable(constructor(id, t, "objectID"), app.BaseObject); + return depth; end end)(); --- Profession Lib +-- Map 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 -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 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); +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 -end)(); - --- PVP Ranks -(function() -local fields = { +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 "pvpRankID"; + return "mapID"; end, - ["text"] = function(t) - return _G["PVP_RANK_" .. (t.pvpRankID + 4) .. "_" .. (t.inverseR or 0)]; + ["name"] = function(t) + return t.creatureID and app.NPCNameFromID[t.creatureID] or app.GetMapName(t.mapID); 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."; + return t.creatureID and L["HEADER_ICONS"][t.creatureID] or app.asset("Category_Zones"); end, - ["r"] = function(t) - return t.parent.r or app.FactionID; + ["back"] = function(t) + if app.CurrentMapID == t.mapID or (t.maps and contains(t.maps, app.CurrentMapID)) then + return 1; + end end, - ["inverseR"] = function(t) - return t.r == Enum.FlightPathFaction.Alliance and 1 or 0; + ["lvl"] = function(t) + return select(1, C_Map_GetMapLevels(t.mapID)); end, - ["lifetimeRank"] = function(t) - return select(3, GetPVPLifetimeStats()); + ["iconForAchievement"] = function(t) + return t.achievementID and select(10, GetAchievementInfo(t.achievementID)) or app.asset("Category_Zones"); end, - ["collectible"] = app.ReturnFalse, - ["collected"] = function(t) - return t.lifetimeRank >= t.pvpRankID; + ["linkForAchievement"] = function(t) + return GetAchievementLink(t.achievementID); 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)(); - --- Quest 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; +app.BaseMap = app.BaseObjectFields(mapFields, "BaseMap"); -local function QuestConsideredSaved(questID) - if app.IsInPartySync then - return C_QuestLog_IsQuestReplayedRecently(questID) or (not C_QuestLog_IsQuestReplayable(questID) and IsQuestFlaggedCompleted(questID)); +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 IsQuestFlaggedCompleted(questID); + 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 + 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, + if not mapObject.text then + local mapInfo = C_Map_GetMapInfo(id); + if mapInfo then + mapObject.text = mapInfo.name; + end + end + return mapObject; +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, +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)(); - ["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 +-- Mount 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 - -- if an alt-quest is completed, then this quest is locked - if t.altcollected then - rawset(t, "locked", t.altcollected); - return true; + 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 - -- 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 + 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 - -- 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); + return app.EmptyTable; end -app.LockedAsQuest = LockedAsQuest; - -local questFields = { +local mountFields = { ["key"] = function(t) - return "questID"; - end, - ["name"] = function(t) - return app.QuestTitleFromID[t.questID]; + return "spellID"; 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) + ["_cache"] = function(t) + return cache; 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 + ["text"] = function(t) + return cache.GetCachedField(t, "text", CacheInfo); 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"; - end - 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 + return cache.GetCachedField(t, "icon", CacheInfo); end, ["link"] = function(t) - return GetQuestLink(t.questID) or "quest:" .. t.questID; + return cache.GetCachedField(t, "link", CacheInfo); end, - ["repeatable"] = function(t) - return rawget(t, "isDaily") or rawget(t, "isWeekly") or rawget(t, "isMonthly") or rawget(t, "isYearly") or rawget(t, "isWorldQuest"); + ["filterID"] = function(t) + return 100; end, ["collectible"] = function(t) - return app.CollectibleAsQuest(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) - return IsQuestFlaggedCompletedForObject(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, - ["trackable"] = app.ReturnTrue, - ["saved"] = function(t) - return QuestConsideredSaved(t.questID); + ["costTotal"] = function(t) + return t.collectibleAsCost and 1 or 0; end, - ["indicatorIcon"] = function(t) - return app.GetQuestIndicator(t); + ["costProgress"] = function(t) + return t.collectedAsCost and 1 or 0; 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 + ["b"] = function(t) + return (t.parent and t.parent.b) or 1; 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); + ["mountID"] = function(t) + return cache.GetCachedField(t, "mountID", CacheInfo); end, - ["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 + ["lore"] = function(t) + return cache.GetCachedField(t, "lore", CacheInfo); 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 + ["displayID"] = function(t) + return cache.GetCachedField(t, "displayID", CacheInfo); 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 + ["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"); - -- 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 +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 - -- 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 +-- 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 - 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); + 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 + 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)(); - -- 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 +-- 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"]; + 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); +end - -- 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 - 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); - 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 "."); end - NestObjects(questObject, nonItemNested, true); 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)(); - -- 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 - - -- Resolve all symbolic links now that the quest contains items - FillSymLinks(questObject, true); +-- 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; + 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, - -- 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 + ["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 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"; + end, + ["indicatorIcon"] = function(t) + if app.CurrentVignettes["npcID"][t.npcID] then + return "Category_Secrets"; 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 + end, +}; +npcFields.icon = npcFields.iconAsDefault; +app.BaseNPC = app.BaseObjectFields(npcFields, "BaseNPC"); --- 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 +local fields = RawCloneData(npcFields); +fields.icon = npcFields.iconAsAchievement; +--fields.link = npcFields.linkAsAchievement; -- Go to Broken Shore -> Command Center -> +app.BaseNPCWithAchievement = app.BaseObjectFields(fields, "BaseNPCWithAchievement"); --- -- 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(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"); --- -- 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(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"); --- 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] = ", "; +-- 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 + 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 + elseif id > 1 then + return setmetatable(constructor(id, t, "npcID"), app.BaseNPC); + else + return setmetatable(constructor(id, t, "headerID"), app.BaseHeader); 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; +-- 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, + + ["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 - if warning then app:PlayRemoveSound(); end - end -end - -app:RegisterEvent("QUEST_SESSION_JOINED"); -end)(); + end, + ["lockedAsQuest"] = app.LockedAsQuest, + ["indicatorIcon"] = function(t) + if app.CurrentVignettes["objectID"][t.objectID] then + return "Category_Secrets"; + end + 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"); + -- 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 - 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, + ["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 + -- 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 + return unsavedCoords; + end, +}; +app.BaseObject = app.BaseObjectFields(objectFields, "BaseObject"); + +local fields = RawCloneData(objectFields); +fields.icon = objectFields.iconAsAchievement; +--fields.link = objectFields.linkAsAchievement; +app.BaseObjectWithAchievement = app.BaseObjectFields(fields, "BaseObjectWithAchievement"); + +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"); + +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); + end end end - if questDiff > 50 then - app.Settings:SetTooltipSetting("Report:CompletedQuests", oldReportSetting); - end + return setmetatable(constructor(id, t, "objectID"), app.BaseObject); end -local function RefreshQuestCompletionState(questID) - -- print("QuestRefresh",questID) - if questID then - CompletedQuests[questID] = true; - else - QueryCompletedQuests(); - end +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); +-- 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 -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); +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 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 +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() @@ -13343,14 +13317,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 @@ -13485,8 +13460,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 @@ -13497,9 +13473,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 @@ -13800,8 +13777,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 @@ -13942,20 +13920,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 @@ -14037,12 +14022,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 @@ -14314,35 +14299,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 @@ -14351,7 +14312,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) @@ -14378,7 +14339,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 @@ -14402,7 +14363,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 @@ -14413,30 +14374,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(); @@ -14602,7 +14565,6 @@ local function CreateMinimapButton() end app.CreateMinimapButton = CreateMinimapButton; - -- Panel Class Library (function() -- Shared Panel Functions @@ -15387,7 +15349,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(); @@ -15433,6 +15395,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; @@ -16055,7 +16028,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(); @@ -16621,7 +16594,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 @@ -16706,13 +16679,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 @@ -16726,7 +16699,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); @@ -16881,7 +16854,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 @@ -17856,7 +17828,7 @@ app._RefreshData = function() app:GetDataCache(); -- Refresh all Quests without callback - RefreshQuestCompletionState(); + app.QueryCompletedQuests(); -- Reapply custom collects app.RefreshCustomCollectibility(); @@ -18084,7 +18056,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..."); @@ -18633,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 @@ -20183,7 +20153,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?", @@ -20236,11 +20206,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, @@ -20281,7 +20251,7 @@ customWindowUpdates["Sync"] = function(self) })); end end - + if #data.g < 1 then table.insert(data.g, { ['text'] = "No characters found.", @@ -20293,11 +20263,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", @@ -20311,7 +20281,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 @@ -20346,7 +20316,7 @@ customWindowUpdates["Sync"] = function(self) }); end end - + if #data.g < 1 then table.insert(data.g, { ['text'] = "No linked accounts found.", @@ -20357,7 +20327,7 @@ customWindowUpdates["Sync"] = function(self) BuildGroups(data, data.g); return app.AlwaysShowUpdate(data); end, - ['visible'] = true, + ['visible'] = true, ['expanded'] = true, ['g'] = {}, }, @@ -20366,14 +20336,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); @@ -21779,6 +21749,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; @@ -22093,6 +22067,7 @@ app.OpenAuctionModule = function(self) end); end end +end)(); -- Creates the data structures and initial 'Default' profiles for ATT app.SetupProfiles = function() @@ -22480,7 +22455,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; 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, ...)