You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5205 lines
168 KiB

--------------------------------------------------------------------------------
-- A L L T H E T H I N G S --
--------------------------------------------------------------------------------
-- Copyright 2017-2024 Dylan Fortune (Crieve-Sargeras) --
--------------------------------------------------------------------------------
-- App locals
local appName, app = ...;
local L = app.L;
local AssignChildren, CloneClassInstance, GetRelativeValue = app.AssignChildren, app.CloneClassInstance, app.GetRelativeValue;
local IsQuestFlaggedCompleted, IsQuestFlaggedCompletedForObject = app.IsQuestFlaggedCompleted, app.IsQuestFlaggedCompletedForObject;
-- Abbreviations
L.ABBREVIATIONS[L.UNSORTED .. " %> " .. L.UNSORTED] = "|T" .. app.asset("WindowIcon_Unsorted") .. ":0|t " .. L.SHORTTITLE .. " %> " .. L.UNSORTED;
-- Binding Localizations
BINDING_HEADER_ALLTHETHINGS = L.TITLE
BINDING_NAME_ALLTHETHINGS_TOGGLEACCOUNTMODE = L.TOGGLE_ACCOUNT_MODE
BINDING_NAME_ALLTHETHINGS_TOGGLECOMPLETIONISTMODE = L.TOGGLE_COMPLETIONIST_MODE
BINDING_NAME_ALLTHETHINGS_TOGGLEDEBUGMODE = L.TOGGLE_DEBUG_MODE
BINDING_NAME_ALLTHETHINGS_TOGGLEFACTIONMODE = L.TOGGLE_FACTION_MODE
BINDING_NAME_ALLTHETHINGS_TOGGLELOOTMODE = L.TOGGLE_LOOT_MODE
BINDING_HEADER_ALLTHETHINGS_PREFERENCES = PREFERENCES
BINDING_NAME_ALLTHETHINGS_TOGGLECOMPLETEDTHINGS = L.TOGGLE_COMPLETEDTHINGS
BINDING_NAME_ALLTHETHINGS_TOGGLECOMPLETEDGROUPS = L.TOGGLE_COMPLETEDGROUPS
BINDING_NAME_ALLTHETHINGS_TOGGLECOLLECTEDTHINGS = L.TOGGLE_COLLECTEDTHINGS
BINDING_NAME_ALLTHETHINGS_TOGGLEBOEITEMS = L.TOGGLE_BOEITEMS
BINDING_NAME_ALLTHETHINGS_TOGGLESOURCETEXT = L.TOGGLE_SOURCETEXT
BINDING_HEADER_ALLTHETHINGS_MODULES = L.MODULES
BINDING_NAME_ALLTHETHINGS_TOGGLEMAINLIST = L.TOGGLE_MAINLIST
BINDING_NAME_ALLTHETHINGS_TOGGLEMINILIST = L.TOGGLE_MINILIST
BINDING_NAME_ALLTHETHINGS_TOGGLE_PROFESSION_LIST = L.TOGGLE_PROFESSION_LIST
BINDING_NAME_ALLTHETHINGS_TOGGLE_RAID_ASSISTANT = L.TOGGLE_RAID_ASSISTANT
BINDING_NAME_ALLTHETHINGS_TOGGLE_WORLD_QUESTS_LIST = L.TOGGLE_WORLD_QUESTS_LIST
BINDING_NAME_ALLTHETHINGS_TOGGLERANDOM = L.TOGGLE_RANDOM
BINDING_NAME_ALLTHETHINGS_REROLL_RANDOM = L.REROLL_RANDOM
-- Global API cache
-- While this may seem silly, caching references to commonly used APIs is actually a performance gain...
local C_DateAndTime_GetServerTimeLocal
= C_DateAndTime.GetServerTimeLocal;
local ipairs, pairs, rawset, rawget, pcall, tinsert, tremove, math_floor
= ipairs, pairs, rawset, rawget, pcall, tinsert, tremove, math.floor;
local C_Map_GetPlayerMapPosition = C_Map.GetPlayerMapPosition;
local GetAchievementInfo = _G["GetAchievementInfo"];
local GetAchievementNumCriteria = _G["GetAchievementNumCriteria"];
local GetAchievementCriteriaInfo = _G["GetAchievementCriteriaInfo"];
local GetAchievementCriteriaInfoByID = _G["GetAchievementCriteriaInfoByID"];
local GetCategoryInfo = _G["GetCategoryInfo"];
---@diagnostic disable-next-line: deprecated
local GetItemCount = _G["GetItemCount"];
local InCombatLockdown = _G["InCombatLockdown"];
local IsPlayerSpell, IsSpellKnown, IsSpellKnownOrOverridesKnown =
IsPlayerSpell, IsSpellKnown, IsSpellKnownOrOverridesKnown;
local C_QuestLog_IsOnQuest = C_QuestLog.IsOnQuest;
local HORDE_FACTION_ID = Enum.FlightPathFaction.Horde;
-- WoW API Cache
local GetItemInfo = app.WOWAPI.GetItemInfo;
local GetItemIcon = app.WOWAPI.GetItemIcon;
local GetItemInfoInstant = app.WOWAPI.GetItemInfoInstant;
local GetFactionCurrentReputation = app.WOWAPI.GetFactionCurrentReputation;
local GetSpellCooldown = app.WOWAPI.GetSpellCooldown;
local GetSpellLink = app.WOWAPI.GetSpellLink;
-- App & Module locals
local contains = app.contains;
local DESCRIPTION_SEPARATOR = app.DESCRIPTION_SEPARATOR;
local SearchForField, SearchForFieldContainer
= app.SearchForField, app.SearchForFieldContainer;
local IsRetrieving = app.Modules.RetrievingData.IsRetrieving;
local GetProgressColor = app.Modules.Color.GetProgressColor;
local GetProgressColorText = app.Modules.Color.GetProgressColorText;
local Colorize = app.Modules.Color.Colorize;
local ColorizeRGB = app.Modules.Color.ColorizeRGB;
local HexToARGB = app.Modules.Color.HexToARGB;
local RGBToHex = app.Modules.Color.RGBToHex;
-- WoW API Cache
local GetSpellName = app.WOWAPI.GetSpellName;
local GetSpellIcon = app.WOWAPI.GetSpellIcon;
-- Helper Functions
local pendingCollection, pendingRemovals, retrievingCollection, pendingCollectionCooldown = {},{},{},0;
local function PendingCollectionCoroutine()
while not app.IsReady do coroutine.yield(); end
while pendingCollectionCooldown > 0 do
pendingCollectionCooldown = pendingCollectionCooldown - 1;
coroutine.yield();
-- If any of the collection objects is retrieving data, try again.
local anyRetrieving = false;
for hash,thing in pairs(retrievingCollection) do
local retries = thing[1];
if retries > 0 then
retries = retries - 1;
thing[1] = retries;
if IsRetrieving(thing[2].text) then
retrievingCollection[hash] = nil;
anyRetrieving = true;
end
end
end
if anyRetrieving then
pendingCollectionCooldown = pendingCollectionCooldown + 1;
end
end
-- Report new things to your collection!
local any,allTypes = false,{};
local reportCollected = app.Settings:GetTooltipSetting("Report:Collected");
for hash,t in pairs(pendingCollection) do
local f = t.f;
if f then allTypes[f] = true; end
if reportCollected then
print((t.text or RETRIEVING_DATA) .. " was added to your collection!");
end
any = true;
end
if any then
wipe(pendingCollection);
-- Check if there was a mount.
if allTypes[app.FilterConstants.MOUNTS] then
app.Audio:PlayMountFanfare();
else
app.Audio:PlayFanfare();
end
end
-- Report removed things from your collection...
any = false;
for hash,t in pairs(pendingRemovals) do
if reportCollected then
print((t.text or RETRIEVING_DATA) .. " was removed from your collection!");
end
any = true;
end
if any then
wipe(pendingRemovals);
app.Audio:PlayRemoveSound();
end
end
local function AddToCollection(group)
if not group then return; end
local hash = group.hash;
if IsRetrieving(group.text) then
retrievingCollection[hash] = { 5, group };
end
-- Do not add the item to the pending list if it was already in it.
if pendingRemovals[hash] then
pendingRemovals[hash] = nil;
else
pendingCollection[hash] = group;
pendingCollectionCooldown = 10;
app:StartATTCoroutine("Pending Collection", PendingCollectionCoroutine);
end
end
local function RemoveFromCollection(group)
if not group then return; end
local hash = group.hash;
if IsRetrieving(group.text) then
retrievingCollection[hash] = { 5, group };
end
-- Do not add the item to the pending list if it was already in it.
if pendingCollection[hash] then
pendingCollection[hash] = nil;
else
pendingRemovals[hash] = group;
pendingCollectionCooldown = 10;
app:StartATTCoroutine("Pending Collection", PendingCollectionCoroutine);
end
end
-- Data Lib
local attData;
local AllTheThingsAD = {}; -- For account-wide data.
local function SetDataMember(member, data)
AllTheThingsAD[member] = data;
end
local function GetDataMember(member, default)
attData = AllTheThingsAD[member];
if attData == nil then
AllTheThingsAD[member] = default;
return default;
else
return attData;
end
end
local function GetDataSubMember(member, submember, default)
attData = AllTheThingsAD[member];
if attData then
attData = attData[submember];
if attData == nil then
AllTheThingsAD[member][submember] = default;
return default;
else
return attData;
end
else
AllTheThingsAD[member] = { [submember] = default };
return default;
end
end
app.SetDataMember = SetDataMember;
app.GetDataMember = GetDataMember;
app.GetDataSubMember = GetDataSubMember;
-- Color Lib
local function GetCollectionIcon(state)
return L[(state and (state == 2 and "COLLECTED_APPEARANCE_ICON" or "COLLECTED_ICON")) or "NOT_COLLECTED_ICON"];
end
local function GetCollectionText(state)
return L[(state and (state == 2 and "COLLECTED_APPEARANCE" or "COLLECTED")) or "NOT_COLLECTED"];
end
local function GetCompletionIcon(state)
return L[state and "COMPLETE_ICON" or "NOT_COLLECTED_ICON"];
end
local function GetCompletionText(state)
return L[(state == 2 and "COMPLETE_OTHER") or ((state == 1 or state == true) and "COMPLETE") or "INCOMPLETE"];
end
local function GetProgressTextForRow(data)
local total = data.total;
if total and (total > 1 or (total > 0 and not data.collectible)) then
return GetProgressColorText(data.progress or 0, total);
elseif data.collectible then
return GetCollectionIcon(data.collected);
elseif data.trackable then
return GetCompletionIcon(data.saved);
end
end
local function GetProgressTextForTooltip(data)
local iconOnly = app.Settings:GetTooltipSetting("ShowIconOnly");
if iconOnly then return GetProgressTextForRow(data); end
if data.total and (data.total > 1 or (data.total > 0 and not data.collectible)) then
return GetProgressColorText(data.progress or 0, data.total);
elseif data.collectible or (data.spellID and data.itemID and data.trackable) then
return GetCollectionText(data.collected);
elseif data.trackable then
return GetCompletionText(data.saved);
end
end
app.GetCollectionIcon = GetCollectionIcon;
app.GetCollectionText = GetCollectionText;
app.GetCompletionIcon = GetCompletionIcon;
app.GetCompletionText = GetCompletionText;
app.GetProgressTextForRow = GetProgressTextForRow;
app.GetProgressTextForTooltip = GetProgressTextForTooltip;
local function GetUnobtainableTexture(group)
if not group then return; end
if type(group) ~= "table" then
-- This function shouldn't be used with only u anymore!
app.print("Invalid use of GetUnobtainableTexture", group);
return;
end
-- Determine the texture color, default is green for events.
-- TODO: Use 4 for inactive events, use 5 for active events
local filter, u = 4, group.u;
if u then
-- only b = 0 (BoE), not BoA/BoP
-- removed, elite, bmah, tcg, summon
if u > 1 and u < 12 and (group.b or 0) == 0 then
filter = 2;
else
local record = L["AVAILABILITY_CONDITIONS"][u];
if record then
if not record[5] or app.GameBuildVersion < record[5] then
filter = record[1] or 0;
else
-- This is a phase that's available. No icon.
return;
end
else
-- otherwise it's an invalid unobtainable filter
app.print("Invalid Unobtainable Filter:",u);
return;
end
end
return L["UNOBTAINABLE_ITEM_TEXTURES"][filter];
end
if group.e then
return L["UNOBTAINABLE_ITEM_TEXTURES"][app.Modules.Events.FilterIsEventActive(group) and 5 or 4];
end
end
app.GetUnobtainableTexture = GetUnobtainableTexture;
app.IsComplete = function(o)
if o.total then return o.total == o.progress; end
if o.collectible then return o.collected; end
if o.trackable then return o.saved; end
end
local function GetDisplayID(data)
if data.displayID then
return data.displayID;
elseif data.creatureID then
local displayID = app.NPCDisplayIDFromID[data.creatureID];
if displayID then
return displayID;
end
end
if data.providers and #data.providers > 0 then
for k,v in pairs(data.providers) do
-- if one of the providers is an NPC, we should show its texture regardless of other providers
if v[1] == "n" then
return app.NPCDisplayIDFromID[v[2]];
end
end
end
if data.qgs and #data.qgs > 0 then
return app.NPCDisplayIDFromID[data.qgs[1]];
end
end
local function GetIconFromProviders(group)
if group.providers then
local icon;
for k,v in ipairs(group.providers) do
if v[2] > 0 then
if v[1] == "o" then
icon = app.ObjectIcons[v[2]];
elseif v[1] == "i" then
icon = GetItemIcon(v[2]);
end
if icon then return icon; end
end
end
end
end
local function GetNameFromProviders(group)
if group.providers then
local name;
for k,v in ipairs(group.providers) do
if v[2] > 0 then
if v[1] == "o" then
name = app.ObjectNames[v[2]];
elseif v[1] == "i" then
name = GetItemInfo(v[2]);
elseif v[1] == "n" then
name = app.NPCNameFromID[v[2]];
end
if name then return name; end
end
end
end
end
app.GetIconFromProviders = GetIconFromProviders;
app.GetNameFromProviders = GetNameFromProviders;
local MergeObject;
local CloneArray = app.CloneArray;
local function GetHash(t)
local hash = t.hash;
if hash then return hash; end
hash = app.CreateHash(t);
--app.PrintDebug("No hash for object:", hash, t.text);
return hash;
end
local function MergeObjects(g, g2)
for i,o in ipairs(g2) do
MergeObject(g, o);
end
end
MergeObject = function(g, t, index)
local hash = GetHash(t);
for i,o in ipairs(g) do
if GetHash(o) == hash then
if t.g then
local tg = t.g;
t.g = nil;
if o.g then
MergeObjects(o.g, tg);
else
o.g = tg;
end
end
for k,v in pairs(t) do
if k == "races" or k == "c" then
local c = rawget(o, k);
if not c then
c = CloneArray(v);
rawset(o, k, c);
else
for _,p in ipairs(v) do
if not contains(c, p) then
tinsert(c, p);
end
end
end
elseif k == "r" then
if o[k] and o[k] ~= v then
rawset(o, k, nil);
else
rawset(o, k, v);
end
elseif k ~= "expanded" then
rawset(o, k, v);
end
end
rawset(o, "nmr", (o.races and not contains(o.races, app.RaceIndex)) or (o.r and o.r ~= app.FactionID));
rawset(o, "nmc", o.c and not contains(o.c, app.ClassIndex));
return o;
end
end
if index then
tinsert(g, index, t);
else
tinsert(g, t);
end
rawset(t, "nmr", (t.races and not contains(t.races, app.RaceIndex)) or (t.r and t.r ~= app.FactionID));
rawset(t, "nmc", t.c and not contains(t.c, app.ClassIndex));
return t;
end
local function MergeClone(g, o)
local clone = CloneClassInstance(o);
local u = GetRelativeValue(o, "u");
if u then clone.u = u; end
local e = GetRelativeValue(o, "e");
if e then clone.e = e; end
local lvl = GetRelativeValue(o, "lvl");
if lvl then clone.lvl = lvl; end
if not o.itemID or o.b == 1 then
local races = o.races;
if races then
clone.races = CloneArray(races);
else
local r = GetRelativeValue(o, "r");
if r then
clone.r = r;
clone.races = nil;
else
races = GetRelativeValue(o, "races");
if races then clone.races = CloneArray(races); end
end
end
local c = GetRelativeValue(o, "c");
if c then clone.c = CloneArray(c); end
end
return MergeObject(g, clone);
end
app.MergeClone = MergeClone;
app.MergeObject = MergeObject;
app.MergeObjects = MergeObjects;
local ResolveSymbolicLink;
(function()
local subroutines;
subroutines = {
["common_recipes_vendor"] = function(npcID)
return {
{"select", "creatureID", npcID}, -- Main Vendor
{"pop"}, -- Remove Main Vendor and push his children into the processing queue.
{"is", "itemID"}, -- Only Items
{"exclude", "itemID",
-- Borya <Tailoring Supplies> Cataclysm Tailoring
6270, -- Pattern: Blue Linen Vest
6274, -- Pattern: Blue Overalls
10314, -- Pattern: Lavender Mageweave Shirt
10317, -- Pattern: Pink Mageweave Shirt
5772, -- Pattern: Red Woolen Bag
-- Sumi <Blacksmithing Supplies> Cataclysm Blacksmithing
12162, -- Plans: Hardened Iron Shortsword
-- Tamar <Leatherworking Supplies> Cataclysm Leatherworking
18731, -- Pattern: Heavy Leather Ball
-- Kithas <Enchanting Supplies> Cataclysm Enchanting
6349, -- Formula: Enchant 2H Weapon - Lesser Intellect
20753, -- Formula: Lesser Wizard Oil
20752, -- Formula: Minor Mana Oil
20758, -- Formula: Minor Wizard Oil
22307, -- Pattern: Enchanted Mageweave Pouch
-- Marith Lazuria <Jewelcrafting Supplies> Cataclysm Jewelcrafting
-- Shazdar <Sous Chef> Cataclysm Cooking
-- Tiffany Cartier <Jewelcrafting Supplies> Northrend Jewelcrafting
-- Timothy Jones <Jewelcrafting Trainer> Northrend Jewelcrafting
},
};
end,
["common_vendor"] = function(npcID)
return {
{"select", "creatureID", npcID}, -- Main Vendor
{"pop"}, -- Remove Main Vendor and push his children into the processing queue.
{"is", "itemID"}, -- Only Items
};
end,
["pvp_gear_base"] = function(expansionID, headerID1, headerID2)
local b = {
{ "select", "expansionID", expansionID }, -- Select the Expansion header
{ "pop" }, -- Discard the Expansion header and acquire the children.
{ "where", "headerID", headerID1 }, -- Select the Season header
};
if headerID2 then
tinsert(b, { "pop" }); -- Discard the Season header and acquire the children.
tinsert(b, { "where", "headerID", headerID2 }); -- Select the Set header
end
return b;
end,
};
ResolveSymbolicLink = function(o)
if o.resolved then return o.resolved; end
if o and o.sym then
local searchResults, finalized = {}, {};
for j,sym in ipairs(o.sym) do
local cmd = sym[1];
if cmd == "select" then
-- Instruction to search the full database for multiple of a given type
local field = sym[2];
local cache;
for i=3,#sym do
local cache = SearchForField(field, sym[i]);
if #cache > 0 then
for k,result in ipairs(cache) do
local ref = ResolveSymbolicLink(result);
if ref then
if result.g then
for _,m in ipairs(result.g) do
tinsert(searchResults, m);
end
end
for _,m in ipairs(ref) do
tinsert(searchResults, m);
end
else
tinsert(searchResults, result);
end
end
elseif field == "itemID" then
tinsert(searchResults, app.CreateItem(sym[i], {
description = "This was dynamically filled using a symlink, but the information wasn't found in the addon.",
}));
else
print(app.GenerateSourceHash(o));
print("Failed to select ", field, sym[i]);
end
end
elseif cmd == "selectparent" then
-- Instruction to select the parent object of the parent that owns the symbolic link.
local cache = sym[2];
if cache and cache > 0 then
local parent = o.parent;
while cache > 1 do
parent = parent.parent;
cache = cache - 1;
end
if parent then
tinsert(searchResults, parent);
else
print("Failed to select parent " .. sym[2] .. " levels up.");
end
else
-- Select the direct parent object.
tinsert(searchResults, o.parent);
end
elseif cmd == "selectprofession" then
local requireSkill, response = sym[2], nil;
if app.Categories.Achievements then
response = app:BuildSearchResponse(app.Categories.Achievements, "requireSkill", requireSkill);
if response then tinsert(searchResults, {text=ACHIEVEMENTS,icon = app.asset("Category_Achievements"),g=response}); end
end
response = app:BuildSearchResponse(app.Categories.Instances, "requireSkill", requireSkill);
if response then tinsert(searchResults, {text=GROUP_FINDER,icon = app.asset("Category_D&R"),g=response}); end
response = app:BuildSearchResponse(app.Categories.Zones, "requireSkill", requireSkill);
if response then tinsert(searchResults, {text=BUG_CATEGORY2,icon = app.asset("Category_Zones"),g=response}); end
response = app:BuildSearchResponse(app.Categories.WorldDrops, "requireSkill", requireSkill);
if response then tinsert(searchResults, {text=TRANSMOG_SOURCE_4,icon = app.asset("Category_WorldDrops"),g=response}); end
response = app:BuildSearchResponse(app.Categories.Craftables, "requireSkill", requireSkill);
if response then tinsert(searchResults, {text=LOOT_JOURNAL_LEGENDARIES_SOURCE_CRAFTED_ITEM,icon = app.asset("Category_Crafting"),g=response}); end
response = app:BuildSearchResponse(app.Categories.Holidays, "requireSkill", requireSkill);
if response then tinsert(searchResults, app.CreateNPC(app.HeaderConstants.HOLIDAYS, response)); end
response = app:BuildSearchResponse(app.Categories.WorldEvents, "requireSkill", requireSkill);
if response then tinsert(searchResults, {text=BATTLE_PET_SOURCE_7,icon = app.asset("Category_Event"),g=response}); end
elseif cmd == "fill" then
-- Instruction to fill with identical content cached elsewhere for this group
local cache = SearchForField(o.key, o[o.key]);
if #cache > 0 then
o.symbolizing = true;
for k,result in ipairs(cache) do
if not result.symbolizing then
local ref = ResolveSymbolicLink(result);
if ref then
if result.g then
for i,m in ipairs(result.g) do
tinsert(searchResults, m);
end
end
for i,m in ipairs(ref) do
tinsert(searchResults, m);
end
else
tinsert(searchResults, result);
end
end
end
o.symbolizing = nil;
else
print("Failed to select ", sym[2], sym[3]);
end
elseif cmd == "pop" then
-- Instruction to "pop" all of the group values up one level.
local orig = searchResults;
searchResults = {};
for k,result in ipairs(orig) do
if result.g then
for l,t in ipairs(result.g) do
tinsert(searchResults, t);
end
end
end
elseif cmd == "where" then
-- Instruction to include only search results where a key value is a value
local key, value = sym[2], sym[3];
for k=#searchResults,1,-1 do
local result = searchResults[k];
if not result[key] or result[key] ~= value then
tremove(searchResults, k);
end
end
elseif cmd == "index" then
-- Instruction to include the search result with a given index within each of the selection's groups.
local index = sym[2];
local orig = searchResults;
searchResults = {};
for k=#orig,1,-1 do
local result = orig[k];
if result.g and index <= #result.g then
tinsert(searchResults, result.g[index]);
end
end
elseif cmd == "not" then
-- Instruction to include only search results where a key value is not a value
if #sym > 3 then
local dict = {};
for k=2,#sym,2 do
dict[sym[k] ] = sym[k + 1];
end
for k=#searchResults,1,-1 do
local result = searchResults[k];
local matched = true;
for key,value in pairs(dict) do
if not result[key] or result[key] ~= value then
matched = false;
break;
end
end
if matched then
tremove(searchResults, k);
end
end
else
local key, value = sym[2], sym[3];
for k=#searchResults,1,-1 do
local result = searchResults[k];
if result[key] and result[key] == value then
tremove(searchResults, k);
end
end
end
elseif cmd == "is" then
-- Instruction to include only search results where a key exists
local key = sym[2];
for k=#searchResults,1,-1 do
if not searchResults[k][key] then tremove(searchResults, k); end
end
elseif cmd == "isnt" then
-- Instruction to include only search results where a key doesn't exist
local key = sym[2];
for k=#searchResults,1,-1 do
if searchResults[k][key] then tremove(searchResults, k); end
end
elseif cmd == "contains" then
-- Instruction to include only search results where a key value contains a value.
local key = sym[2];
local clone = {unpack(sym)};
tremove(clone, 1);
tremove(clone, 1);
if #clone > 0 then
for k=#searchResults,1,-1 do
local result = searchResults[k];
if not result[key] or not contains(clone, result[key]) then
tremove(searchResults, k);
end
end
end
elseif cmd == "exclude" then
-- Instruction to exclude search results where a key value contains a value.
local key = sym[2];
local clone = {unpack(sym)};
tremove(clone, 1);
tremove(clone, 1);
if #clone > 0 then
for k=#searchResults,1,-1 do
local result = searchResults[k];
if result[key] and contains(clone, result[key]) then
tremove(searchResults, k);
end
end
end
elseif cmd == "finalize" then
-- Instruction to finalize the current search results and prevent additional queries from affecting this selection.
for k,result in ipairs(searchResults) do
tinsert(finalized, result);
end
wipe(searchResults);
elseif cmd == "merge" then
-- Instruction to take all of the finalized and non-finalized search results and merge them back in to the processing queue.
for k,result in ipairs(searchResults) do
tinsert(finalized, result);
end
searchResults = finalized;
finalized = {};
elseif cmd == "postprocess" then
-- Instruction to take all of the current search results and ensure that there are no duplicated primary keys.
local uniques = {};
MergeObjects(uniques, searchResults);
searchResults = uniques;
elseif cmd == "invtype" then
-- Instruction to include only search results where an item is of a specific inventory type.
local types = {unpack(sym)};
tremove(types, 1);
if #types > 0 then
for k=#searchResults,1,-1 do
local result = searchResults[k];
if result.itemID and not contains(types, select(4, GetItemInfoInstant(result.itemID))) then
tremove(searchResults, k);
end
end
end
elseif cmd == "sub" then
local subroutine = subroutines[sym[2]];
if subroutine then
local args = {unpack(sym)};
tremove(args, 1);
tremove(args, 1);
local commands = subroutine(unpack(args));
if commands then
local results = ResolveSymbolicLink(setmetatable({sym=commands}, {__index=o}));
if results then
for k,result in ipairs(results) do
tinsert(searchResults, result);
end
end
end
else
print("Could not find subroutine", sym[2]);
end
elseif cmd == "subif" then
-- Instruction to perform a set of commands if a conditional is returned true.
local subroutine = subroutines[sym[2]];
if subroutine then
-- If the subroutine's conditional was successful.
if sym[3] and (sym[3])(o) then
local args = {unpack(sym)};
tremove(args, 1);
tremove(args, 1);
tremove(args, 1);
local commands = subroutine(unpack(args));
if commands then
local results = ResolveSymbolicLink(setmetatable({sym=commands}, {__index=o}));
if results then
for k,result in ipairs(results) do
tinsert(searchResults, result);
end
end
end
end
else
print("Could not find subroutine", sym[2]);
end
elseif cmd == "achievement_criteria" then
-- Instruction to select the criteria provided by the achievement this is attached to. (maybe build this into achievements?)
if GetAchievementNumCriteria then
local achievementID = o.achievementID;
local num = GetAchievementNumCriteria(achievementID);
if type(num) ~= "number" then
--print("Attempting to use 'achievement_criteria' with achievement", achievementID);
return;
end
local cache;
for criteriaID=1,num,1 do
---@diagnostic disable-next-line: redundant-parameter
local _, criteriaType, _, _, _, _, _, assetID, _, uniqueID = GetAchievementCriteriaInfo(achievementID, criteriaID, true);
if not uniqueID or uniqueID <= 0 then uniqueID = criteriaID; end
local criteriaObject = app.CreateAchievementCriteria(uniqueID);
if criteriaObject then
criteriaObject.achievementID = achievementID;
if criteriaType == 27 then
cache = SearchForField("questID", assetID);
elseif criteriaType == 36 or criteriaType == 42 then
criteriaObject.providers = {{ "i", assetID }};
elseif criteriaType == 110 or criteriaType == 29 or criteriaType == 69 or criteriaType == 52 or criteriaType == 53 or criteriaType == 54 or criteriaType == 32 then
-- Ignored
else
--print("Unhandled Criteria Type", criteriaType, assetID);
end
if cache and #cache > 0 then
local uniques = {};
MergeObjects(uniques, cache);
for i,p in ipairs(uniques) do
rawset(p, "text", nil);
for key,value in pairs(p) do
criteriaObject[key] = value;
end
end
end
criteriaObject.parent = o;
tinsert(searchResults, criteriaObject);
end
end
end
elseif cmd == "meta_achievement" then
-- Instruction to search the full database for multiple achievementID's
local cache;
for i=2,#sym do
local cache = SearchForField("achievementID", sym[i]);
if #cache > 0 then
for k,result in ipairs(cache) do
local ref = ResolveSymbolicLink(result);
if ref then
local cs = app.CloneReference(result);
if not cs.g then cs.g = {}; end
for i,m in ipairs(ref) do
tinsert(cs.g, m);
end
tinsert(searchResults, cs);
else
tinsert(searchResults, result);
end
end
else
print("Failed to select achievementID", sym[i]);
end
end
-- Remove any Criteria groups associated with those achievements
for k=#searchResults,1,-1 do
local result = searchResults[k];
if result.criteriaID then tremove(searchResults, k); end
end
end
end
-- If we have any pending finalizations to make, then merge them into the finalized table. [Equivalent to a "finalize" instruction]
if #searchResults > 0 then
for k,result in ipairs(searchResults) do
tinsert(finalized, result);
end
end
-- If we had any finalized search results, then return it.
if #finalized > 0 then
-- print("Symbolic Link for ", o.key, " ", o[o.key], " contains ", #finalized, " values after filtering.");
o.resolved = finalized;
return finalized;
else
-- print("Symbolic Link for ", o.key, " ", o[o.key], " contained no values after filtering.");
end
end
end
end)();
app.ResolveSymbolicLink = ResolveSymbolicLink;
local function BuildContainsInfo(groups, entries, paramA, paramB, indent, layer)
for i,group in ipairs(groups) do
if app.RecursiveGroupRequirementsFilter(group) then
local right = nil;
if group.total and (group.total > 1 or (group.total > 0 and not group.collectible)) then
if (group.progress / group.total) < 1 or app.Settings:Get("Show:CompletedGroups") then
right = GetProgressColorText(group.progress, group.total);
end
elseif paramA and paramB and (not group[paramA] or (group[paramA] and group[paramA] ~= paramB)) then
if group.collectible then
if group.collected then
if app.Settings:Get("Show:CollectedThings") then
right = GetCollectionIcon(group.collected);
end
else
right = L["NOT_COLLECTED_ICON"];
end
elseif group.trackable then
if app.Settings:Get("Show:TrackableThings") then
if group.saved then
if app.Settings:Get("Show:CollectedThings") then
right = L["COMPLETE_ICON"];
end
else
right = L["NOT_COLLECTED_ICON"];
end
elseif group.visible then
right = group.count and (group.count .. "x") or "---";
end
elseif group.visible then
right = group.count and (group.count .. "x") or "---";
end
end
if right then
-- Insert into the display.
local o = { prefix = indent, group = group, right = right };
if group.u then
local condition = L["AVAILABILITY_CONDITIONS"][group.u];
if condition and (not condition[5] or app.GameBuildVersion < condition[5]) then
o.texture = L["UNOBTAINABLE_ITEM_TEXTURES"][condition[1]];
end
elseif group.e then
o.texture = L["UNOBTAINABLE_ITEM_TEXTURES"][4];
end
if o.texture then
o.prefix = o.prefix:sub(4) .. "|T" .. o.texture .. ":0|t ";
o.texture = nil;
end
tinsert(entries, o);
-- Only go down one more level.
if layer < 2 and group.g and #group.g > 0 and not group.symbolized then
BuildContainsInfo(group.g, entries, paramA, paramB, indent .. " ", layer + 1);
end
end
end
end
end
local function BuildReagentInfo(groups, entries, paramA, paramB, indent, layer)
for i,group in ipairs(groups) do
if app.RecursiveGroupRequirementsFilter(group) then
local o = { prefix = indent, group = group };
if group.u then
local condition = L["AVAILABILITY_CONDITIONS"][group.u];
if condition and (not condition[5] or app.GameBuildVersion < condition[5]) then
o.texture = L["UNOBTAINABLE_ITEM_TEXTURES"][condition[1]];
end
elseif group.e then
o.texture = L["UNOBTAINABLE_ITEM_TEXTURES"][4];
end
if o.texture then
o.prefix = o.prefix:sub(4) .. "|T" .. o.texture .. ":0|t ";
o.texture = nil;
end
if group.count then
o.right = group.count .. "x";
end
tinsert(entries, o);
end
end
end
-- Search Caching
local searchCache, working = {}, nil;
app.GetCachedData = function(cacheKey, method, ...)
if IsRetrieving(cacheKey) then return; end
local cache = searchCache[cacheKey];
if not cache then
cache, working = method(...);
if not working then
-- Only cache if the tooltip if no additional work is needed.
searchCache[cacheKey] = cache;
end
return cache, working;
end
return cache;
end
app.WipeSearchCache = function()
wipe(searchCache);
end
app.AddEventHandler("OnRefreshComplete", app.WipeSearchCache);
local InitialCachedSearch;
local IsQuestReadyForTurnIn = app.IsQuestReadyForTurnIn;
local SourceLocationSettingsKey = setmetatable({
creatureID = "SourceLocations:Creatures",
}, {
__index = function(t, key)
return "SourceLocations:Things";
end
});
local UnobtainableTexture = "|T" .. app.asset("status-unobtainable.blp") .. ":0|t";
local function HasCost(group, idType, id)
-- check if the group has a cost which includes the given parameters
if group.cost and type(group.cost) == "table" then
if idType == "itemID" then
for i,c in ipairs(group.cost) do
if c[2] == id and c[1] == "i" then
return true;
end
end
elseif idType == "currencyID" then
for i,c in ipairs(group.cost) do
if c[2] == id and c[1] == "c" then
return true;
end
end
end
end
return false;
end
local function GetRelativeDifficulty(group, difficultyID)
if group then
if group.difficultyID then
if group.difficultyID == difficultyID then
return true;
end
if group.difficulties then
for i, difficulty in ipairs(group.difficulties) do
if difficulty == difficultyID then
return true;
end
end
end
return false;
end
if group.parent then
return GetRelativeDifficulty(group.sourceParent or group.parent, difficultyID);
else
return true;
end
end
end
local function SortByCommonBossDrops(a, b)
return not (a.headerID and a.headerID == app.HeaderConstants.COMMON_BOSS_DROPS) and b.headerID and b.headerID == app.HeaderConstants.COMMON_BOSS_DROPS;
end
local function SortByCraftTypeID(a, b)
local craftTypeA = a.craftTypeID or 0;
local craftTypeB = b.craftTypeID or 0;
if craftTypeA == craftTypeB then
return (a.name or RETRIEVING_DATA) < (b.name or RETRIEVING_DATA);
end
return craftTypeA > craftTypeB;
end
local function AddSourceLinesForTooltip(tooltipInfo, paramA, paramB, group)
if group and app.Settings:GetTooltipSetting("SourceLocations") and (not paramA or app.Settings:GetTooltipSetting(SourceLocationSettingsKey[paramA])) then
--print("SourceLocations", paramA, paramB);
local temp, text, parent = {}, nil, nil;
local unfiltered, right = {}, nil;
local showUnsorted = app.Settings:GetTooltipSetting("SourceLocations:Unsorted");
local showCompleted = app.Settings:GetTooltipSetting("SourceLocations:Completed");
local wrap = app.Settings:GetTooltipSetting("SourceLocations:Wrapping");
local FilterUnobtainable, FilterCharacter, FirstParent
= app.RecursiveUnobtainableFilter, app.RecursiveCharacterRequirementsFilter, app.GetRelativeGroup
local abbrevs = L["ABBREVIATIONS"];
-- Include Cost Sources
local sourceGroups = group;
if #sourceGroups == 0 and (paramA == "itemID" or paramA == "currencyID") then
local costGroups = SearchForField(paramA .. "AsCost", paramB);
if costGroups and #costGroups > 0 then
local regroup = {};
for i,g in ipairs(sourceGroups) do
tinsert(regroup, g);
end
for i,g in ipairs(costGroups) do
tinsert(regroup, g);
end
sourceGroups = regroup;
end
end
for _,j in ipairs(sourceGroups) do
parent = j.parent;
if parent and not FirstParent(j, "hideText") and parent.parent
and (showCompleted or not app.IsComplete(j))
and not HasCost(j, paramA, paramB)
then
text = app.GenerateSourcePathForTooltip(parent);
if showUnsorted or (not text:match(L.UNSORTED) and not text:match(L.HIDDEN_QUEST_TRIGGERS)) then
for source,replacement in pairs(abbrevs) do
text = text:gsub(source, replacement);
end
-- doesn't meet current unobtainable filters
if not FilterUnobtainable(parent) then
tinsert(unfiltered, { text, UnobtainableTexture });
-- from obtainable, different character source
elseif not FilterCharacter(parent) then
tinsert(unfiltered, { text, "|TInterface\\FriendsFrame\\StatusIcon-Away:0|t" });
else
-- check if this needs an unobtainable icon even though it's being shown
right = GetUnobtainableTexture(FirstParent(parent, "e") or FirstParent(parent, "u") or j) or (j.rwp and app.asset("status-prerequisites"));
tinsert(temp, { text, right and ("|T" .. right .. ":0|t") });
end
end
end
end
-- if in Debug or no sources visible, add any unfiltered sources
if app.MODE_DEBUG or (#temp < 1 and not (paramA == "creatureID" or paramA == "encounterID")) then
for _,data in ipairs(unfiltered) do
tinsert(temp, data);
end
end
if #temp > 0 then
local listing = {};
local maximum = app.Settings:GetTooltipSetting("Locations");
local count = 0;
app.Sort(temp, app.SortDefaults.IndexOneStrings);
for _,data in ipairs(temp) do
text = data[1];
right = data[2];
if right and right:len() > 0 then
text = text .. " " .. right;
end
if not contains(listing, text) then
count = count + 1;
if count <= maximum then
tinsert(listing, text);
end
end
end
if count > maximum then
tinsert(listing, (L.AND_OTHER_SOURCES):format(count - maximum));
end
if #listing > 0 then
tooltipInfo.hasSourceLocations = true;
for _,text in ipairs(listing) do
if not working and IsRetrieving(text) then working = true; end
local left, right = DESCRIPTION_SEPARATOR:split(text);
tinsert(tooltipInfo, { left = left, right = right, wrap = wrap });
end
end
end
end
end
app.Settings.CreateInformationType("SourceLocations", {
priority = 2.7,
text = "Source Locations",
HideCheckBox = true,
keys = {
["autoID"] = false,
["creatureID"] = true,
["expansionID"] = false,
["explorationID"] = true,
["factionID"] = true,
["flightPathID"] = true,
["headerID"] = false,
["itemID"] = true,
["speciesID"] = true,
["titleID"] = true,
},
Process = function(t, data, tooltipInfo)
local key, id = data.key, data[data.key];
if key and id and t.keys[key] then
if tooltipInfo.hasSourceLocations then return; end
AddSourceLinesForTooltip(tooltipInfo, key, id, app.SearchForField(key, id));
end
end
})
---@param method function
---@param paramA string
---@param paramB number
local function GetSearchResults(method, paramA, paramB, ...)
-- app.PrintDebug("GetSearchResults",method,paramA,paramB,...)
if not method then
print("GetSearchResults: Invalid method: nil");
return nil, true;
end
if not paramA then
print("GetSearchResults: Invalid paramA: nil");
return nil, true;
end
-- If we are searching for only one parameter, it is a raw link.
local rawlink;
if paramB then paramB = tonumber(paramB) or paramB;
else rawlink = paramA; end
-- This method can be called nested, and some logic should only process for the initial call
local isTopLevelSearch;
if not InitialCachedSearch then
InitialCachedSearch = true;
isTopLevelSearch = true;
end
-- Determine if this tooltip needs more work the next time it refreshes.
local working, tooltipInfo, mostAccessibleSource = false, {}, nil;
-- Call to the method to search the database.
local group, a, b = method(paramA, paramB);
if group then
if a then paramA = a; end
if b then paramB = b; end
-- Move all post processing here?
if paramA == "creatureID" or paramA == "encounterID" then
local difficultyID = app.GetCurrentDifficultyID();
if difficultyID > 0 then
local subgroup = {};
for _,j in ipairs(group) do
if GetRelativeDifficulty(j, difficultyID) then
tinsert(subgroup, j);
end
end
group = subgroup;
end
local regroup = {};
if app.MODE_DEBUG then
for i,j in ipairs(group) do
tinsert(regroup, j);
end
else
if app.MODE_ACCOUNT then
for i,j in ipairs(group) do
if app.RecursiveUnobtainableFilter(j) then
if j.questID and j.itemID then
if not j.saved then
-- Only show the item on the tooltip if the quest is active and incomplete or the item is a provider.
if C_QuestLog_IsOnQuest(j.questID) then
if not IsQuestReadyForTurnIn(j.questID) then
tinsert(regroup, j);
end
elseif j.providers then
tinsert(regroup, j);
else
-- Do a quick search on the itemID.
local searchResults = SearchForField("itemID", j.itemID);
if #searchResults > 0 then
for k,searchResult in ipairs(searchResults) do
if searchResult.providers then
for l,provider in ipairs(searchResult.providers) do
if provider[1] == 'i' and provider[2] == j.itemID then
tinsert(regroup, j);
break;
end
end
break;
end
end
end
end
end
else
tinsert(regroup, j);
end
end
end
else
for i,j in ipairs(group) do
if app.RecursiveCharacterRequirementsFilter(j) and app.RecursiveUnobtainableFilter(j) and app.RecursiveGroupRequirementsFilter(j) then
if j.questID and j.itemID then
if not j.saved then
-- Only show the item on the tooltip if the quest is active and incomplete or the item is a provider.
if C_QuestLog_IsOnQuest(j.questID) then
if not IsQuestReadyForTurnIn(j.questID) then
tinsert(regroup, j);
end
elseif j.providers then
tinsert(regroup, j);
else
-- Do a quick search on the itemID.
local searchResults = SearchForField("itemID", j.itemID);
if #searchResults > 0 then
for k,searchResult in ipairs(searchResults) do
if searchResult.providers then
for l,provider in ipairs(searchResult.providers) do
if provider[1] == 'i' and provider[2] == j.itemID then
tinsert(regroup, j);
break;
end
end
break;
end
end
end
end
end
else
tinsert(regroup, j);
end
end
end
end
end
if #regroup > 0 then
app.Sort(regroup, SortByCommonBossDrops);
end
group = regroup;
elseif paramA == "titleID" or paramA == "followerID" then
-- Don't do anything
local regroup = {};
if app.MODE_ACCOUNT then
for i,j in ipairs(group) do
if app.RecursiveUnobtainableFilter(j) then
tinsert(regroup, setmetatable({["g"] = {}}, { __index = j }));
end
end
else
for i,j in ipairs(group) do
if app.RecursiveCharacterRequirementsFilter(j) and app.RecursiveUnobtainableFilter(j) and app.RecursiveGroupRequirementsFilter(j) then
tinsert(regroup, setmetatable({["g"] = {}}, { __index = j }));
end
end
end
group = regroup;
end
else
group = {};
end
group.working = nil;
-- For Creatures that are inside of an instance, we only want the data relevant for the instance.
-- Determine if this is a search for an item
local itemID, itemString;
if rawlink then
---@diagnostic disable-next-line: undefined-field
itemString = rawlink:match("item[%-?%d:]+");
if itemString then
---@diagnostic disable-next-line: undefined-field
local itemID2 = select(2, (":"):split(itemString));
if itemID2 then
itemID = tonumber(itemID2) or itemID2;
paramA = "itemID";
paramB = itemID;
end
if not itemID or itemID == 0 then return nil, true; end
else
---@diagnostic disable-next-line: undefined-field
local kind, id = (":"):split(rawlink);
if id == "" then return nil, true; end
kind = kind:lower();
if id then id = tonumber(id) or id; end
if kind == "itemid" then
paramA = "itemID";
paramB = id;
itemID = id;
elseif kind == "questid" then
paramA = "questID";
paramB = id;
elseif kind == "creatureid" or kind == "npcid" then
paramA = "creatureID";
paramB = id;
end
end
elseif paramA == "itemID" then
itemID = paramB;
end
-- Find the most accessible version of the thing we're looking for.
if paramA == "spellID" and not itemID then
-- We want spells to have higher preference for the spell itself rather than the recipe.
for i,j in ipairs(group) do
if j.itemID then j.AccessibilityScore = j.AccessibilityScore + 100; end
end
end
app.Sort(group, app.SortDefaults.Accessibility);
--[[
for i,j in ipairs(group) do
print(i, j.key, j[j.key], j.text, j.AccessibilityScore);
end
]]--
for i,j in ipairs(group) do
if j[paramA] == paramB then
mostAccessibleSource = j;
--print("Most Accessible", i, j.text);
break;
end
end
-- Create a list of sources
if isTopLevelSearch then
AddSourceLinesForTooltip(tooltipInfo, paramA, paramB, group);
end
-- Create an unlinked version of the object.
if not group.g then
local merged = {};
for i,o in ipairs(group) do
MergeClone(merged, o);
end
if #merged == 1 and merged[1][paramA] == paramB then
group = merged[1];
local symbolicLink = ResolveSymbolicLink(group);
if symbolicLink then
if group.g and #group.g >= 0 then
for j=1,#symbolicLink,1 do
MergeClone(group.g, symbolicLink[j]);
end
else
for j=#symbolicLink,1,-1 do
symbolicLink[j] = CloneClassInstance(symbolicLink[j]);
end
group.g = symbolicLink;
end
end
else
for i,o in ipairs(merged) do
local symbolicLink = ResolveSymbolicLink(o);
if symbolicLink then
o.symbolized = true;
if o.g and #o.g >= 0 then
for j=1,#symbolicLink,1 do
MergeClone(o.g, symbolicLink[j]);
end
else
for j=#symbolicLink,1,-1 do
symbolicLink[j] = CloneClassInstance(symbolicLink[j]);
end
o.g = symbolicLink;
end
end
end
group = CloneClassInstance({ [paramA] = paramB, key = paramA });
group.g = merged;
end
end
if mostAccessibleSource then
group.parent = mostAccessibleSource.parent;
group.rwp = mostAccessibleSource.rwp;
group.e = mostAccessibleSource.e;
group.u = mostAccessibleSource.u;
group.f = mostAccessibleSource.f;
end
-- Resolve Cost
--print("GetSearchResults", paramA, paramB);
if paramA == "currencyID" then
local costResults = SearchForField("currencyIDAsCost", paramB);
if #costResults > 0 then
if not group.g then group.g = {} end
local usedToBuy = app.CreateNPC(app.HeaderConstants.VENDORS);
usedToBuy.text = "Currency For";
if not usedToBuy.g then usedToBuy.g = {}; end
for i,o in ipairs(costResults) do
MergeClone(usedToBuy.g, o);
end
MergeObject(group.g, usedToBuy);
end
elseif paramA == "itemID" then
local costResults = SearchForField("itemIDAsCost", paramB);
if #costResults > 0 then
if not group.g then group.g = {} end
local attunement = app.CreateNPC(app.HeaderConstants.QUESTS);
if not attunement.g then attunement.g = {}; end
local usedToBuy = app.CreateNPC(app.HeaderConstants.VENDORS);
if not usedToBuy.g then usedToBuy.g = {}; end
for i,o in ipairs(costResults) do
if o.key == "instanceID" or ((o.key == "difficultyID" or o.key == "mapID" or o.key == "headerID") and (o.parent and GetRelativeValue(o.parent, "instanceID"))) then
if app.Settings.Collectibles.Quests then
local d = CloneClassInstance(o);
d.sourceParent = o.parent;
d.collectible = true;
d.collected = GetItemCount(paramB, true) > 0;
d.progress = nil;
d.total = nil;
d.g = {};
MergeObject(attunement.g, d);
end
else
MergeClone(usedToBuy.g, o);
end
end
if #attunement.g > 0 then
attunement.text = "Attunement For";
MergeObject(group.g, attunement);
end
if #usedToBuy.g > 0 then
usedToBuy.text = "Currency For";
MergeObject(group.g, usedToBuy);
end
end
end
-- Only need to build/update groups from the top level
if isTopLevelSearch and group.g then
group.total = 0;
group.progress = 0;
--AssignChildren(group); -- Turning this off fixed a bug with objective tooltips.
app.UpdateGroups(group, group.g);
if group.collectible then
group.total = group.total + 1;
if group.collected then
group.progress = group.progress + 1;
end
end
end
if group.isLimited then
tinsert(tooltipInfo, 1, { left = L.LIMITED_QUANTITY, wrap = true, color = app.Colors.TooltipDescription });
end
if isTopLevelSearch then
-- Add various extra field info if enabled in settings
group.itemString = itemString
app.ProcessInformationTypesForExternalTooltips(tooltipInfo, group);
if group.working then working = true; end
end
if app.AddSourceInformation(group.sourceID, tooltipInfo, group) then
working = true;
end
if app.Settings:GetTooltipSetting("SummarizeThings") then
-- Contents
if group.g and #group.g > 0 then
local entries = {};
BuildContainsInfo(group.g, entries, paramA, paramB, " ", app.noDepth and 99 or 1);
if #entries > 0 then
local currentMapID = app.CurrentMapID;
local realmName, left, right = GetRealmName();
tinsert(tooltipInfo, { left = "Contains:" });
if #entries < 25 then
for i,item in ipairs(entries) do
left = item.group.text or RETRIEVING_DATA;
if IsRetrieving(left) then working = true; end
local mapID = app.GetBestMapForGroup(item.group, currentMapID);
if mapID and mapID ~= currentMapID then left = left .. " (" .. app.GetMapName(mapID) .. ")"; end
if item.group.icon then item.prefix = item.prefix .. "|T" .. item.group.icon .. ":0|t "; end
tinsert(tooltipInfo, { left = item.prefix .. left, right = item.right });
end
else
for i=1,math.min(25, #entries) do
local item = entries[i];
left = item.group.text or RETRIEVING_DATA;
if IsRetrieving(left) then working = true; end
local mapID = app.GetBestMapForGroup(item.group, currentMapID);
if mapID and mapID ~= currentMapID then left = left .. " (" .. app.GetMapName(mapID) .. ")"; end
if item.group.icon then item.prefix = item.prefix .. "|T" .. item.group.icon .. ":0|t "; end
tinsert(tooltipInfo, { left = item.prefix .. left, right = item.right });
end
local more = #entries - 25;
if more > 0 then tinsert(tooltipInfo, { left = "And " .. more .. " more..." }); end
end
end
end
if itemID then
local reagentCache = app.GetDataSubMember("Reagents", itemID);
if reagentCache then
-- Crafted Items
if app.Settings:GetTooltipSetting("Show:CraftedItems") then
local crafted = {};
for craftedItemID,count in pairs(reagentCache[2]) do
local item = app.CreateItem(craftedItemID);
item.count = count;
tinsert(crafted, item);
end
if #crafted > 0 then
local entries = {};
BuildReagentInfo(crafted, entries, paramA, paramB, " ", app.noDepth and 99 or 1);
if #entries > 0 then
local left, right;
tinsert(tooltipInfo, { left = "Used to Craft:" });
if #entries < 25 then
app.Sort(entries, function(a, b)
if a.group.name then
if b.group.name then
return a.group.name <= b.group.name;
end
return true;
end
return false;
end);
for i,item in ipairs(entries) do
left = item.group.text or RETRIEVING_DATA;
if IsRetrieving(left) then working = true; end
if item.group.icon then item.prefix = item.prefix .. "|T" .. item.group.icon .. ":0|t "; end
tinsert(tooltipInfo, { left = item.prefix .. left, right = item.right });
end
else
for i=1,math.min(25, #entries) do
local item = entries[i];
left = item.group.text or RETRIEVING_DATA;
if IsRetrieving(left) then working = true; end
if item.group.icon then item.prefix = item.prefix .. "|T" .. item.group.icon .. ":0|t "; end
tinsert(tooltipInfo, { left = item.prefix .. left, right = item.right });
end
local more = #entries - 25;
if more > 0 then tinsert(tooltipInfo, { left = "And " .. more .. " more..." }); end
end
end
end
end
-- Recipes
if app.Settings:GetTooltipSetting("Show:Recipes") then
local recipes = {};
for spellID,count in pairs(reagentCache[1]) do
local spell = app.CreateSpell(spellID);
spell.count = count;
tinsert(recipes, spell);
end
if #recipes > 0 then
if app.Settings:GetTooltipSetting("Show:OnlyShowNonTrivialRecipes") then
local nonTrivialRecipes = {};
for _, o in pairs(recipes) do
local craftTypeID = o.craftTypeID;
if craftTypeID and craftTypeID > 0 then
tinsert(nonTrivialRecipes, o);
end
end
recipes = nonTrivialRecipes;
end
app.Sort(recipes, SortByCraftTypeID);
local entries, left = {}, nil;
BuildReagentInfo(recipes, entries, paramA, paramB, " ", app.noDepth and 99 or 1);
if #entries > 0 then
tinsert(tooltipInfo, { left = "Used in Recipes:" });
if #entries < 25 then
for i,item in ipairs(entries) do
left = item.group.craftText or item.group.text or RETRIEVING_DATA;
if IsRetrieving(left) then working = true; end
if item.group.icon then item.prefix = item.prefix .. "|T" .. item.group.icon .. ":0|t "; end
tinsert(tooltipInfo, { left = item.prefix .. left, right = item.right });
end
else
for i=1,math.min(25, #entries) do
local item = entries[i];
left = item.group.craftText or item.group.text or RETRIEVING_DATA;
if IsRetrieving(left) then working = true; end
if item.group.icon then item.prefix = item.prefix .. "|T" .. item.group.icon .. ":0|t "; end
tinsert(tooltipInfo, { left = item.prefix .. left, right = item.right });
end
local more = #entries - 25;
if more > 0 then tinsert(tooltipInfo, { left = "And " .. more .. " more..." }); end
end
end
end
end
end
end
end
-- If there was any informational text generated, then attach that info.
if #tooltipInfo > 0 then
for i,item in ipairs(tooltipInfo) do
if item.color then item.a, item.r, item.g, item.b = HexToARGB(item.color); end
end
group.tooltipInfo = tooltipInfo;
end
-- Cache the result depending on if there is more work to be done.
if isTopLevelSearch then InitialCachedSearch = nil; end
return group, working;
end
app.GetCachedSearchResults = function(method, paramA, paramB, ...)
return app.GetCachedData((paramB and table.concat({ paramA, paramB, ...}, ":")) or paramA, GetSearchResults, method, paramA, paramB, ...);
end
-- Temporary functions to update the cache without breaking ATT.
local function UpdateRawID(field, id)
app:RefreshDataQuietly("UpdateRawID", true);
end
app.UpdateRawID = UpdateRawID;
-- Temporary functions to update the cache without breaking ATT.
local function UpdateRawIDs(field, ids)
app:RefreshDataQuietly("UpdateRawIDs", true);
end
app.UpdateRawIDs = UpdateRawIDs;
-- Item Information Lib
local function SearchForLink(link)
if link:match("item") then
-- Parse the link and get the itemID and bonus ids.
local itemString = link:match("item[%-?%d:]+") or link;
if itemString then
-- Cache the Item ID and the Suffix ID.
---@diagnostic disable-next-line: undefined-field
local _, itemID, _, _, _, _, _, suffixID = (":"):split(itemString);
if itemID then
-- Ensure that the itemID and suffixID are properly formatted.
itemID = tonumber(itemID) or 0;
return SearchForField("itemID", itemID), "itemID", itemID;
end
end
end
---@diagnostic disable-next-line: undefined-field
local kind, id = (":"):split(link);
kind = kind:lower():gsub("id", "ID");
if kind:sub(1,2) == "|c" then
kind = kind:sub(11);
end
if kind:sub(1,2) == "|h" then
kind = kind:sub(3);
end
---@diagnostic disable-next-line: undefined-field
if id then id = tonumber(("|["):split(id) or id); end
--print("SearchForLink A:", kind, id);
--print("SearchForLink B:", link:gsub("|c", "c"):gsub("|h", "h"));
if kind == "i" then
kind = "itemID";
elseif kind == "quest" or kind == "q" then
kind = "questID";
elseif kind == "faction" or kind == "rep" then
kind = "factionID";
elseif kind == "ach" or kind == "achievement" then
kind = "achievementID";
elseif kind == "creature" or kind == "npcID" or kind == "npc" or kind == "n" then
kind = "creatureID";
elseif kind == "currency" then
kind = "currencyID";
elseif kind == "spell" or kind == "enchant" or kind == "talent" or kind == "recipe" or kind == "mount" then
kind = "spellID";
elseif kind == "pet" or kind == "battlepet" then
kind = "speciesID";
elseif kind == "filterforrwp" then
kind = "filterForRWP";
elseif kind == "pettype" or kind == "pettypeID" then
kind = "petTypeID";
elseif kind == "azeriteessence" or kind == "azeriteessenceID" then
kind = "azeriteEssenceID";
end
local cache;
if id then
cache = SearchForField(kind, id);
if #cache == 0 then
local obj = CloneClassInstance({
key = kind, [kind] = id,
hash = kind .. ":" .. id,
});
if not obj.__type then
obj.icon = "Interface\\ICONS\\INV_Misc_EngGizmos_20";
obj.text = "Search Results for '" .. obj.hash .. "'";
local response = app:BuildSearchResponse(app:GetDataCache().g, kind, id);
if response and #response > 0 then
obj.g = {};
for i,o in ipairs(response) do
tinsert(obj.g, o);
end
end
else
obj.description = "@Crieve: This has not been sourced in ATT yet!";
end
tinsert(cache, obj);
end
else
local obj = { hash = kind };
obj.icon = "Interface\\ICONS\\INV_Misc_EngGizmos_20";
obj.text = "Search Results for '" .. obj.hash .. "'";
local response = app:BuildSearchResponseForField(app:GetDataCache().g, kind);
if response and #response > 0 then
obj.g = {};
for i,o in ipairs(response) do
tinsert(obj.g, o);
end
end
cache = {};
tinsert(cache, obj);
end
return cache, kind, id;
end
app.SearchForLink = SearchForLink;
-- TODO: Move the generation of this into Parser
function app:GetDataCache()
if app.Categories then
local rootData = setmetatable({
text = L["TITLE"],
hash = "ATT",
icon = app.asset("logo_32x32"),
preview = app.asset("Discord_2_128"),
description = L["DESCRIPTION"],
font = "GameFontNormalLarge",
expanded = true,
visible = true,
progress = 0,
total = 0,
g = {},
}, {
__index = function(t, key)
if key == "title" then
return t.modeString .. DESCRIPTION_SEPARATOR .. t.untilNextPercentage;
elseif key == "progressText" then
if t.total < 1 then
local primeData = app.CurrentCharacter.PrimeData;
if primeData then
return GetProgressColorText(primeData.progress, primeData.total);
end
end
return GetProgressColorText(t.progress, t.total);
elseif key == "modeString" then
return app.Settings:GetModeString();
elseif key == "untilNextPercentage" then
if t.total < 1 then
local primeData = app.CurrentCharacter.PrimeData;
if primeData then
return app.Modules.Color.GetProgressTextToNextPercent(primeData.progress, primeData.total);
end
end
return app.Modules.Color.GetProgressTextToNextPercent(t.progress, t.total);
else
-- Something that isn't dynamic.
return table[key];
end
end
});
local g = rootData.g;
-- Dungeons & Raids
if app.Categories.Instances then
tinsert(g, {
text = GROUP_FINDER,
icon = app.asset("Category_D&R"),
g = app.Categories.Instances,
});
end
-- Outdoor Zones
if app.Categories.Zones then
tinsert(g, {
mapID = 947,
text = BUG_CATEGORY2,
icon = app.asset("Category_Zones"),
g = app.Categories.Zones,
});
end
-- World Drops
if app.Categories.WorldDrops then
tinsert(g, {
text = TRANSMOG_SOURCE_4,
icon = app.asset("Category_WorldDrops"),
g = app.Categories.WorldDrops,
isWorldDropCategory = true
});
end
-- Crafted Items
if app.Categories.Craftables then
tinsert(g, {
text = LOOT_JOURNAL_LEGENDARIES_SOURCE_CRAFTED_ITEM,
icon = app.asset("Category_Crafting"),
DontEnforceSkillRequirements = true,
g = app.Categories.Craftables,
isCraftedCategory = true
});
end
-- Group Finder
if app.Categories.GroupFinder then
tinsert(g, {
text = DUNGEONS_BUTTON,
icon = app.asset("Category_GroupFinder"),
u = 33, -- WRATH_PHASE_FOUR
g = app.Categories.GroupFinder,
});
end
-- Professions
if app.Categories.Professions then
tinsert(g, {
text = TRADE_SKILLS,
icon = app.asset("Category_Professions"),
description = "This section will only show your character's professions outside of Account and Debug Mode.",
g = app.Categories.Professions
});
end
-- Holidays
if app.Categories.Holidays then
tinsert(g, app.CreateNPC(app.HeaderConstants.HOLIDAYS, {
description = "These events occur at consistent dates around the year based on and themed around real world holiday events.",
g = app.Categories.Holidays,
SortType = "EventStart",
isHolidayCategory = true,
}));
end
-- Expansion Features
if app.Categories.ExpansionFeatures and #app.Categories.ExpansionFeatures > 0 then
tinsert(g, {
text = "Expansion Features",
icon = app.asset("Category_ExpansionFeatures"),
g = app.Categories.ExpansionFeatures
});
end
-- Character
if app.Categories.Character then
local db = {};
db.g = app.Categories.Character;
db.text = CHARACTER;
db.name = db.text;
db.icon = app.asset("Category_ItemSets");
tinsert(g, db);
end
-- In-Game Store
if app.Categories.InGameShop then
tinsert(g, app.CreateNPC(app.HeaderConstants.IN_GAME_SHOP, {
g = app.Categories.InGameShop,
expanded = false
}));
end
-- PvP
if app.Categories.PVP then
tinsert(g, app.CreateNPC(app.HeaderConstants.PVP, {
g = app.Categories.PVP,
isPVPCategory = true
}));
end
-- Promotions
if app.Categories.Promotions then
tinsert(g, {
text = BATTLE_PET_SOURCE_8,
icon = app.asset("Category_Promo"),
description = "This section is for real world promotions that seeped extremely rare content into the game prior to some of them appearing within the In-Game Shop.",
g = app.Categories.Promotions,
isPromotionCategory = true
});
end
-- Season of Discovery
if app.Categories.SeasonOfDiscovery then
for i,o in ipairs(app.Categories.SeasonOfDiscovery) do
tinsert(g, o);
end
end
-- Skills
if app.Categories.Skills then
tinsert(g, {
text = SKILLS,
icon = "Interface\\ICONS\\SPELL_NATURE_THUNDERCLAP",
g = app.Categories.Skills
});
end
-- World Events
if app.Categories.WorldEvents then
tinsert(g, {
text = BATTLE_PET_SOURCE_7;
icon = app.asset("Category_Event"),
description = "These events occur at different times in the game's timeline, typically as one time server wide events. Special celebrations such as Anniversary events and such may be found within this category.",
g = app.Categories.WorldEvents,
isEventCategory = true,
});
end
-- Dynamic Categories
if app.Windows then
local keys,sortedList = {},{};
for suffix,window in pairs(app.Windows) do
if window and window.IsDynamicCategory then
keys[suffix] = window;
end
end
for suffix,window in pairs(keys) do
tinsert(sortedList, suffix);
end
app.Sort(sortedList, app.SortDefaults.Strings);
for i,suffix in ipairs(sortedList) do
tinsert(g, app.CreateDynamicCategory(suffix));
end
end
-- Track Deaths!
tinsert(g, app:CreateDeathClass());
-- Yourself.
tinsert(g, app.CreateUnit("player", {
description = "Awarded for logging in.\n\nGood job! YOU DID IT!\n\nOnly visible while in Debug Mode.",
races = { app.RaceIndex },
c = { app.ClassIndex },
r = app.FactionID,
collected = 1,
nmr = false,
OnUpdate = function(self)
self.lvl = app.Level;
if app.MODE_DEBUG then
self.collectible = true;
else
self.collectible = false;
end
end
}));
-- Now assign the parent hierarchy for this cache.
AssignChildren(rootData);
-- Now that we have all of the root data, cache it.
app.CacheFields(rootData);
-- Determine how many expansionID instances could be found
local expansionCounter = 0;
local expansionCache = SearchForFieldContainer("expansionID");
for key,value in pairs(expansionCache) do
expansionCounter = expansionCounter + 1;
end
if expansionCounter == 1 then
-- Purge the Expansion Objects. This is the Classic Layout style.
for key,values in pairs(expansionCache) do
for j,value in ipairs(values) do
local parent = value.parent;
if parent then
-- Remove the expansion object reference.
for i=#parent.g,1,-1 do
if parent.g[i] == value then
tremove(parent.g, i);
break;
end
end
-- Feed the children to its parent.
if value.g then
for i,child in ipairs(value.g) do
child.parent = parent;
tinsert(parent.g, child);
end
end
end
end
end
-- Wipe out the expansion object cache.
wipe(expansionCache);
end
-- All future calls to this function will return the root data.
app.GetDataCache = function()
return rootData;
end
return rootData;
end
end
-- Achievement Lib
(function()
local SetAchievementCollected = function(t, achievementID, collected)
return app.SetCollected(t, "Achievements", achievementID, collected);
end
-- These achievement handlers are good to use at any point.
local LOREMASTER_CreateQuests = function(t, extraQuestIDs)
local parent = t.sourceParent or t.parent;
if parent then
-- Get the quests list from the zone itself.
local g, quests, count = parent.parent and parent.parent.g, {}, 0;
if g and #g > 0 then
for i,o in ipairs(g) do
if o.headerID == app.HeaderConstants.QUESTS then
-- Clone the list to prevent dirtying the quest list in the zone.
for j,quest in ipairs(o.g) do
if quest.key == "questID" and not quest.repeatable then
count = count + 1;
quests[count] = quest;
end
end
break;
end
end
end
-- If additional questIDs were manually included, let's do some extra work.
if extraQuestIDs and #extraQuestIDs > 0 then
for i,questID in ipairs(extraQuestIDs) do
local results = SearchForField("questID", questID);
if #results > 0 then
local quest = results[1];
if quest.key == "questID" and not quest.repeatable then
count = count + 1;
quests[count] = quest;
end
end
end
end
if count > 0 then
return quests;
end
end
end;
local LOREMASTER_CreateQuestsAndStructures = function(t, mapID, extraQuestIDs)
-- Grab all of the quests on the continent.
local response;
local results = SearchForField("mapID", mapID);
local count = #results;
if count < 1 then
return;
elseif count > 1 then
-- Uh wasn't expecting this.
local bestResult;
for i=1,#results,1 do
local g = results[i].g;
if g and not bestResult or #g > #bestResult.g then
bestResult = results[i];
end
end
response = app:BuildSearchResponseForField(bestResult.g, "questID");
else
response = app:BuildSearchResponseForField(results[1].g, "questID");
end
local quests, structures = {}, {};
if response then
-- Get the quests list from the zone itself.
local zones_header = {text=BUG_CATEGORY2,icon = app.asset("Category_Zones"),description = "These are outdoor quests that involve the associated maps for the continent.",g = response};
app:BuildFlatSearchResponseForField(response, "questID", quests);
if #zones_header.g > 0 then
tinsert(structures, zones_header);
end
-- Get a list of all of the mapIDs in this structure.
response = {};
app:BuildFlatSearchResponseForField(zones_header.g, "mapID", response);
if #response > 0 then
local mapIDs = {};
for i,o in ipairs(response) do
if o.mapID and not mapIDs[o.mapID] then
mapIDs[o.mapID] = true;
end
if o.maps then
for j,id in ipairs(o.maps) do
if not mapIDs[id] then
mapIDs[id] = true;
end
end
end
end
response = app:BuildSearchFilteredResponse(app.Categories.Instances, function(group)
if group.questID and not group.repeatable then
if group.coords then
for i,coord in ipairs(group.coords) do
if coord[3] and mapIDs[coord[3]] then
return true;
end
end
end
if group.maps then
for i,id in ipairs(group.maps) do
if mapIDs[id] then
return true;
end
end
end
return false;
end
end);
if response then
local dungeon_header = {text=GROUP_FINDER,icon = app.asset("Category_D&R"),description = "These are dungeon quests that involve the associated maps for the continent. They may or may not count towards the loremaster achievement. Just get it done and don't be lazy or complain to me.\n\n - Crieve",g = response};
app:BuildFlatSearchResponseForField(response, "questID", quests);
if #dungeon_header.g > 0 then
tinsert(structures, 1, dungeon_header);
end
end
end
end
-- If additional questIDs were manually included, let's do some extra work.
if extraQuestIDs and #extraQuestIDs > 0 then
for i,questID in ipairs(extraQuestIDs) do
local results = SearchForField("questID", questID);
if #results > 0 then
tinsert(quests, 1, results[1]);
end
end
end
-- Return the Outdoor Zones and Dungeon structures.
return quests, structures;
end
local commonAchievementHandlers = {
["COMPANIONS_OnClick"] = function(row, button)
if button == "RightButton" then
local t = row.ref;
local template = {};
for i,o in pairs(SearchForFieldContainer("speciesID")) do
tinsert(template, o[1]);
end
local clone = app:CreateMiniListForGroup(app.CreateAchievement(t[t.key], template)).data;
clone.OnTooltip = t.OnTooltip;
clone.OnUpdate = t.OnUpdate;
clone.rank = t.rank;
return true;
end
end,
["DEDICATED_10M_OnUpdate"] = function(t)
rawset(t, "collectible", nil);
if app.MODE_DEBUG_OR_ACCOUNT then
return false;
elseif IsInGroup() and GetNumGroupMembers() >= 9 then
rawset(t, "collectible", false);
return true;
end
end,
["DEDICATED_25M_OnUpdate"] = function(t)
rawset(t, "collectible", nil);
if app.MODE_DEBUG_OR_ACCOUNT then
return false;
elseif IsInGroup() and GetNumGroupMembers() >= 21 then
rawset(t, "collectible", false);
return true;
end
end,
["EXALTED_REP_OnInit"] = function(t, factionID)
t.BuildReputation = function()
local r = t.rep;
if not r then
local f = SearchForField("factionID", factionID);
if #f > 0 then
r = f[1];
for i,o in ipairs(f) do
if o.key == "factionID" then
if o.maxReputation then
r = app.CloneObject(o);
r.maxReputation = nil;
else
r = o;
end
break;
end
end
t.rep = r;
end
end
return r;
end
return t;
end,
["EXALTED_REP_OnClick"] = function(row, button)
if button == "RightButton" then
local t = row.ref;
local r = t.rep or (t.BuildReputation and t:BuildReputation());
local clone = app:CreateMiniListForGroup(app.CreateAchievement(t[t.key], { r })).data;
clone.description = t.description;
return true;
end
end,
["EXALTED_REP_OnUpdate"] = function(t)
if t.collectible then
local r = t.rep or (t.BuildReputation and t:BuildReputation());
if not r then return true; end
t:SetAchievementCollected(t.achievementID, r.standing == 8);
end
end,
["EXALTED_REP_OnTooltip"] = function(t, tooltipInfo)
if t.collectible then
local r = t.rep or (t.BuildReputation and t:BuildReputation());
if r then
tinsert(tooltipInfo, { left = " " });
tinsert(tooltipInfo, {
left = " |T" .. r.icon .. ":0|t " .. r.text,
right = app.L[r.standing == 8 and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"],
r = 1, g = 1, b = 1
});
end
end
end,
["EXALTED_REPS_OnInit"] = function(t, ...)
local factionIDs = { ... };
t.BuildReputations = function()
local reps = t.reps;
if not reps then
reps = {};
for i,factionID in ipairs(factionIDs) do
local f = SearchForField("factionID", factionID);
if #f > 0 then
local best = f[1];
for _,o in ipairs(f) do
if o.key == "factionID" then
best = o;
break;
end
end
if best.maxReputation then
best = app.CloneObject(best);
best.maxReputation = nil;
end
tinsert(reps, best);
end
end
if #reps > 0 then
t.reps = reps;
else
reps = nil;
end
end
return reps;
end
return t;
end,
["EXALTED_REPS_OnClick"] = function(row, button)
if button == "RightButton" then
local t = row.ref;
local reps = t.reps or (t.BuildReputations and t:BuildReputations());
local clone = app:CreateMiniListForGroup(app.CreateAchievement(t[t.key], reps)).data;
clone.description = t.description;
return true;
end
end,
["EXALTED_REPS_OnTooltip"] = function(t, tooltipInfo)
if t.collectible then
local reps = t.reps or (t.BuildReputations and t:BuildReputations());
if reps then
tinsert(tooltipInfo, { left = " " });
for i,faction in ipairs(reps) do
tinsert(tooltipInfo, {
left = " |T" .. faction.icon .. ":0|t " .. faction.text,
right = app.L[faction.standing == 8 and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"],
r = 1, g = 1, b = 1
});
end
end
end
end,
["LOREMASTER_OnClick"] = function(row, button)
if button == "RightButton" then
local t = row.ref;
local data = t.quests or (t.BuildQuests and t:BuildQuests());
local clone = app:CreateMiniListForGroup(app.CreateAchievement(t[t.key], data)).data;
clone.description = t.description;
return true;
end
end,
["LOREMASTER_CONTINENT_OnClick"] = function(row, button)
if button == "RightButton" then
local t = row.ref;
local data = t.structures or (t.BuildStructures and t:BuildStructures()) or t.quests;
local clone = app:CreateMiniListForGroup(app.CreateAchievement(t[t.key], data)).data;
clone.description = t.description;
return true;
end
end,
["MOUNTS_OnClick"] = function(row, button)
if button == "RightButton" then
local t = row.ref;
local template,r = {},{};
for i,o in pairs(SearchForFieldContainer("spellID")) do
if #o > 0 then
if ((o[1].f and o[1].f == app.FilterConstants.MOUNTS)
or (o[1].filterID and o[1].filterID == app.FilterConstants.MOUNTS)) and not r[i] then
tinsert(template, o[1]);
r[i] = 1;
end
end
end
local clone = app:CreateMiniListForGroup(app.CreateAchievement(t[t.key], template)).data;
clone.OnTooltip = t.OnTooltip;
clone.OnUpdate = t.OnUpdate;
clone.rank = t.rank;
return true;
end
end,
["REPUTATIONS_OnClick"] = function(row, button)
if button == "RightButton" then
local t = row.ref;
local template = {};
for i,o in ipairs(app:GetDataCache().g) do
if o.headerID == app.HeaderConstants.FACTIONS then
for j,p in ipairs(o.g) do
if (not p.minReputation or (p.minReputation[1] == p.factionID and p.minReputation[2] >= 41999)) and (not p.maxReputation or (p.maxReputation[1] ~= p.factionID and p.reputation >= 0)) then
tinsert(template, p);
end
end
end
end
local clone = app:CreateMiniListForGroup(app.CreateAchievement(t[t.key], template)).data;
clone.OnTooltip = t.OnTooltip;
clone.OnUpdate = t.OnUpdate;
clone.rank = t.rank;
return true;
end
end,
};
app.CommonAchievementHandlers = commonAchievementHandlers;
local fields = {
["collectible"] = function(t)
return app.Settings.Collectibles.Achievements;
end,
["collected"] = function(t)
if app.CurrentCharacter.Achievements[t.achievementID] then return 1; end
if app.Settings.AccountWide.Achievements and ATTAccountWideData.Achievements[t.achievementID] then return 2; end
end,
["OnUpdate"] = function(t) ResolveSymbolicLink(t); end,
};
local categoryFields = {
["icon"] = function(t)
return app.asset("Category_Achievements");
end,
["ignoreSourceLookup"] = function(t)
return true;
end,
};
if GetCategoryInfo and (GetCategoryInfo(92) ~= "" and GetCategoryInfo(92) ~= nil) then
-- Achievements are in. We can use the API.
local GetAchievementCategory = _G["GetAchievementCategory"];
fields.text = function(t)
return t.link or ("|cffffff00[" .. (t.name or ("@CRIEVE: INVALID ACHIEVEMENT " .. t.achievementID)) .. "]|r");
end
fields.name = function(t)
local name = select(2, GetAchievementInfo(t.achievementID));
if name then return name; end
local data = L.ACHIEVEMENT_DATA[t.achievementID];
if data and data[2] then return data[2]; end
return GetNameFromProviders(t)
or (t.spellID and GetSpellName(t.spellID));
end
fields.link = function(t)
return GetAchievementLink(t.achievementID);
end
fields.icon = function(t)
local name = select(10, GetAchievementInfo(t.achievementID));
if name then return name; end
local data = L.ACHIEVEMENT_DATA[t.achievementID];
if data and data[3] then return data[3]; end
return GetIconFromProviders(t)
or (t.spellID and GetSpellIcon(t.spellID))
or t.parent.icon or "Interface\\Worldmap\\Gear_64Grey";
end
fields.parentCategoryID = function(t)
local data = GetAchievementCategory(t.achievementID);
if data then return data; end
data = L.ACHIEVEMENT_DATA[t.achievementID];
if data then return data[1]; end
return -1;
end
fields.SetAchievementCollected = function(t)
if t.achievementID == 5788 or t.achievementID == 6059 then
return SetAchievementCollected;
else
print("Attempted to retrieve the function SetAchievementCollected from the Achievement object. (no longer available)");
return function(t, achievementID, collected)
print("Attempted to set achievement " .. achievementID .. " as collected: " .. (collected and 1 or 0));
end
end
end
fields.isStatistic = function(t)
return select(15, GetAchievementInfo(t.achievementID));
end
local onTooltipForAchievement = function(t, tooltipInfo)
local achievementID = t.achievementID;
if achievementID and IsShiftKeyDown() then
local criteriaDatas,criteriaDatasByUID = {}, {};
for criteriaID=1,99999,1 do
local criteriaString, criteriaType, completed, _, _, _, _, assetID, quantityString, criteriaUID = GetAchievementCriteriaInfoByID(achievementID, criteriaID);
if criteriaString and criteriaUID then
criteriaDatasByUID[criteriaUID] = true;
tinsert(criteriaDatas, {
" [" .. criteriaUID .. "]: " .. tostring(criteriaString),
"(" .. tostring(assetID) .. " @ " .. tostring(criteriaType) .. ") " .. tostring(quantityString) .. " " .. GetCompletionIcon(completed)
});
end
end
local totalCriteria = GetAchievementNumCriteria(achievementID) or 0;
if totalCriteria > 0 then
for criteriaIndex=1,totalCriteria,1 do
---@diagnostic disable-next-line: redundant-parameter
local criteriaString, criteriaType, completed, _, _, _, _, assetID, quantityString, criteriaUID = GetAchievementCriteriaInfo(achievementID, criteriaIndex, true);
if criteriaString and (not criteriaDatasByUID[criteriaUID] or criteriaUID == 0) then
tinsert(criteriaDatas, {
" [" .. criteriaUID .. " @ Index: " .. criteriaIndex .. "]: " .. tostring(criteriaString),
"(" .. tostring(assetID) .. " @ " .. tostring(criteriaType) .. ") " .. tostring(quantityString) .. " " .. GetCompletionIcon(completed)
});
end
end
end
if #criteriaDatas > 0 then
tinsert(tooltipInfo, { left = " " });
tinsert(tooltipInfo, {
left = "Total Criteria",
right = tostring(#criteriaDatas),
r = 0.8, g = 0.8, b = 1
});
for i,criteriaData in ipairs(criteriaDatas) do
tinsert(tooltipInfo, {
left = criteriaData[1],
right = criteriaData[2],
r = 1, g = 1, b = 1
});
end
end
end
end
local onTooltipForAchievementCriteria = function(t, tooltipInfo)
local achievementID = t.achievementID;
if achievementID then
tinsert(tooltipInfo, {
left = L.CRITERIA_FOR,
right = "|cffffff00[" .. (select(2, GetAchievementInfo(achievementID)) or RETRIEVING_DATA) .. "]|r",
});
if IsShiftKeyDown() then
local criteriaID = t.criteriaID;
if criteriaID then
tinsert(tooltipInfo, { left = " " });
local criteriaString, criteriaType, completed, quantity, reqQuantity, charName, flags, assetID, quantityString, criteriaUID = t.GetInfo(achievementID, criteriaID, true)
if criteriaString then
tinsert(tooltipInfo, {
left = " [" .. criteriaUID .. "]: " .. tostring(criteriaString),
right = "(" .. tostring(assetID) .. " @ " .. tostring(criteriaType) .. ") " .. tostring(quantityString) .. " " .. GetCompletionIcon(completed),
r = 1, g = 1, b = 1
});
end
end
end
end
end
fields.OnTooltip = function()
return onTooltipForAchievement;
end
categoryFields.text = function(t)
local data = GetCategoryInfo(t.achievementCategoryID);
if data then return data; end
data = L.ACHIEVEMENT_CRITERIA_DATA[t.achievementCategoryID];
if data then return data[2]; end
return RETRIEVING_DATA .. " achcat:" .. t.achievementCategoryID;
end
categoryFields.parentCategoryID = function(t)
local data = select(2, GetCategoryInfo(t.achievementCategoryID));
if data then return data; end
data = L.ACHIEVEMENT_CRITERIA_DATA[t.achievementCategoryID];
if data then return data[1]; end
return -1;
end
app.CreateAchievement = app.CreateClass("Achievement", "achievementID", fields);
app.CreateGuildAchievement = app.ExtendClass("Achievement", "GuildAchievement", "guildAchievementID", {
collectible = app.ReturnFalse,
achievementID = function(t) return t.guildAchievementID; end,
isGuild = app.ReturnTrue,
});
app.CreateAchievementCriteria = app.CreateClass("AchievementCriteria", "criteriaID", {
["achievementID"] = function(t)
return t.achID or t.criteriaParent.achievementID;
end,
["criteriaParent"] = function(t)
return t.sourceParent or t.parent or app.EmptyTable;
end,
["index"] = function(t)
return 1;
end,
["collectible"] = function(t)
return app.Settings.Collectibles.Achievements;
end,
["trackable"] = app.ReturnTrue,
["text"] = function(t)
return "|cffffff00[Criteria: " .. (t.name or RETRIEVING_DATA) .. "]|r";
end,
["name"] = function(t)
local achievementID = t.achievementID;
if achievementID then
local criteriaID = t.criteriaID;
if criteriaID then
local name = t.GetInfo(achievementID, criteriaID, true) or GetNameFromProviders(t);
if not IsRetrieving(name) then return name; end
local sourceQuests = t.sourceQuests;
if sourceQuests then
for k,id in ipairs(sourceQuests) do
return app.GetQuestName(id);
end
end
return "achievementID:" .. achievementID .. ":" .. criteriaID;
end
end
end,
["icon"] = function(t)
return GetIconFromProviders(t)
or (t.achievementID and select(10, GetAchievementInfo(t.achievementID)));
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
local model = app.ObjectModels[v[2]];
if model then return model; end
end
end
end
end
end,
["collected"] = function(t)
-- Check to see if the criteria was completed.
local achievementID = t.achievementID;
if achievementID then
if app.CurrentCharacter.Achievements[achievementID] then return 1; end
if app.Settings.AccountWide.Achievements and ATTAccountWideData.Achievements[achievementID] then return 2; end
local criteriaID = t.criteriaID;
if criteriaID then
local collected = false;
local status, err = pcall(function()
collected = select(3, t.GetInfo(achievementID, criteriaID, true));
end);
if not status then
print("ERROR", achievementID, criteriaID, err);
end
return collected;
end
end
end,
["saved"] = function(t)
-- Check to see if the criteria was completed.
local achievementID = t.achievementID;
if achievementID then
if app.CurrentCharacter.Achievements[achievementID] then return true; end
local criteriaID = t.criteriaID;
if criteriaID then
return select(3, t.GetInfo(achievementID, criteriaID, true));
end
end
end,
["OnTooltip"] = function()
return onTooltipForAchievementCriteria;
end,
GetInfo = function()
return GetAchievementCriteriaInfoByID;
end,
},
"WithIndex", {
GetInfo = function()
return GetAchievementCriteriaInfo;
end;
}, (function(t) return t.criteriaID < 100; end));
app.CreateGuildAchievementCriteria = app.ExtendClass("AchievementCriteria", "GuildAchievementCriteria", "guildCriteriaID", {
collectible = app.ReturnFalse,
criteriaID = function(t) return t.guildCriteriaID; end,
isGuild = app.ReturnTrue,
});
local function CheckAchievementCollectionStatus(achievementID)
achievementID = tonumber(achievementID) or achievementID;
SetAchievementCollected(SearchForField("achievementID", achievementID)[1], achievementID, select(13, GetAchievementInfo(achievementID)));
end
local function refreshAchievementCollection()
if ATTAccountWideData then
for achievementID,container in pairs(SearchForFieldContainer("achievementID")) do
local id = tonumber(achievementID) or achievementID;
if id ~= 5788 then
SetAchievementCollected(container[1], id, select(13, GetAchievementInfo(id)));
end
end
end
end
app.AddEventHandler("OnRecalculate", refreshAchievementCollection);
app:RegisterEvent("ADDON_LOADED");
app:RegisterEvent("ACHIEVEMENT_EARNED");
app.events.ACHIEVEMENT_EARNED = CheckAchievementCollectionStatus;
app.events.ADDON_LOADED = function(addonName)
if addonName == "Blizzard_AchievementUI" then
refreshAchievementCollection();
app:UnregisterEvent("ADDON_LOADED");
end
end
-- Achievements are supported in this version, so we don't need to manually check anything!
-- Most calculations that were previously in the OnUpdate can now exist in a build script instead.
commonAchievementHandlers.EXPLORATION_OnClick = function(row, button)
if button == "RightButton" then
local t = row.ref;
local areas = t.areas;
if not areas then
local g = (t.sourceParent or t.parent).parent.g;
if g and #g > 0 then
for i,o in ipairs(g) do
if o.headerID == app.HeaderConstants.EXPLORATION then
areas = o.g;
break;
end
end
if not areas then return; end
else
return;
end
t.areas = areas;
end
local clone = app:CreateMiniListForGroup(app.CreateAchievement(t[t.key], areas)).data;
clone.description = t.description;
return true;
end
end
commonAchievementHandlers.LOREMASTER_CONTINENT_OnUpdate = function(t, mapID, ...)
t.OnUpdate = nil;
local extraQuestIDs = { ... };
t.BuildStructures = function()
if not t.structures then
local quests, structures = LOREMASTER_CreateQuestsAndStructures(t, mapID, extraQuestIDs);
if quests then
t.quests = quests;
t.structures = structures;
return structures;
end
else
return t.structures;
end
end
end
commonAchievementHandlers.LOREMASTER_OnUpdate = function(t, ...)
t.OnUpdate = nil;
local extraQuestIDs = { ... };
t.BuildQuests = function()
local quests = t.quests;
if not quests then
quests = LOREMASTER_CreateQuests(t, extraQuestIDs);
if quests then
t.quests = quests;
end
end
return quests;
end
end
else
-- Achievements are NOT in. We can't use the API.
fields.text = function(t)
return "|cffffff00[" .. t.name .. "]|r";
end
fields.name = function(t)
local data = L.ACHIEVEMENT_DATA[t.achievementID];
if data and data[2] then return data[2]; end
return GetNameFromProviders(t) or (t.spellID or GetSpellName(t.spellID)) or RETRIEVING_DATA;
end
fields.icon = function(t)
local data = L.ACHIEVEMENT_DATA[t.achievementID];
if data and data[3] then return data[3]; end
return GetIconFromProviders(t)
or (t.spellID and GetSpellIcon(t.spellID))
or t.parent.icon or "Interface\\Worldmap\\Gear_64Grey";
end
fields.parentCategoryID = function(t)
local data = L.ACHIEVEMENT_DATA[t.achievementID];
if data then return data[1]; end
return -1;
end
fields.SetAchievementCollected = function(t)
return SetAchievementCollected;
end
categoryFields.text = function(t)
local data = L.ACHIEVEMENT_CRITERIA_DATA[t.achievementCategoryID];
if data then return data[2]; end
return RETRIEVING_DATA .. " achcat:" .. t.achievementCategoryID;
end
categoryFields.parentCategoryID = function(t)
local data = L.ACHIEVEMENT_CRITERIA_DATA[t.achievementCategoryID];
if data then return data[1]; end
return -1;
end
local fieldsWithSpellID = {
OnUpdate = function(t)
if t.collectible then
t:SetAchievementCollected(t.achievementID, app.IsSpellKnown(t.spellID, t.rank));
end
end
};
app.CreateAchievement = app.CreateClass("Achievement", "achievementID", fields,
"WithSpell", fieldsWithSpellID, function(t) return t.spellID; end); -- This is a conditional contructor.
app.CreateAchievementCriteria = function(id, t) return nil; end -- We don't support showing criteria before Wrath
-- Add in manual achievement handlers
-- These are required to manually detect if an "achievement" is collected or not.
-- They do not use the WOW Achievement API since it doesn't exist in this version.
-- A lot of times these require additional calculations in the OnUpdate, so do not use these outside of the required environment!
local LOREMASTER_EXPLICIT_OnUpdate = function(t)
-- This is detached because we don't want contribs calling to this directly, it's a helper function.
local quests = t.quests;
if quests and #quests > 0 then
local p = 0;
local groupFilter = app.Modules.Filter.Get.Group();
if not groupFilter then app.Modules.Filter.Set.Group(true); end
if app.Modules.Filter.Filters.Race(t) then
for i,o in ipairs(quests) do
if app.GroupFilter(o) then
if o.collected == 1 then
p = p + 1;
end
end
end
end
if not groupFilter then app.Modules.Filter.Set.Group(); end
t.p = p;
t:SetAchievementCollected(t.achievementID, p >= t.rank);
else
return true;
end
end
commonAchievementHandlers.ALL_ITEM_PROVIDERS = function(t)
local collected = true;
for i,provider in ipairs(t.providers) do
if provider[1] == "i" and GetItemCount(provider[2], true) == 0 then
collected = false;
break;
end
end
t:SetAchievementCollected(t.achievementID, collected);
end
commonAchievementHandlers.ANY_ITEM_PROVIDER = function(t)
local collected = false;
for i,provider in ipairs(t.providers) do
if provider[1] == "i" and GetItemCount(provider[2], true) > 0 then
collected = true;
break;
end
end
t:SetAchievementCollected(t.achievementID, collected);
end
commonAchievementHandlers.ALL_SOURCE_QUESTS = function(t)
local collected = true;
for i,questID in ipairs(t.sourceQuests) do
if not IsQuestFlaggedCompleted(questID) then
collected = false;
break;
end
end
t:SetAchievementCollected(t.achievementID, collected);
end
commonAchievementHandlers.ANY_SOURCE_QUEST = function(t)
local collected = false;
for i,questID in ipairs(t.sourceQuests) do
if IsQuestFlaggedCompleted(questID) then
collected = true;
break;
end
end
t:SetAchievementCollected(t.achievementID, collected);
end
commonAchievementHandlers.COMPANIONS_OnUpdate = function(t)
if app.Settings.Collectibles.BattlePets then
local count = 0;
for i,g in pairs(SearchForFieldContainer("speciesID")) do
if g[1].collected then
count = count + 1;
end
end
if t.rank > 1 then
t.progress = math.min(count, t.rank);
t.total = t.rank;
t.collectible = false;
if app.GroupFilter(t) then
local parent = t.parent;
parent.total = (parent.total or 0) + t.total;
parent.progress = (parent.progress or 0) + t.progress;
t.visible = (t.progress < t.total or app.CollectedItemVisibilityFilter(t));
else
t.visible = false;
end
else
t.collected = count >= 1;
t.collectible = true;
if app.GroupFilter(t) then
local parent = t.parent;
parent.total = (parent.total or 0) + 1;
if t.collected then parent.progress = (parent.progress or 0) + 1; end
t.visible = (not t.collected or app.CollectedItemVisibilityFilter(t));
else
t.visible = false;
end
end
else
t.collected = nil;
t.collectible = false;
t.progress = nil;
t.total = nil;
t.visible = false;
end
return true;
end
commonAchievementHandlers.COMPANIONS_OnTooltip = function(t, tooltipInfo)
tinsert(tooltipInfo, { left = "Collect " .. t.rank .. " companion pets." });
if t.total and t.progress < t.total and t.rank >= 25 then
tinsert(tooltipInfo, { left = " " });
local c = 0;
for i,g in pairs(SearchForFieldContainer("speciesID")) do
local p = g[1];
if p.visible then
c = c + 1;
if c < 16 then
tinsert(tooltipInfo, {
left = " |T" .. p.icon .. ":0|t " .. p.text,
right = app.L[p.collected and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"],
r = 1, g = 1, b = 1
});
end
end
end
if c > 15 then tinsert(tooltipInfo, { " And " .. (c - 15) .. " more!" }); end
end
end
commonAchievementHandlers.EXALTED_REPS_OnUpdate = function(t)
if t.collectible then
local reps = t.reps or (t.BuildReputations and t:BuildReputations());
if not reps then return true; end
local collected = true;
for i,r in ipairs(reps) do
if r.standing < 8 then
collected = false;
break;
end
end
t:SetAchievementCollected(t.achievementID, collected);
end
end
commonAchievementHandlers.EXALTED_REPS_ANY_OnUpdate = function(t)
if t.collectible then
local reps = t.reps or (t.BuildReputations and t:BuildReputations());
if not reps then return true; end
local collected = false;
for i,r in ipairs(reps) do
if r.standing == 8 then
collected = true;
break;
end
end
t:SetAchievementCollected(t.achievementID, collected);
end
end
commonAchievementHandlers.EXPLORATION_OnUpdate = function(t)
if t.collectible and t.parent then
if not t.areas then
local g = (t.sourceParent or t.parent).parent.g;
if g and #g > 0 then
for i,o in ipairs(g) do
if o.headerID == app.HeaderConstants.EXPLORATION then
t.areas = o.g;
break;
end
end
if not t.areas then return; end
else
return;
end
end
local collected = true;
for i,o in ipairs(t.areas) do
if o.collected ~= 1 and app.RecursiveUnobtainableFilter(o) then
collected = false;
break;
end
end
t:SetAchievementCollected(t.achievementID, collected);
end
end
commonAchievementHandlers.EXPLORATION_OnClick = function(row, button)
if button == "RightButton" then
local t = row.ref;
local clone = app:CreateMiniListForGroup(app.CreateAchievement(t[t.key], t.areas)).data;
clone.description = t.description;
return true;
end
end
commonAchievementHandlers.KNOW_SPELLS_OnUpdate = function(t, ...)
if t.collectible then
if not t.spells then
local spells = {};
for i,spellID in ipairs({ ... }) do
local f = SearchForField("spellID", spellID);
if #f > 0 then
tinsert(spells, f[1]);
else
return true;
end
end
if #spells < 1 then return true; end
t.spells = spells;
end
local collected = true;
for i,spell in ipairs(t.spells) do
if spell.collected then
collected = false;
break;
end
end
t:SetAchievementCollected(t.achievementID, collected);
end
end
commonAchievementHandlers.KNOW_SPELLS_ANY_OnUpdate = function(t, ...)
if t.collectible then
if not t.spells then
local spells = {};
for i,spellID in ipairs({ ... }) do
local f = SearchForField("spellID", spellID);
if #f > 0 then
tinsert(spells, f[1]);
else
tinsert(spells, app.CreateSpell(spellID));
end
end
if #spells < 1 then return true; end
t.spells = spells;
end
local collected = false;
for i,spell in ipairs(t.spells) do
if spell.collected then
collected = true;
break;
end
end
t:SetAchievementCollected(t.achievementID, collected);
end
end
commonAchievementHandlers.KNOW_SPELLS_OnClick = function(row, button)
if button == "RightButton" then
local t = row.ref;
local clone = app:CreateMiniListForGroup(app.CreateAchievement(t[t.key], t.spells)).data;
clone.description = t.description;
return true;
end
end
commonAchievementHandlers.KNOW_SPELLS_OnTooltip = function(t, tooltipInfo)
if t.collectible and t.spells then
tinsert(tooltipInfo, { left = " " });
for i,spell in ipairs(t.spells) do
tinsert(tooltipInfo, {
left = " |T" .. spell.icon .. ":0|t " .. spell.text,
right = app.L[spell.collected and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"],
r = 1, g = 1, b = 1
});
end
end
end
commonAchievementHandlers.LEVEL_OnUpdate = function(t)
t:SetAchievementCollected(t.achievementID, app.Level >= t.lvl);
end
commonAchievementHandlers.LOREMASTER_CONTINENT_OnUpdate = function(t, mapID, ...)
if t.collectible and t.parent then
if not t.quests then
local quests, structures = LOREMASTER_CreateQuestsAndStructures(t, mapID, { ... });
if quests then
t.quests = quests;
t.structures = structures;
else
return;
end
end
return LOREMASTER_EXPLICIT_OnUpdate(t);
end
end
commonAchievementHandlers.LOREMASTER_OnUpdate = function(t, ...)
if t.collectible and t.parent then
local quests = t.quests;
if not quests then
quests = LOREMASTER_CreateQuests(t, { ... });
if quests then
t.quests = quests;
else
return;
end
end
return LOREMASTER_EXPLICIT_OnUpdate(t);
end
end
commonAchievementHandlers.LOREMASTER_OnTooltip = function(t, tooltipInfo)
if t.collectible and t.p and not t.collected then
tinsert(tooltipInfo, { left = " " });
tinsert(tooltipInfo, {
right = app.Modules.Color.GetProgressText(min(t.rank, t.p),t.rank),
r = 1, g = 1, b = 1
});
end
end
commonAchievementHandlers.META_ACHCAT_OnUpdate = function(t, achievementCategoryID)
if t.collectible then
if not t.achievements then
local achievements;
for i,o in ipairs(t.parent.g) do
if o.achievementCategoryID == achievementCategoryID then
achievements = o.g;
break;
end
end
if not achievements then return true; end
t.achievements = achievements;
end
local collected = true;
for i,faction in ipairs(t.achievements) do
if not faction.collected and app.RecursiveUnobtainableFilter(faction) then
collected = false;
break;
end
end
t:SetAchievementCollected(t.achievementID, collected);
end
end
commonAchievementHandlers.META_OnUpdate = function(t, ...)
if t.collectible then
if not t.achievements then
local achievements = {};
for i,achievementID in ipairs({ ... }) do
local f = SearchForField("achievementID", achievementID);
if #f > 0 then
tinsert(achievements, f[1]);
else
return true;
end
end
if #achievements < 1 then return true; end
t.achievements = achievements;
end
local collected = true;
for i,faction in ipairs(t.achievements) do
if not faction.collected then
collected = false;
break;
end
end
t:SetAchievementCollected(t.achievementID, collected);
end
end
commonAchievementHandlers.META_OnClick = function(row, button)
if button == "RightButton" then
local t = row.ref;
local clone = app:CreateMiniListForGroup(app.CreateAchievement(t[t.key], t.achievements)).data;
clone.description = t.description;
return true;
end
end
commonAchievementHandlers.META_OnTooltip = function(t, tooltipInfo)
if t.collectible and t.achievements then
tinsert(tooltipInfo, { left = " " });
for i,achievement in ipairs(t.achievements) do
tinsert(tooltipInfo, {
left = " |T" .. achievement.icon .. ":0|t " .. achievement.text,
right = app.L[achievement.collected and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"],
r = 1, g = 1, b = 1
});
end
end
end
commonAchievementHandlers.MOUNTS_OnUpdate = function(t)
if app.Settings.Collectibles.Mounts then
local count,r = 0,{};
for i,g in pairs(SearchForFieldContainer("spellID")) do
for j,o in ipairs(g) do
if ((o.f and o.f == app.FilterConstants.MOUNTS)
or (o.filterID and o.filterID == app.FilterConstants.MOUNTS)) and not r[i] then
if o.collected then count = count + 1; end
r[i] = 1;
break;
end
end
end
if t.rank > 1 then
t.progress = math.min(count, t.rank);
t.total = t.rank;
t.collectible = false;
if app.GroupFilter(t) then
local parent = t.parent;
parent.total = (parent.total or 0) + t.total;
parent.progress = (parent.progress or 0) + t.progress;
t.visible = (t.progress < t.total or app.CollectedItemVisibilityFilter(t));
else
t.visible = false;
end
else
t.collected = count >= 1;
t.collectible = true;
if app.GroupFilter(t) then
local parent = t.parent;
parent.total = (parent.total or 0) + 1;
if t.collected then parent.progress = (parent.progress or 0) + 1; end
t.visible = (not t.collected or app.CollectedItemVisibilityFilter(t));
else
t.visible = false;
end
end
else
t.collected = nil;
t.collectible = false;
t.progress = nil;
t.total = nil;
t.visible = false;
end
return true;
end
commonAchievementHandlers.MOUNTS_OnTooltip = function(t, tooltipInfo)
tinsert(tooltipInfo, { left = "Collect " .. t.rank .. " mounts." });
if t.total and t.progress < t.total and t.rank >= 25 then
tinsert(tooltipInfo, { left = " " });
local c = 0;
local template,r = {},{};
for i,o in pairs(SearchForFieldContainer("spellID")) do
local p = o[1];
if ((p.f and p.f == app.FilterConstants.MOUNTS)
or (p.filterID and p.filterID == app.FilterConstants.MOUNTS)) and not r[i] then
r[i] = 1;
if p.visible then
c = c + 1;
if c < 16 then
tinsert(tooltipInfo, {
left = " |T" .. p.icon .. ":0|t " .. p.text,
right = app.L[p.collected and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"],
r = 1, g = 1, b = 1
});
end
end
end
end
if c > 15 then tinsert(tooltipInfo, { left = " And " .. (c - 15) .. " more!" }); end
end
end
commonAchievementHandlers.REPUTATIONS_OnUpdate = function(t)
if app.Settings.Collectibles.Achievements then
local count = 0;
local ignored = app.IgnoredReputationsForAchievements;
if not ignored then
ignored = {[169] = 1};
app.IgnoredReputationsForAchievements = ignored;
end
for factionID,g in pairs(SearchForFieldContainer("factionID")) do
if not ignored[factionID] and #g > 0 and g[1].standing == 8 then
count = count + 1;
end
end
if t.rank > 1 then
t.progress = math.min(count, t.rank);
t.total = t.rank;
t.collectible = false;
if app.GroupFilter(t) then
local parent = t.parent;
parent.total = (parent.total or 0) + t.total;
parent.progress = (parent.progress or 0) + t.progress;
t.visible = (t.progress < t.total or app.CollectedItemVisibilityFilter(t));
else
t.visible = false;
end
else
t.collected = count >= 1;
t.collectible = true;
if app.GroupFilter(t) then
local parent = t.parent;
parent.total = (parent.total or 0) + 1;
if t.collected then parent.progress = (parent.progress or 0) + 1; end
t.visible = (not t.collected or app.CollectedItemVisibilityFilter(t));
else
t.visible = false;
end
end
else
t.collected = nil;
t.collectible = false;
t.progress = nil;
t.total = nil;
t.visible = false;
end
return true;
end
commonAchievementHandlers.REPUTATIONS_OnTooltip = function(t, tooltipInfo)
tinsert(tooltipInfo, { left = "Raise " .. t.rank .. " reputations to Exalted." });
if t.total and t.progress < t.total and t.rank >= 25 then
tinsert(tooltipInfo, { left = " " });
local ignored = app.IgnoredReputationsForAchievements;
if not ignored then
ignored = {[169] = 1};
app.IgnoredReputationsForAchievements = ignored;
end
for i,o in ipairs(app:GetDataCache().g) do
if o.headerID == app.HeaderConstants.FACTIONS then
for j,p in ipairs(o.g) do
if (p.visible or p.factionID == 909) and not ignored[p.factionID] and (not p.minReputation or (p.minReputation[1] == p.factionID and p.minReputation[2] >= 41999)) and (not p.maxReputation or (p.maxReputation[1] ~= p.factionID and p.reputation >= 0)) then
tinsert(tooltipInfo, {
left = " |T" .. p.icon .. ":0|t " .. p.text,
right = app.L[p.standing >= 8 and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"],
r = 1, g = 1, b = 1
});
end
end
end
end
end
end
end
app.CreateAchievementCategory = app.CreateClass("AchievementCategory", "achievementCategoryID", categoryFields);
end)();
-- Currency Lib
(function()
local CurrencyInfo = {};
local GetCurrencyCount;
---@diagnostic disable-next-line: undefined-global
local GetCurrencyLink = GetCurrencyLink;
local GetRelativeField = app.GetRelativeField;
if C_CurrencyInfo and C_CurrencyInfo.GetCurrencyInfo then
local C_CurrencyInfo_GetCurrencyInfo = C_CurrencyInfo.GetCurrencyInfo;
if C_CurrencyInfo.GetCurrencyLink then
GetCurrencyLink = C_CurrencyInfo.GetCurrencyLink;
end
setmetatable(CurrencyInfo, { __index = function(t, id)
local currencyInfo = C_CurrencyInfo_GetCurrencyInfo(id);
if currencyInfo then
local info = {
name = currencyInfo.name,
icon = currencyInfo.iconFileID
}
rawset(t, id, info);
return info;
end
end });
GetCurrencyCount = function(id)
return C_CurrencyInfo_GetCurrencyInfo(id).quantity or 0;
end
else
---@diagnostic disable-next-line: undefined-global
local GetCurrencyInfo = GetCurrencyInfo;
setmetatable(CurrencyInfo, { __index = function(t, id)
local name, amount, icon = GetCurrencyInfo(id);
if name then
local info = {
name = name,
icon = icon
}
rawset(t, id, info);
return info;
end
end });
GetCurrencyCount = function(id)
return select(2, GetCurrencyInfo(id)) or 0;
end
end
local CurrencyCollectibleAsCost = setmetatable({}, { __index = function(t, id)
local results = SearchForField("currencyIDAsCost", id, true);
if #results > 0 then
for _,ref in pairs(results) do
if ref.currencyID ~= id and app.RecursiveGroupRequirementsFilter(ref) then
if ref.collectible and not ref.collected then
t[id] = true;
return true;
elseif ref.total and ref.total > ref.progress then
t[id] = true;
return true;
end
end
end
end
t[id] = false;
return false;
end });
local CurrencyCollectedAsCost = setmetatable({}, { __index = function(t, id)
local any, partial;
local results = SearchForField("currencyIDAsCost", id, true);
if #results > 0 then
local count = GetCurrencyCount(id);
for _,ref in pairs(results) do
if ref.currencyID ~= id and app.RecursiveDefaultCharacterRequirementsFilter(ref) then
if ref.collectible and ref.collected ~= 1 then
if ref.cost then
for k,v in ipairs(ref.cost) do
if v[2] == id and v[1] == "c" then
if count >= (v[3] or 1) then
partial = true;
else
t[id] = false;
return false;
end
end
end
end
elseif (ref.total and ref.total > 0 and not GetRelativeField(t, "parent", ref) and ref.progress < ref.total) then
if ref.cost then
for k,v in ipairs(ref.cost) do
if v[2] == id and v[1] == "c" then
if count >= (v[3] or 1) then
partial = true;
else
t[id] = false;
return false;
end
end
end
end
end
any = true;
end
end
if any then
t[id] = partial and 2 or 1;
return partial and 2 or 1;
end
end
t[id] = false;
return false;
end });
app.AddEventHandler("OnRecalculate", function()
wipe(CurrencyCollectibleAsCost);
wipe(CurrencyCollectedAsCost);
end);
app.CreateCurrencyClass = app.CreateClass("Currency", "currencyID", {
["text"] = function(t)
return t.info.name;
end,
["icon"] = function(t)
return t.info.icon;
end,
["info"] = function(t)
local info = CurrencyInfo[t.currencyID];
if info then
t.info = info;
return info;
end
return {};
end,
["link"] = function(t)
return GetCurrencyLink(t.currencyID, 1);
end,
["collectible"] = function(t)
return t.collectibleAsCost;
end,
["collectibleAsCost"] = function(t)
if not t.parent or not t.parent.saved then
if CurrencyCollectibleAsCost[t.currencyID] then
return true;
elseif t.simplemeta then
setmetatable(t, t.simplemeta);
return false;
end
end
end,
["collected"] = function(t)
return t.collectedAsCost;
end,
["collectedAsCost"] = function(t)
return CurrencyCollectedAsCost[t.currencyID];
end,
});
end)();
-- Flight Path Lib
(function()
local function distance( x1, y1, x2, y2 )
return math.sqrt( (x2-x1)^2 + (y2-y1)^2 )
end
local arrOfNodes = {
1414, -- Kalimdor
1415, -- Eastern Kingdoms
1941, -- Eversong Woods (and Ghostlands + Isle of Quel'Danas)
1943, -- Azuremyst Isle (and Bloodmyst)
1944, -- Hellfire Peninsula (All of Outland)
-- TODO:
118, -- Icecrown (All of Northrend)
--422, -- Dread Wastes (All of Pandaria)
--525, -- Frostfire Ridge (All of Draenor)
--630, -- Azsuna (All of Broken Isles)
--882, -- Mac'Aree (All of Argus)
--862, -- Zuldazar (All of Zuldazar)
--896, -- Drustvar (All of Kul Tiras)
1209, -- Kalimdor
1208, -- Eastern Kingdoms
1467, -- Outland
1384, -- Northrend
1923, -- Pandaria
1922, -- Draenor
993, -- Broken Isles
994, -- Argus
1011, -- Zandalar
1014, -- Kul Tiras
1504, -- Nazjatar
1647, -- The Shadowlands
1409, -- Exile's Reach [Correct]
2046, -- Zereth Mortis
2057, -- Dragon Isles
2055, -- Sepulcher of the First Ones (has FPs inside)
2149, -- Ohn'ahran Plains [The Nokhud Offensive] (has FPs inside)
2175, -- Zaralek Cavern
};
app.AddEventHandler("OnReady", function()
-- Cache Flight Path Data once the addon is ready.
local newNodes, anyNew = {}, false;
for i,mapID in ipairs(arrOfNodes) do
local allNodeData = C_TaxiMap.GetTaxiNodesForMap(mapID);
if allNodeData then
for j,nodeData in ipairs(allNodeData) do
if nodeData.name then
AllTheThingsAD.LocalizedFlightPathNames[nodeData.nodeID] = nodeData.name;
if #SearchForField("flightPathID", nodeData.nodeID) < 1 then
newNodes[nodeData.nodeID] = nodeData.name;
anyNew = true;
end
end
end
end
end
if anyNew then
print("Found new flight path data:");
for i,name in pairs(newNodes) do
print(i, name);
end
SetDataMember("NewFlightPathData", newNodes);
end
end);
app.CacheFlightPathDataForMap = function(mapID, nodes)
local count = 0;
local temp = {};
for nodeID,_ in pairs(SearchForFieldContainer("flightPathID")) do
for i,node in ipairs(_) do
if not node.u and node.coords and node.coords[1][3] == mapID then
if not node.r or node.r == app.FactionID then
temp[nodeID] = node;
count = count + 1;
end
end
end
end
if count > 0 then
if count > 1 then
count = 0;
local mapID = app.CurrentMapID;
if mapID then
local pos = C_Map_GetPlayerMapPosition(mapID, "player");
if pos then
local px, py = pos:GetXY();
px, py = px * 100, py * 100;
-- Select the best flight path node.
for nodeID,node in pairs(temp) do
local coord = node.coords and node.coords[1];
if coord then
-- Allow for a little bit of leeway.
if distance(px, py, coord[1], coord[2]) < 0.6 then
nodes[nodeID] = true;
end
end
end
end
end
else
for nodeID,_ in pairs(temp) do
nodes[nodeID] = true;
end
end
end
return count;
end
app.CacheFlightPathDataForTarget = function(nodes)
local guid = UnitGUID("npc") or UnitGUID("target");
if guid then
---@diagnostic disable-next-line: undefined-field
local type, _, _, _, _, npcID = ("-"):split(guid);
if type == "Creature" and npcID then
npcID = tonumber(npcID);
local count = 0;
local searchResults = SearchForField("creatureID", npcID);
if searchResults and #searchResults > 0 then
for i,group in ipairs(searchResults) do
if group.flightPathID and not group.nmr and not group.nmc and (not group.u or group.u > 1) then
nodes[group.flightPathID] = true;
count = count + 1;
end
end
end
return count;
end
end
return 0;
end
app.CreateFlightPath = app.CreateClass("FlightPath", "flightPathID", {
["text"] = function(t)
return t.name;
end,
["name"] = function(t)
return AllTheThingsAD.LocalizedFlightPathNames[t.flightPathID] or "Visit the Flight Master to cache.";
end,
["icon"] = function(t)
local r = t.r;
if r then
if r == HORDE_FACTION_ID then
return app.asset("fp_horde");
else
return app.asset("fp_alliance");
end
end
return app.asset("fp_neutral");
end,
["description"] = function(t)
return "Flight paths are cached when you look at the flight master at each location.\n - Crieve";
end,
["collectible"] = function(t)
return app.Settings.Collectibles.FlightPaths;
end,
["collected"] = function(t)
if app.CurrentCharacter.FlightPaths[t.flightPathID] then return 1; end
if app.Settings.AccountWide.FlightPaths and ATTAccountWideData.FlightPaths[t.flightPathID] then return 2; end
if t.altQuests then
for i,questID in ipairs(t.altQuests) do
if IsQuestFlaggedCompleted(questID) then
return 2;
end
end
end
end,
["nmc"] = function(t)
local c = t.c;
if c and not contains(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,
});
app.events.GOSSIP_SHOW = function()
local knownNodeIDs = {};
if app.CacheFlightPathDataForTarget(knownNodeIDs) > 0 then
local any = false;
for nodeID,_ in pairs(knownNodeIDs) do
nodeID = tonumber(nodeID);
if not app.CurrentCharacter.FlightPaths[nodeID] then
local searchResults = SearchForField("flightPathID", nodeID);
app.SetCollected(#searchResults > 0 and searchResults[1], "FlightPaths", nodeID, true);
any = true;
end
end
if any then
app:RefreshDataQuietly("GOSSIP_SHOW", true);
end
end
end
app.events.TAXIMAP_OPENED = function()
local knownNodeIDs = {};
app.CacheFlightPathDataForTarget(knownNodeIDs);
app.CacheFlightPathDataForMap(app.CurrentMapID, knownNodeIDs);
-- apparently this can be nil somehow
if not app.CurrentMapID then return end
local allNodeData = C_TaxiMap.GetAllTaxiNodes(app.CurrentMapID);
if allNodeData then
for j,nodeData in ipairs(allNodeData) do
if nodeData.state and nodeData.state < 2 then
knownNodeIDs[nodeData.nodeID] = true;
end
end
end
local any = false;
for nodeID,_ in pairs(knownNodeIDs) do
nodeID = tonumber(nodeID);
if not app.CurrentCharacter.FlightPaths[nodeID] then
local searchResults = SearchForField("flightPathID", nodeID);
app.SetCollected(#searchResults > 0 and searchResults[1], "FlightPaths", nodeID, true);
any = true;
end
end
if any then
app:RefreshDataQuietly("TAXIMAP_OPENED", true);
end
end
end)();
-- NPC Lib
(function()
-- NPC Model Harvester (also acquires the displayID)
local npcModelHarvester = CreateFrame("DressUpModel", nil, UIParent);
npcModelHarvester:SetPoint("TOPRIGHT", UIParent, "BOTTOMRIGHT", 0, 0);
npcModelHarvester:SetSize(1, 1);
npcModelHarvester:Hide();
local 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);
app.HandleEvent("OnRenderDirty");
return displayID;
end
end
end});
app.NPCDisplayIDFromID = NPCDisplayIDFromID;
-- NPC & Title Name Harvesting Lib (https://us.battle.net/forums/en/wow/topic/20758497390?page=1#post-4, Thanks Gello!)
local NPCNameFromID, NPCTitlesFromID = {},{};
local blacklisted = {
[TOOLTIP_UNIT_LEVEL:format("??")] = true,
[TOOLTIP_UNIT_LEVEL_TYPE:format("??", ELITE)] = true,
};
local C_TooltipInfo_GetHyperlink = C_TooltipInfo and C_TooltipInfo.GetHyperlink;
if C_TooltipInfo_GetHyperlink then
setmetatable(NPCNameFromID, { __index = function(t, id)
if id > 0 then
---@diagnostic disable-next-line: undefined-field
local tooltipData = C_TooltipInfo_GetHyperlink(("unit:Creature-0-0-0-0-%d-0000000000"):format(id));
if tooltipData then
local title = tooltipData.lines[1].leftText;
if title and #tooltipData.lines > 2 then
local leftText = tooltipData.lines[2].leftText;
if leftText and not blacklisted[leftText] then
NPCTitlesFromID[id] = leftText;
end
end
if not IsRetrieving(title) then
t[id] = title;
return title;
end
end
else
return L.HEADER_NAMES[id];
end
end});
else
local ATTCNPCHarvester = CreateFrame("GameTooltip", "ATTCNPCHarvester", UIParent, "GameTooltipTemplate");
setmetatable(NPCNameFromID, { __index = function(t, id)
if id > 0 then
---@diagnostic disable-next-line: param-type-mismatch
ATTCNPCHarvester:SetOwner(UIParent,"ANCHOR_NONE")
---@diagnostic disable-next-line: param-type-mismatch, undefined-field
ATTCNPCHarvester:SetHyperlink(("unit:Creature-0-0-0-0-%d-0000000000"):format(id))
---@diagnostic disable-next-line: undefined-global
local title = ATTCNPCHarvesterTextLeft1:GetText();
---@diagnostic disable-next-line: param-type-mismatch
if title and ATTCNPCHarvester:NumLines() > 2 then
---@diagnostic disable-next-line: undefined-global
local leftText = ATTCNPCHarvesterTextLeft2:GetText();
if leftText and not blacklisted[leftText] then
NPCTitlesFromID[id] = leftText;
end
end
ATTCNPCHarvester:Hide();
if not IsRetrieving(title) then
t[id] = title;
return title;
end
else
return L.HEADER_NAMES[id];
end
end});
end
app.NPCNameFromID = NPCNameFromID;
app.NPCTitlesFromID = NPCTitlesFromID;
-- Event, Header, and NPC Lib
local createNPC = app.CreateClass("NPC", "npcID", {
["key"] = function(t)
return "npcID";
end,
["text"] = function(t)
return t.isRaid and ("|c" .. app.Colors.Raid .. t.name .. "|r") or t.name;
end,
["name"] = function(t)
return NPCNameFromID[t.npcID] or RETRIEVING_DATA;
end,
["icon"] = function(t)
return (t.parent and t.parent.headerID and t.parent.headerID == app.HeaderConstants.VENDORS and "Interface\\Icons\\INV_Misc_Coin_01") or app.GetRelativeDifficultyIcon(t);
end,
["title"] = function(t)
return 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,
},
"WithQuest", {
collectible = function(t)
return app.Settings.Collectibles.Quests and (not t.repeatable and not t.isBreadcrumb or C_QuestLog_IsOnQuest(t.questID));
end,
collected = function(t)
return IsQuestFlaggedCompletedForObject(t);
end,
trackable = app.ReturnTrue,
saved = function(t)
return IsQuestFlaggedCompletedForObject(t) == 1;
end
}, (function(t) return t.questID; end));
local createCustomHeader = app.CreateClass("Header", "headerID", {
["text"] = function(t)
return t.isRaid and ("|c" .. app.Colors.Raid .. t.name .. "|r") or t.name;
end,
["name"] = function(t)
return L["HEADER_NAMES"][t.headerID];
end,
["icon"] = function(t)
return L["HEADER_ICONS"][t.headerID] or app.asset("Category_Zones");
end,
["description"] = function(t)
return L["HEADER_DESCRIPTIONS"][t.headerID];
end,
["lore"] = function(t)
return L["HEADER_LORE"][t.headerID];
end,
["saved"] = function(t)
return IsQuestFlaggedCompletedForObject(t) == 1;
end,
["trackable"] = function(t)
return t.questID;
end,
["ignoreSourceLookup"] = function(t)
return true;
end,
},
"WithQuest", {
collectible = function(t)
return app.Settings.Collectibles.Quests and (not t.repeatable and not t.isBreadcrumb or C_QuestLog_IsOnQuest(t.questID));
end,
collected = function(t)
return IsQuestFlaggedCompletedForObject(t);
end,
trackable = app.ReturnTrue,
saved = function(t)
return IsQuestFlaggedCompletedForObject(t) == 1;
end
}, (function(t) return t.questID; end),
"WithReputation", {
collectible = function(t)
if app.Settings.Collectibles.Reputations then
return true;
end
end,
collected = function(t)
if GetFactionCurrentReputation(t.maxReputation[1]) >= t.maxReputation[2] then
return 1;
end
if app.Settings.AccountWide.Reputations then
local searchResults = SearchForField("factionID", t.maxReputation[1]);
if #searchResults > 0 then
for i,searchResult in ipairs(searchResults) do
if searchResult.key == "factionID" and searchResult.collected then
return 2;
end
end
end
end
end
}, (function(t) return t.maxReputation; end),
"WithEvent", app.Modules.Events.Fields, (function(t) return L.HEADER_EVENTS[t.headerID]; end));
app.CreateCustomHeader = createCustomHeader;
app.CreateNPC = function(id, t)
if not id then
print("Broken ID for CreateNPC");
t = {};
if t[1] then
t = { g = t };
end
t.OnUpdate = function()
print("HEY! FIX THIS", app.GenerateSourceHash(t, 0));
print(t.progress, t.total, t.g and #t.g);
end
id = 0;
end
if id < 1 then
if t and t.npcID == id then t.npcID = nil; end
return createCustomHeader(id, t);
else
return createNPC(id, t);
end
end
-- Automatic Headers
local HeaderTypeAbbreviations = {
["a"] = "achievementID",
["c"] = "classID",
["m"] = "mapID",
["n"] = "npcID",
["i"] = "itemID",
["o"] = "objectID",
["q"] = "questID",
["s"] = "spellID",
};
-- Alternate functions to attach data into a table based on an id for a given type code
local AlternateDataTypes = {
["ac"] = function(id)
return { text = GetCategoryInfo(id) };
end,
["crit"] = function(id)
local ach = math_floor(id);
local crit = math_floor(100 * (id - ach) + 0.005);
return { text = GetAchievementCriteriaInfo(ach, crit) };
end,
["d"] = function(id)
local name, _, _, _, _, _, _, _, _, _, textureFilename = GetLFGDungeonInfo(id);
return { text = name, icon = textureFilename };
end,
["df"] = function(id)
local aid = math_floor(id);
local hid = math_floor(10000 * (id - aid) + 0.005);
id = app.FactionID == Enum.FlightPathFaction.Alliance and tonumber(aid) or tonumber(hid);
local name, _, _, _, _, _, _, _, _, _, textureFilename = GetLFGDungeonInfo(id);
return { text = name, icon = textureFilename };
end,
["_G"] = function(id)
return { ["text"] = _G[id] };
end,
};
app.CreateHeader = app.CreateClass("AutomaticHeader", "autoID", {
["text"] = function(t)
return t.isRaid and ("|c" .. app.Colors.Raid .. t.name .. "|r") or t.name;
end,
["name"] = function(t)
return t.result.name or t.result.text;
end,
["icon"] = function(t)
return t.result.icon;
end,
["result"] = function(t)
local typ = HeaderTypeAbbreviations[t.type];
if typ then
local cache = SearchForField(typ, t.autoID);
if #cache > 0 then
for i,o in ipairs(cache) do
if o.key == typ then
t.result = o;
return o;
end
end
end
cache = CloneClassInstance({[typ] = t.autoID,key = typ});
t.result = cache;
return cache;
else
local cache = AlternateDataTypes[t.type];
if cache then
cache = cache(t.autoID);
if cache then
t.result = cache;
return cache;
end
end
end
print("Unhandled Header Type", t.type, t.autoID, typ);
return app.EmptyTable;
end,
["ignoreSourceLookup"] = function(t)
return true;
end,
},
"WithQuest", {
collectible = function(t)
return app.Settings.Collectibles.Quests and (not t.repeatable and not t.isBreadcrumb or C_QuestLog_IsOnQuest(t.questID));
end,
collected = function(t)
return IsQuestFlaggedCompletedForObject(t);
end,
trackable = app.ReturnTrue,
saved = function(t)
return IsQuestFlaggedCompletedForObject(t) == 1;
end
}, (function(t) return t.questID; end));
end)();
-- Profession Lib
(function()
app.SkillIDToSpellID = setmetatable({
[171] = 2259, -- Alchemy
[794] = 78670, -- 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
[165] = 2108, -- Leatherworking
[186] = 2575, -- Mining
[393] = 8613, -- Skinning
[197] = 3908, -- Tailoring
[960] = 53428, -- Runeforging
[40] = 2842, -- Poisons
[633] = 1809, -- Lockpicking
[921] = 921, -- Pickpocketing
-- Riding
[762] = 33388, -- Riding
-- 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
}, {__index = function(t,k) return k; end});
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 BLACKSMITHING = ATTC.SkillIDToSpellID[164];
local LEATHERWORKING = ATTC.SkillIDToSpellID[165];
local TAILORING = ATTC.SkillIDToSpellID[197];
app.OnUpdateForCrafter = function(t)
t.visible = nil;
t.collectible = nil;
if app.MODE_DEBUG_OR_ACCOUNT then
return false;
else
local skills = app.CurrentCharacter.ActiveSkills;
if skills[BLACKSMITHING] or skills[LEATHERWORKING] or skills[TAILORING] then
return false;
end
t.collectible = false;
t.visible = false;
return true;
end
end;
app.OnUpdateForOmarionsHandbook = function(t)
t.visible = true;
t.collectible = nil;
if app.MODE_DEBUG_OR_ACCOUNT or IsQuestFlaggedCompleted(9233) or C_QuestLog_IsOnQuest(9233) then
return false;
else
for spellID,skills in pairs(app.CurrentCharacter.ActiveSkills) do
if (spellID == BLACKSMITHING or spellID == LEATHERWORKING or spellID == TAILORING) and skills[1] > 270 then
t.collectible = false;
t.visible = false;
return true;
end
end
end
end;
app.CreateProfession = app.CreateClass("Profession", "professionID", {
["text"] = function(t)
return GetSpellName(t.spellID);
end,
["icon"] = function(t)
return GetSpellIcon(t.spellID);
end,
["spellID"] = function(t)
return app.SkillIDToSpellID[t.professionID];
end,
["requireSkill"] = function(t)
return t.professionID;
end,
["ignoreSourceLookup"] = function(t)
return true;
end,
["sym"] = function(t)
return {{"selectprofession", t.professionID}};
end
});
end)();
-- Recipe & Spell Lib
(function()
local grey = RGBToHex(0.75, 0.75, 0.75);
local craftColors = {
RGBToHex(0.25,0.75,0.25),
RGBToHex(1,1,0),
RGBToHex(1,0.5,0.25),
[0]=grey,
}
local CraftTypeIDToColor = function(craftTypeID)
return craftColors[craftTypeID] or grey;
end
app.CraftTypeToCraftTypeID = function(craftType)
if craftType then
if craftType == "optimal" then
return 3;
elseif craftType == "medium" then
return 2;
elseif craftType == "easy" then
return 1;
elseif craftType == "trivial" then
return 0;
end
end
return nil;
end
local MaxSpellRankPerSpellName = {};
local SpellIDToSpellName = {};
app.GetSpellName = function(spellID, rank)
local spellName = rawget(SpellIDToSpellName, spellID);
if spellName then return spellName; end
if rank then
spellName = GetSpellName(spellID, rank);
else
spellName = GetSpellName(spellID);
end
if not IsRetrieving(spellName) then
if not rawget(app.SpellNameToSpellID, spellName) then
rawset(app.SpellNameToSpellID, spellName, spellID);
if not rawget(SpellIDToSpellName, spellID) then
rawset(SpellIDToSpellName, spellID, spellName);
end
end
if rank then
if (rawget(MaxSpellRankPerSpellName, spellName) or 0) < rank then
rawset(MaxSpellRankPerSpellName, spellName, rank);
end
spellName = spellName .. " (" .. RANK .. " " .. rank .. ")";
if not rawget(app.SpellNameToSpellID, spellName) then
rawset(app.SpellNameToSpellID, spellName, spellID);
if not rawget(SpellIDToSpellName, spellID) then
rawset(SpellIDToSpellName, spellID, spellName);
end
end
end
return spellName;
end
end
local isSpellKnownHelper = function(spellID)
return spellID and (IsPlayerSpell(spellID) or IsSpellKnown(spellID, true) or IsSpellKnownOrOverridesKnown(spellID, true));
end
app.IsSpellKnown = function(spellID, rank, ignoreHigherRanks)
if isSpellKnownHelper(spellID) then return true; end
if rank then
local spellName = GetSpellName(spellID);
if spellName then
local maxRank = ignoreHigherRanks and rank or rawget(MaxSpellRankPerSpellName, spellName);
if maxRank then
spellName = spellName .. " (" .. RANK .. " ";
for i=maxRank,rank,-1 do
if isSpellKnownHelper(app.SpellNameToSpellID[spellName .. i .. ")"]) then
return true;
end
end
end
end
end
end
app.SpellNameToSpellID = setmetatable(L.SPELL_NAME_TO_SPELL_ID, {
__index = function(t, key)
for _,spellID in pairs(app.SkillIDToSpellID) do
app.GetSpellName(spellID);
end
for specID,spellID in pairs(app.SpecializationSpellIDs) do
app.GetSpellName(spellID);
end
for spellID,g in pairs(SearchForFieldContainer("spellID")) do
local rank;
for i,o in ipairs(g) do
if o.rank then
rank = o.rank;
break;
end
end
app.GetSpellName(spellID, rank);
end
local numSpellTabs, offset, lastSpellName, currentSpellRank, lastSpellRank = GetNumSpellTabs(), 1, "", nil, 1;
for spellTabIndex=1,numSpellTabs do
local numSpells = select(4, GetSpellTabInfo(spellTabIndex));
for spellIndex=1,numSpells do
local spellName, _, _, _, _, _, spellID = GetSpellInfo(offset, BOOKTYPE_SPELL);
if spellName then
currentSpellRank = GetSpellRank(spellID);
if not currentSpellRank then
if lastSpellName == spellName then
currentSpellRank = lastSpellRank + 1;
else
lastSpellName = spellName;
currentSpellRank = 1;
end
end
app.GetSpellName(spellID, currentSpellRank);
if not rawget(t, spellName) then
rawset(t, spellName, spellID);
end
lastSpellRank = currentSpellRank;
offset = offset + 1;
end
end
end
return rawget(t, key);
end
});
-- The difference between a recipe and a spell is that a spell is not collectible.
local baseIconFromSpellID = function(t)
return GetSpellIcon(t.spellID) or (t.requireSkill and GetSpellIcon(t.requireSkill));
end;
local linkFromSpellID = function(t)
local link = GetSpellLink(t.spellID);
if not link then
if GetRelativeValue(t, "requireSkill") == 333 then
return "|cffffffff|Henchant:" .. t.spellID .. "|h[" .. t.name .. "]|h|r";
else
return "|cffffffff|Hspell:" .. t.spellID .. "|h[" .. t.name .. "]|h|r";
end
end
return link;
end;
local nameFromSpellID = function(t)
return app.GetSpellName(t.spellID) or GetSpellLink(t.spellID) or RETRIEVING_DATA;
end;
local spellFields = {
["text"] = function(t)
return t.link;
end,
["craftText"] = function(t)
return Colorize(t.name, CraftTypeIDToColor(t.craftTypeID));
end,
["icon"] = function(t)
local icon = t.baseIcon;
if icon and icon ~= 136235 and icon ~= 136192 then
return icon;
end
return "Interface\\ICONS\\INV_Scroll_04";
end,
["description"] = app.GameBuildVersion < 20000 and function(t)
return GetSpellDescription(t.spellID);
end,
["craftTypeID"] = function(t)
return app.CurrentCharacter.SpellRanks[t.spellID] or 0;
end,
["trackable"] = function(t)
return GetSpellCooldown(t.spellID) > 0;
end,
["saved"] = function(t)
return GetSpellCooldown(t.spellID) > 0 and 1;
end,
["baseIcon"] = baseIconFromSpellID,
["name"] = nameFromSpellID,
["link"] = linkFromSpellID,
};
local createSpell = app.CreateClass("Spell", "spellID", spellFields);
local recipeFields = app.CloneDictionary(spellFields);
recipeFields.collectible = function(t)
return app.Settings.Collectibles.Recipes;
end;
recipeFields.collected = function(t)
if app.CurrentCharacter.Spells[t.spellID] then return 1; end
local isKnown = not t.nmc and app.IsSpellKnown(t.spellID, t.rank, GetRelativeValue(t, "requireSkill") == 261);
return app.SetCollectedForSubType(t, "Spells", "Recipes", t.spellID, isKnown);
end;
recipeFields.f = function(t)
return app.FilterConstants.RECIPES;
end;
recipeFields.IsClassIsolated = true;
local createRecipe = app.CreateClass("Recipe", "spellID", recipeFields,
"WithItem", {
baseIcon = function(t)
return GetItemIcon(t.itemID) or baseIconFromSpellID(t);
end,
link = function(t)
return select(2, GetItemInfo(t.itemID));
end,
name = function(t)
return GetItemInfo(t.itemID) or nameFromSpellID(t);
end,
tsm = function(t)
---@diagnostic disable-next-line: undefined-field
return ("i:%d"):format(t.itemID);
end,
b = function(t)
return app.Settings.AccountWide.Recipes and 2;
end,
}, (function(t) return t.itemID; end));
local createItem = app.CreateItem; -- Temporary Recipe fix until someone fixes parser.
app.CreateItem = function(id, t)
if t and t.spellID and t.f == app.FilterConstants.RECIPES then -- This is pretty slow, would be great it someone fixes it.
t.f = nil;
t.itemID = id;
return createRecipe(t.spellID, t);
end
return createItem(id, t);
end
app.CreateRecipe = createRecipe;
app.CreateSpell = function(id, t)
if t and t.itemID then
return createRecipe(id, t);
else
return createSpell(id, t);
end
end
end)();
-- Companion Lib
(function()
local SetBattlePetCollected = function(t, speciesID, collected)
return app.SetCollected(t, "BattlePets", speciesID, collected);
end
local SetMountCollected = function(t, spellID, collected)
return app.SetCollectedForSubType(t, "Spells", "Mounts", spellID, collected);
end
local speciesFields = {
["f"] = function(t)
return app.FilterConstants.BATTLE_PETS;
end,
["collectible"] = function(t)
return app.Settings.Collectibles.BattlePets;
end,
["text"] = function(t)
return "|cff0070dd" .. (t.name or RETRIEVING_DATA) .. "|r";
end,
["link"] = function(t)
if t.itemID then
local link = select(2, GetItemInfo(t.itemID));
if link and not IsRetrieving(link) then
t.link = link;
return link;
end
end
end,
["tsm"] = function(t)
---@diagnostic disable-next-line: undefined-field
if t.itemID then return ("i:%d"):format(t.itemID); end
---@diagnostic disable-next-line: undefined-field
return ("p:%d:1:3"):format(t.speciesID);
end,
};
local mountFields = {
IsClassIsolated = true,
["text"] = function(t)
return "|cffb19cd9" .. t.name .. "|r";
end,
["icon"] = function(t)
return GetSpellIcon(t.spellID);
end,
["link"] = function(t)
return (t.itemID and select(2, GetItemInfo(t.itemID))) or GetSpellLink(t.spellID);
end,
["f"] = function(t)
return app.FilterConstants.MOUNTS;
end,
["collectible"] = function(t)
return app.Settings.Collectibles.Mounts;
end,
["explicitlyCollected"] = function(t)
return IsSpellKnown(t.spellID) or (t.questID and IsQuestFlaggedCompleted(t.questID)) or (t.itemID and GetItemCount(t.itemID, true) > 0);
end,
["b"] = function(t)
return (t.parent and t.parent.b) or 1;
end,
["name"] = function(t)
return GetSpellName(t.spellID) or RETRIEVING_DATA;
end,
["tsmForItem"] = function(t)
---@diagnostic disable-next-line: undefined-field
if t.itemID then return ("i:%d"):format(t.itemID); end
---@diagnostic disable-next-line: undefined-field
if t.parent and t.parent.itemID then return ("i:%d"):format(t.parent.itemID); end
end,
["linkForItem"] = function(t)
return select(2, GetItemInfo(t.itemID)) or GetSpellLink(t.spellID);
end,
};
if C_PetJournal and app.GameBuildVersion > 30000 then
local C_PetJournal = _G["C_PetJournal"];
-- Once the Pet Journal API is available, then all pets become account wide.
SetBattlePetCollected = function(t, speciesID, collected)
if collected then
return app.SetAccountCollected(t, "BattlePets", speciesID, collected);
else
-- Stop turning it off, dumbass Blizzard API.
return app.IsAccountCached("BattlePets", speciesID);
end
end
speciesFields.icon = function(t)
return select(2, C_PetJournal.GetPetInfoBySpeciesID(t.speciesID));
end
speciesFields.name = function(t)
return C_PetJournal.GetPetInfoBySpeciesID(t.speciesID) or (t.itemID and GetItemInfo(t.itemID)) or RETRIEVING_DATA;
end
speciesFields.petTypeID = function(t)
return select(3, C_PetJournal.GetPetInfoBySpeciesID(t.speciesID));
end
speciesFields.displayID = function(t)
return select(12, C_PetJournal.GetPetInfoBySpeciesID(t.speciesID));
end
speciesFields.description = function(t)
return select(6, C_PetJournal.GetPetInfoBySpeciesID(t.speciesID));
end
speciesFields.collected = function(t)
local count = C_PetJournal.GetNumCollectedInfo(t.speciesID);
return SetBattlePetCollected(t, t.speciesID, count and count > 0);
end
local C_MountJournal = _G["C_MountJournal"];
if C_MountJournal then
-- Once the Mount Journal API is available, then all mounts become account wide.
SetMountCollected = function(t, spellID, collected)
return app.SetAccountCollectedForSubType(t, "Spells", "Mounts", spellID, collected);
end
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 });
mountFields.mountID = function(t)
return SpellIDToMountID[t.spellID];
end
mountFields.name = function(t)
local mountID = t.mountID;
if mountID then return C_MountJournal.GetMountInfoByID(mountID); end
return GetSpellName(t.spellID) or RETRIEVING_DATA;
end
mountFields.displayID = function(t)
local mountID = t.mountID;
if mountID then return C_MountJournal.GetMountInfoExtraByID(mountID); end
end
mountFields.lore = function(t)
local mountID = t.mountID;
if mountID then return select(2, C_MountJournal.GetMountInfoExtraByID(mountID)); end
end
mountFields.collected = function(t)
local mountID = t.mountID;
if mountID then
local _, spellID, _, _, _, _, _, _, _, _, isCollected = C_MountJournal.GetMountInfoByID(mountID);
return SetMountCollected(t, spellID, isCollected);
else
local spellID = t.spellID;
for i,o in ipairs(SearchForField("spellID", spellID)) do
if o.explicitlyCollected then
return SetMountCollected(t, spellID, true);
end
end
return SetMountCollected(t, spellID, false);
end
end
else
mountFields.name = function(t)
return GetSpellName(t.spellID) or RETRIEVING_DATA;
end
mountFields.collected = function(t)
local spellID = t.spellID;
for i,o in ipairs(SearchForField("spellID", spellID)) do
if o.explicitlyCollected then
return SetMountCollected(t, spellID, true);
end
end
return SetMountCollected(t, spellID, false);
end
end
else
speciesFields.icon = function(t)
if t.itemID then
return GetItemIcon(t.itemID) or "Interface\\Icons\\INV_Misc_QuestionMark";
end
return "Interface\\Icons\\INV_Misc_QuestionMark";
end
speciesFields.name = function(t)
return t.itemID and GetItemInfo(t.itemID) or RETRIEVING_DATA;
end
mountFields.name = function(t)
return GetSpellName(t.spellID) or RETRIEVING_DATA;
end
if GetCompanionInfo and GetNumCompanions("CRITTER") ~= nil then
local CollectedBattlePetHelper = {};
local CollectedMountHelper = {};
local function RefreshCompanionCollectionStatus(companionType)
local anythingNew = false;
if not companionType or companionType == "CRITTER" then
setmetatable(CollectedBattlePetHelper, nil);
local critterCount = GetNumCompanions("CRITTER");
if not critterCount then
print("Failed to get Companion Info for Critters");
else
for i=critterCount,1,-1 do
local spellID = select(3, GetCompanionInfo("CRITTER", i));
if spellID then
if not CollectedBattlePetHelper[spellID] then
CollectedBattlePetHelper[spellID] = true;
anythingNew = true;
end
else
print("Failed to get Companion Info for Critter ".. i);
end
end
end
end
if not companionType or companionType == "MOUNT" then
setmetatable(CollectedMountHelper, nil);
for i=GetNumCompanions("MOUNT"),1,-1 do
local spellID = select(3, GetCompanionInfo("MOUNT", i));
if spellID then
if not CollectedMountHelper[spellID] then
CollectedMountHelper[spellID] = true;
anythingNew = true;
end
else
print("Failed to get Companion Info for Mount ".. i);
end
end
end
if anythingNew then app:RefreshDataQuietly("RefreshCompanionCollectionStatus", true); end
end
local meta = { __index = function(t, spellID)
RefreshCompanionCollectionStatus();
return rawget(t, spellID);
end };
setmetatable(CollectedBattlePetHelper, meta);
setmetatable(CollectedMountHelper, meta);
speciesFields.collected = function(t)
return SetBattlePetCollected(t, t.speciesID, (t.spellID and CollectedBattlePetHelper[t.spellID]));
end
mountFields.collected = function(t)
return SetMountCollected(t, t.spellID, (t.spellID and CollectedMountHelper[t.spellID]));
end
app:RegisterEvent("COMPANION_LEARNED");
app:RegisterEvent("COMPANION_UNLEARNED");
app:RegisterEvent("COMPANION_UPDATE");
app.events.COMPANION_LEARNED = RefreshCompanionCollectionStatus;
app.events.COMPANION_UNLEARNED = RefreshCompanionCollectionStatus;
app.events.COMPANION_UPDATE = RefreshCompanionCollectionStatus;
else
speciesFields.collected = function(t)
return SetBattlePetCollected(t, t.speciesID, t.itemID and GetItemCount(t.itemID, true) > 0);
end
mountFields.collected = function(t)
local spellID = t.spellID;
for i,o in ipairs(SearchForField("spellID", spellID)) do
if o.explicitlyCollected then
return SetMountCollected(t, spellID, true);
end
end
return SetMountCollected(t, spellID, false);
end
end
end
app.CreateMount = app.CreateClass("Mount", "spellID", mountFields,
"WithItem", { -- This is a conditional contructor.
link = mountFields.linkForItem;
tsm = mountFields.tsmForItem
}, function(t) return t.itemID; end);
app.CreatePetType = app.CreateClass("PetType", "petTypeID", {
["text"] = function(t)
return _G["BATTLE_PET_NAME_" .. t.petTypeID];
end,
["icon"] = function(t)
return app.asset("Icon_PetFamily_"..PET_TYPE_SUFFIX[t.petTypeID]);
end,
});
app.CreateSpecies = app.CreateClass("Species", "speciesID", speciesFields);
end)();
-- Unsupported Libs
(function()
app.CreateMusicRoll = app.CreateUnimplementedClass("MusicRoll", "questID");
app.CreatePetAbility = app.CreateUnimplementedClass("PetAbility", "petAbilityID");
app.CreateSelfieFilter = app.CreateUnimplementedClass("SelfieFilter", "questID");
end)();
-- TomTom Support
local AttachTooltipSearchResults = app.Modules.Tooltip.AttachTooltipSearchResults;
local __TomTomWaypointCacheIndexY = { __index = function(t, y)
local o = {};
rawset(t, y, o);
return o;
end };
local __TomTomWaypointCacheIndexX = { __index = function(t, x)
local o = setmetatable({}, __TomTomWaypointCacheIndexY);
rawset(t, x, o);
return o;
end };
local __TomTomWaypointCache = setmetatable({}, { __index = function(t, mapID)
local o = setmetatable({}, __TomTomWaypointCacheIndexX);
rawset(t, mapID, o);
return o;
end });
local __TomTomWaypointFirst = nil;
local function AddTomTomWaypointCache(coord, group)
local mapID = coord[3];
if mapID then
__TomTomWaypointCache[mapID][math_floor(coord[1] * 10)][math_floor(coord[2] * 10)][group.key .. ":" .. group[group.key]] = group;
else
print("Missing mapID for", group.text, coord[1], coord[2], mapID);
end
end
local function AddTomTomWaypointInternal(group, depth)
if group.visible then
if group.plotting then return false; end
group.plotting = true;
if group.g then
depth = depth + 1;
for _,o in ipairs(group.g) do
AddTomTomWaypointInternal(o, depth);
end
depth = depth - 1;
end
local searchResults = ResolveSymbolicLink(group);
if searchResults then
depth = depth + 1;
for _,o in ipairs(searchResults) do
AddTomTomWaypointInternal(o, depth);
end
depth = depth - 1;
end
group.plotting = nil;
if TomTom then
if (depth == 0 and not __TomTomWaypointFirst) or not group.saved then
if group.coords or group.coord then
__TomTomWaypointFirst = false;
if group.coords then
for _,coord in ipairs(group.coords) do
AddTomTomWaypointCache(coord, group);
end
end
if group.coord then AddTomTomWaypointCache(group.coord, group); end
end
end
elseif C_SuperTrack then
if depth == 0 or __TomTomWaypointFirst then
local coord = group.coords and group.coords[1] or group.coord;
if coord then
__TomTomWaypointFirst = false;
C_SuperTrack.SetSuperTrackedUserWaypoint(false);
C_Map.ClearUserWaypoint();
C_Map.SetUserWaypoint(UiMapPoint.CreateFromCoordinates(coord[3],coord[1]/100,coord[2]/100));
C_SuperTrack.SetSuperTrackedUserWaypoint(true);
end
end
end
end
end
local function AddTomTomWaypoint(group)
if TomTom or C_SuperTrack then
__TomTomWaypointFirst = true;
wipe(__TomTomWaypointCache);
AddTomTomWaypointInternal(group, 0);
if TomTom then
local xnormal;
for mapID,c in pairs(__TomTomWaypointCache) do
for x,d in pairs(c) do
xnormal = x / 1000;
for y,datas in pairs(d) do
-- Determine the Root and simplify NPC/Object data.
-- An NPC/Object can contain all of the other types by reference and don't need individual entries.
local root,rootByCreatureID,rootByObjectID = {},{},{};
for key,group in pairs(datas) do
local creatureID, objectID;
if group.npcID or group.creatureID then
creatureID = group.npcID or group.creatureID;
elseif group.objectID then
objectID = group.objectID;
else
if group.providers then
for i,provider in ipairs(group.providers) do
if provider[1] == "n" then
if provider[2] > 0 then
creatureID = provider[2];
end
elseif provider[1] == "o" then
if provider[2] > 0 then
objectID = provider[2];
end
end
end
end
if group.qgs then
local count = #group.qgs;
if count > 1 and group.coords and #group.coords == count then
for i=count,1,-1 do
local coord = group.coords[i];
if coord[3] == mapID and math_floor(coord[1] * 10) == x and math_floor(coord[2] * 10) == y then
creatureID = group.qgs[i];
break;
end
end
if not creatureID then
creatureID = group.qgs[1];
end
else
creatureID = group.qgs[1];
end
end
if group.crs then
local count = #group.crs;
if count > 1 and group.coords and #group.coords == count then
for i=count,1,-1 do
local coord = group.coords[i];
if coord[3] == mapID and math_floor(coord[1] * 10) == x and math_floor(coord[2] * 10) == y then
creatureID = group.crs[i];
break;
end
end
if not creatureID then
creatureID = group.crs[1];
end
else
creatureID = group.crs[1];
end
end
end
if creatureID then
if not rootByCreatureID[creatureID] then
rootByCreatureID[creatureID] = group;
tinsert(root, app.CreateNPC(creatureID));
end
elseif objectID then
if not rootByObjectID[objectID] then
rootByObjectID[objectID] = group;
tinsert(root, app.CreateObject(objectID));
end
else
tinsert(root, group);
end
end
local first = root[1];
if first then
local sourcePath = app.GenerateSourceHash(first);
for i=2,#root,1 do sourcePath = sourcePath .. ";" .. app.GenerateSourceHash(root[i]); end
TomTom:AddWaypoint(mapID, xnormal, y / 1000, {
from = "ATT",
persistent = true,
sourcePath = sourcePath,
title = (first.text or RETRIEVING_DATA)
}, root);
end
end
end
end
if TomTom.SetClosestWaypoint then
TomTom:SetClosestWaypoint();
end
end
if C_SuperTrack and group.questID and C_QuestLog_IsOnQuest(group.questID) then
C_SuperTrack.SetSuperTrackedQuestID(group.questID);
end
else
app.print("You must have TomTom installed to plot coordinates.");
end
end
app.AddTomTomWaypoint = AddTomTomWaypoint;
app.AddEventHandler("OnReady", function()
local tomTom = TomTom;
if tomTom then
local oldAddWaypoint = tomTom.AddWaypoint;
tomTom.AddWaypoint = function(self, m, x, y, opts, root, ...)
if opts and (opts.from == "ATT" and opts.sourcePath) then
local sourceString = opts.sourcePath;
if sourceString then
if not root then
root = {};
---@diagnostic disable-next-line: undefined-field
local sourceStrings = { (";"):split(sourceString) };
for i,sourcePath in ipairs(sourceStrings) do
---@diagnostic disable-next-line: undefined-field
local hashes = { (">"):split(sourcePath) };
local ref = app.SearchForSourcePath(app:GetDataCache().g, hashes, 2, #hashes);
if ref then
tinsert(root, ref);
else
---@diagnostic disable-next-line: undefined-field
hashes = { ("ID"):split(sourcePath) };
if #hashes == 3 then
ref = app.CreateClassInstance(hashes[1] .. "ID", tonumber(hashes[3]));
if ref then tinsert(root, ref); end
end
end
end
end
if #root > 0 then
local first = root[1];
if IsRetrieving(opts.title) then
opts.title = first.text or RETRIEVING_DATA;
end
local displayID = GetDisplayID(first);
if displayID then
opts.minimap_displayID = displayID;
opts.worldmap_displayID = displayID;
end
if first.icon then
opts.minimap_icon = first.icon;
opts.worldmap_icon = first.icon;
end
---@diagnostic disable-next-line: undefined-global
if TomTom.DefaultCallbacks then
---@diagnostic disable-next-line: undefined-global
local callbacks = TomTom:DefaultCallbacks();
callbacks.minimap.tooltip_update = nil;
callbacks.minimap.tooltip_show = function(event, tooltip, uid, dist)
tooltip:ClearLines();
for i,o in ipairs(root) do
local line = tooltip:NumLines() + 1;
tooltip:AddLine(o.text);
if o.title and not o.explorationID then tooltip:AddLine(o.title); end
local key = o.key;
if key == "objectiveID" then
if o.parent and o.parent.questID then tooltip:AddLine("Objective for " .. o.parent.text); end
elseif key == "criteriaID" then
tooltip:AddDoubleLine(L.CRITERIA_FOR, GetAchievementLink(o.achievementID));
else
if key == "npcID" then key = "creatureID"; end
AttachTooltipSearchResults(tooltip, line, SearchForField, key, o[o.key]);
end
end
tooltip:Show();
end
callbacks.world.tooltip_update = nil;
callbacks.world.tooltip_show = callbacks.minimap.tooltip_show;
opts.callbacks = callbacks;
end
else
print("Failed to rebuild TomTom Waypoint");
end
end
end
return oldAddWaypoint(self, m, x, y, opts, root, ...);
end
local function AreAnyATTWaypointsPersisted()
-- If there are any persisted waypoints, recover their tooltips
local waypointsByMapID = tomTom.waypoints;
if not waypointsByMapID then return false; end
local any = false;
for mapID,waypointsByMap in pairs(waypointsByMapID) do
for waypointUID,waypoint in pairs(waypointsByMap) do
if waypoint.from == "ATT" then
return true;
end
end
end
return false;
end
if AreAnyATTWaypointsPersisted() then tomTom:ReloadWaypoints(); end
end
end);
-- Clickable ATT Chat Link Handling
(function()
hooksecurefunc("SetItemRef", function(link, text)
-- print("Chat Link Click",link,string_gsub(text, "\|","&"));
---@diagnostic disable-next-line: undefined-field
local type, info, data1, data2, data3 = (":"):split(link);
--print(type, info, data1, data2, data3)
if type == "addon" and info == "ATT" then
local op = link:sub(17)
--print("ATT Link",op)
-- local type, paramA, paramB = (":"):split(data);
-- print(type,paramA,paramB)
if data1 == "search" then
local cmd = data2 .. ":" .. data3;
app.SetSkipLevel(2);
local group = app.GetCachedSearchResults(app.SearchForLink, cmd);
app.SetSkipLevel(0);
app:CreateMiniListForGroup(group);
return true;
elseif data1 == "dialog" then
return app:TriggerReportDialog(data2);
end
end
end);
-- Turns a bit of text into a colored link which ATT will attempt to understand
function app:Linkify(text, color, operation)
text = "|Haddon:ATT:"..operation.."|h|c"..color.."["..text.."]|r|h";
return text;
end
function app:WaypointLink(mapID, x, y, text)
return "|cffffff00|Hworldmap:" .. mapID .. ":" .. math_floor(x * 10000) .. ":" .. math_floor(y * 10000)
.. "|h[|A:Waypoint-MapPin-ChatIcon:13:13:0:0|a" .. (text or "") .. "]|h|r";
end
-- Stores some information for use by a report popup by id
local reports = {};
function app:SetupReportDialog(id, reportMessage, text)
if not reports[id] then
-- print("Setup Report", id, reportMessage)
reports[id] = {
msg = reportMessage,
text = (type(text) == "table" and app.TableConcat(text, nil, "", "\n") or text)
};
return true;
end
end
-- Retrieves stored information for a report dialog and attempts to display the dialog if possible
function app:TriggerReportDialog(id)
local popup = reports[id];
if popup then
app:ShowPopupDialogToReport(popup.msg, popup.text);
return true;
end
end
end)();
-- Startup Event
local ADDON_LOADED_HANDLERS = {
[appName] = function()
AllTheThingsAD = _G["AllTheThingsAD"]; -- For account-wide data.
if not AllTheThingsAD then
AllTheThingsAD = { };
_G["AllTheThingsAD"] = AllTheThingsAD;
end
-- Cache the Localized Category Data
AllTheThingsAD.LocalizedCategoryNames = setmetatable(AllTheThingsAD.LocalizedCategoryNames or {}, { __index = app.CategoryNames });
app.CategoryNames = nil;
-- Cache the Localized Flight Path Data
AllTheThingsAD.LocalizedFlightPathNames = setmetatable(AllTheThingsAD.LocalizedFlightPathNames or {}, { __index = app.FlightPathNames });
app.FlightPathDB = nil;
-- Character Data Storage
local characterData = ATTCharacterData;
if not characterData then
characterData = {};
ATTCharacterData = characterData;
end
local currentCharacter = characterData[app.GUID];
if not currentCharacter then
currentCharacter = {};
characterData[app.GUID] = currentCharacter;
end
local name, realm = UnitName("player");
if not realm then realm = GetRealmName(); end
if name then currentCharacter.name = name; end
if realm then currentCharacter.realm = realm; end
if app.Me then currentCharacter.text = app.Me; end
if app.GUID then currentCharacter.guid = app.GUID; end
if app.Level then currentCharacter.lvl = app.Level; end
if app.FactionID then currentCharacter.factionID = app.FactionID; end
if app.ClassIndex then currentCharacter.classID = app.ClassIndex; end
if app.RaceIndex then currentCharacter.raceID = app.RaceIndex; end
if app.Class then currentCharacter.class = app.Class; end
if race then currentCharacter.race = race; end
if not currentCharacter.Achievements then currentCharacter.Achievements = {}; end
if not currentCharacter.ActiveSkills then currentCharacter.ActiveSkills = {}; end
if not currentCharacter.BattlePets then currentCharacter.BattlePets = {}; end
if not currentCharacter.Deaths then currentCharacter.Deaths = 0; end
if not currentCharacter.Exploration then currentCharacter.Exploration = {}; end
if not currentCharacter.Factions then currentCharacter.Factions = {}; end
if not currentCharacter.FlightPaths then currentCharacter.FlightPaths = {}; end
if not currentCharacter.Lockouts then currentCharacter.Lockouts = {}; end
if not currentCharacter.Quests then currentCharacter.Quests = {}; end
if not currentCharacter.Spells then currentCharacter.Spells = {}; end
if not currentCharacter.SpellRanks then currentCharacter.SpellRanks = {}; end
if not currentCharacter.Titles then currentCharacter.Titles = {}; end
if not currentCharacter.Transmog then currentCharacter.Transmog = {}; end
-- Update timestamps.
local now = time();
local timeStamps = currentCharacter.TimeStamps;
if not timeStamps then
timeStamps = {};
currentCharacter.TimeStamps = timeStamps;
end
for key,value in pairs(currentCharacter) do
if type(value) == "table" and not timeStamps[key] then
timeStamps[key] = now;
end
end
currentCharacter.lastPlayed = now;
app.CurrentCharacter = currentCharacter;
app.AddEventHandler("OnPlayerLevelUp", function()
currentCharacter.lvl = app.Level;
end);
-- Account Wide Data Storage
local accountWideData = ATTAccountWideData;
if not accountWideData then
accountWideData = {};
ATTAccountWideData = accountWideData;
end
if not accountWideData.Achievements then accountWideData.Achievements = {}; end
if not accountWideData.BattlePets then accountWideData.BattlePets = {}; end
if not accountWideData.Deaths then accountWideData.Deaths = 0; end
if not accountWideData.Exploration then accountWideData.Exploration = {}; end
if not accountWideData.Factions then accountWideData.Factions = {}; end
if not accountWideData.FlightPaths then accountWideData.FlightPaths = {}; end
if not accountWideData.Quests then accountWideData.Quests = {}; end
if not accountWideData.Spells then accountWideData.Spells = {}; end
if not accountWideData.Titles then accountWideData.Titles = {}; end
if not accountWideData.Transmog then accountWideData.Transmog = {}; end
if not accountWideData.OneTimeQuests then accountWideData.OneTimeQuests = {}; end
-- Account Wide Settings
local accountWideSettings = app.Settings.AccountWide;
local function IsAccountCached(field, id)
return accountWideData[field][id] or nil
end
local function SetAccountCollected(t, field, id, collected)
local container = accountWideData[field];
local oldstate = container[id];
if collected then
if not oldstate then
local now = time();
timeStamps[field] = now;
currentCharacter.lastPlayed = now;
AddToCollection(t);
container[id] = 1;
end
return 1;
elseif oldstate then
local now = time();
timeStamps[field] = now;
currentCharacter.lastPlayed = now;
RemoveFromCollection(t);
container[id] = nil;
end
end
local function SetAccountCollectedForSubType(t, field, subtype, id, collected)
local container = accountWideData[field];
local oldstate = container[id];
if collected then
if not oldstate then
local now = time();
timeStamps[field] = now;
currentCharacter.lastPlayed = now;
AddToCollection(t);
container[id] = 1;
end
return 1;
elseif oldstate then
local now = time();
timeStamps[field] = now;
currentCharacter.lastPlayed = now;
RemoveFromCollection(t);
container[id] = nil;
end
end
local function SetCollected(t, field, id, collected)
local container = currentCharacter[field];
local oldstate = container[id];
if collected then
if not oldstate then
if t and not (accountWideSettings[field] and accountWideData[field][id]) then
--print("SetCollected", field, id, accountWideSettings[field], accountWideData[field][id]);
AddToCollection(t);
end
container[id] = 1;
accountWideData[field][id] = 1;
local now = time();
timeStamps[field] = now;
currentCharacter.lastPlayed = now;
else
accountWideData[field][id] = 1;
end
return 1;
elseif oldstate then
container[id] = nil;
local now = time();
timeStamps[field] = now;
currentCharacter.lastPlayed = now;
for guid,other in pairs(characterData) do
local otherContainer = other[field];
if otherContainer and otherContainer[id] then
accountWideData[field][id] = 1;
return accountWideSettings[field] and 2;
end
end
if accountWideData[field][id] then
RemoveFromCollection(t);
accountWideData[field][id] = nil;
end
elseif accountWideSettings[field] and accountWideData[field][id] then
return 2;
end
end
local function SetCollectedForSubType(t, field, subtype, id, collected)
local container = currentCharacter[field];
local oldstate = container[id];
if collected then
if not oldstate then
if t and not (accountWideSettings[subtype] and accountWideData[field][id]) then
--print("SetCollectedForSubType", field, subtype, id, accountWideSettings[subtype], accountWideData[field][id]);
AddToCollection(t);
end
container[id] = 1;
accountWideData[field][id] = 1;
local now = time();
timeStamps[field] = now;
currentCharacter.lastPlayed = now;
else
accountWideData[field][id] = 1;
end
return 1;
elseif oldstate then
container[id] = nil;
local now = time();
timeStamps[field] = now;
currentCharacter.lastPlayed = now;
for guid,other in pairs(characterData) do
local otherContainer = other[field];
if otherContainer and otherContainer[id] then
accountWideData[field][id] = 1;
return accountWideSettings[subtype] and 2;
end
end
if accountWideData[field][id] then
RemoveFromCollection(t);
accountWideData[field][id] = nil;
end
elseif accountWideSettings[subtype] and accountWideData[field][id] then
return 2;
end
end
app.IsAccountCached = IsAccountCached;
app.SetAccountCollected = SetAccountCollected;
app.SetAccountCollectedForSubType = SetAccountCollectedForSubType;
app.SetCollected = SetCollected;
app.SetCollectedForSubType = SetCollectedForSubType;
-- Notify Event Handlers that Saved Variable Data is available.
app.HandleEvent("OnSavedVariablesAvailable", currentCharacter, accountWideData, accountWideSettings);
-- Check to see if we have a leftover ItemDB cache
GetDataMember("GroupQuestsByGUID", {});
-- Clean up settings
local oldsettings = {};
for i,key in ipairs({
"GroupQuestsByGUID",
"LocalizedCategoryNames",
"LocalizedFlightPathNames",
"Reagents",
"SoftReserves",
"SoftReservePersistence",
}) do
oldsettings[key] = AllTheThingsAD[key];
end
wipe(AllTheThingsAD);
for key,value in pairs(oldsettings) do
AllTheThingsAD[key] = value;
end
-- Wipe the Debugger Data
AllTheThingsDebugData = nil;
-- If we have RWP data on this character, let's update that to use Transmog.
for guid,character in pairs(characterData) do
local characterRWP = character.RWP;
if characterRWP then
local accountWideTransmog = accountWideData.Transmog;
local currentCharacterTransmog = character.Transmog;
if not currentCharacterTransmog then
currentCharacterTransmog = {};
character.Transmog = currentCharacterTransmog;
end
for itemID,collected in pairs(characterRWP) do
for _,item in ipairs(SearchForField("itemID", itemID)) do
if item.sourceID then
currentCharacterTransmog[item.sourceID] = collected;
accountWideTransmog[item.sourceID] = 1;
characterRWP[itemID] = nil;
end
end
end
local any = false;
for itemID,collected in pairs(characterRWP) do
any = true;
break;
end
if not any then character.RWP = nil; end
end
end
-- Refresh Collections
app.RefreshCollections();
-- Tooltip Settings
app.Settings:Initialize();
end,
};
app:RegisterEvent("ADDON_LOADED");
app.events.ADDON_LOADED = function(addonName)
local handler = ADDON_LOADED_HANDLERS[addonName];
if handler then handler(); end
end
app.AddEventHandler("OnStartupDone", function()
-- Prepare the Sound Pack!
app.Audio:ReloadSoundPack();
-- Execute the OnReady handlers.
app.HandleEvent("OnReady");
-- Mark that we're ready now!
app.IsReady = true;
end);
app.HandleEvent("OnLoad")