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.

14649 lines
486 KiB

--------------------------------------------------------------------------------
-- A L L T H E T H I N G S --
--------------------------------------------------------------------------------
-- Copyright 2017-2023 Dylan Fortune (Crieve-Sargeras) --
--------------------------------------------------------------------------------
-- App locals
local appName, app = ...;
local contains, containsAny, containsValue = app.contains, app.containsAny, app.containsValue;
local CloneArray, CloneDictionary, CloneReference = app.CloneArray, app.CloneDictionary, app.CloneReference;
local GetRelativeValue = app.GetRelativeValue;
local L = app.L;
-- Binding Localizations
BINDING_HEADER_ALLTHETHINGS = L.TITLE
BINDING_NAME_ALLTHETHINGS_TOGGLEACCOUNTMODE = L["TOGGLE_ACCOUNT_MODE"]
BINDING_NAME_ALLTHETHINGS_TOGGLEDEBUGMODE = L["TOGGLE_DEBUG_MODE"]
BINDING_NAME_ALLTHETHINGS_TOGGLEFACTIONMODE = L["TOGGLE_FACTION_MODE"]
BINDING_HEADER_ALLTHETHINGS_PREFERENCES = L["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_TOGGLELOOTDROPS = L["TOGGLE_LOOTDROPS"]
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_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, sformat
= ipairs, pairs, rawset, rawget, pcall, tinsert, tremove, string.format;
local C_Map_GetMapInfo, C_Map_GetAreaInfo = C_Map.GetMapInfo, C_Map.GetAreaInfo;
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"];
local GetFactionInfoByID = _G["GetFactionInfoByID"];
local GetItemInfo = _G["GetItemInfo"];
local GetItemInfoInstant = _G["GetItemInfoInstant"];
local GetItemCount = _G["GetItemCount"];
local C_ToyBox, PlayerHasToy = _G["C_ToyBox"], _G["PlayerHasToy"];
local InCombatLockdown = _G["InCombatLockdown"];
local GetSpellInfo, IsPlayerSpell, IsSpellKnown, IsSpellKnownOrOverridesKnown, IsTitleKnown =
GetSpellInfo, IsPlayerSpell, IsSpellKnown, IsSpellKnownOrOverridesKnown, IsTitleKnown;
local C_QuestLog_GetAllCompletedQuestIDs = C_QuestLog.GetAllCompletedQuestIDs;
local C_QuestLog_IsQuestFlaggedCompleted = C_QuestLog.IsQuestFlaggedCompleted;
local C_QuestLog_IsOnQuest = C_QuestLog.IsOnQuest;
local ALLIANCE_FACTION_ID = Enum.FlightPathFaction.Alliance;
local HORDE_FACTION_ID = Enum.FlightPathFaction.Horde;
-- App & Module locals
local SearchForField, SearchForFieldContainer
= app.SearchForField, app.SearchForFieldContainer;
local IsRetrieving = app.Modules.RetrievingData.IsRetrieving;
-- Add a Feader & Filter debugger
setmetatable(app.FilterConstants, {
__index = function(t, key)
print("MISSING FilterConstant:", key);
rawset(t, key, -9999999999);
return -9999999999;
end
});
setmetatable(app.HeaderConstants, {
__index = function(t, key)
print("MISSING HeaderConstant:", key);
rawset(t, key, -9999999999);
return -9999999999;
end
});
-- Local Variables
local DESCRIPTION_SEPARATOR = "`";
local ALLIANCE_ONLY = {
1,
3,
4,
7,
11,
22,
25,
29,
30,
32,
34,
37,
};
local HORDE_ONLY = {
2,
5,
6,
8,
9,
10,
26,
27,
28,
31,
35,
36,
};
-- Helper Functions
local UpdateGroup, UpdateGroups;
local constructor = function(id, t, typeID)
if t then
if not t.g and t[1] then
return { g=t, [typeID]=id };
else
t[typeID] = id;
return t;
end
else
return {[typeID] = id};
end
end
function distance( x1, y1, x2, y2 )
return math.sqrt( (x2-x1)^2 + (y2-y1)^2 )
end
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:PlayRareFindSound();
else
app: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: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 SetDataSubMember(member, submember, data)
attData = AllTheThingsAD[member];
if attData == nil then
AllTheThingsAD[member] = { [submember] = data };
else
attData[submember] = data;
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.SetDataSubMember = SetDataSubMember;
app.GetDataSubMember = GetDataSubMember;
app.SetAccountCollected = function()
app.print("SetCollected not initialized yet...");
end;
app.SetAccountCollectedForSubType = function()
app.print("SetCollectedForSubType not initialized yet...");
end
app.SetCollected = function()
app.print("SetCollected not initialized yet...");
end;
app.SetCollectedForSubType = function()
app.print("SetCollectedForSubType not initialized yet...");
end
local backdrop = {
bgFile = "Interface/Tooltips/UI-Tooltip-Background",
edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 }
};
-- Game Tooltip Icon
local GameTooltipIcon = CreateFrame("FRAME", nil, GameTooltip);
GameTooltipIcon:SetPoint("TOPRIGHT", GameTooltip, "TOPLEFT", 0, 0);
GameTooltipIcon:SetSize(72, 72);
GameTooltipIcon.icon = GameTooltipIcon:CreateTexture(nil, "ARTWORK");
GameTooltipIcon.icon:SetAllPoints(GameTooltipIcon);
GameTooltipIcon.icon:Show();
GameTooltipIcon.icon.Background = GameTooltipIcon:CreateTexture(nil, "BACKGROUND");
GameTooltipIcon.icon.Background:SetAllPoints(GameTooltipIcon);
GameTooltipIcon.icon.Background:Show();
GameTooltipIcon.icon.Border = GameTooltipIcon:CreateTexture(nil, "BORDER");
GameTooltipIcon.icon.Border:SetAllPoints(GameTooltipIcon);
GameTooltipIcon.icon.Border:Show();
GameTooltipIcon:Hide();
-- Model is used to display the model of an NPC/Encounter.
local GameTooltipModel, model, fi = CreateFrame("FRAME", "ATTGameTooltipModel", GameTooltip, BackdropTemplateMixin and "BackdropTemplate");
GameTooltipModel:SetPoint("TOPRIGHT", GameTooltip, "TOPLEFT", 0, 0);
GameTooltipModel:SetSize(128, 128);
GameTooltipModel:SetBackdrop(backdrop);
GameTooltipModel:SetBackdropBorderColor(1, 1, 1, 1);
GameTooltipModel:SetBackdropColor(0, 0, 0, 1);
GameTooltipModel.Models = {};
GameTooltipModel.Model = CreateFrame("DressUpModel", nil, GameTooltipModel);
GameTooltipModel.Model:SetPoint("TOPLEFT", GameTooltipModel ,"TOPLEFT", 4, -4)
GameTooltipModel.Model:SetPoint("BOTTOMRIGHT", GameTooltipModel ,"BOTTOMRIGHT", -4, 4)
GameTooltipModel.Model:SetFacing(MODELFRAME_DEFAULT_ROTATION);
GameTooltipModel.Model:SetScript("OnUpdate", function(self, elapsed)
self:SetFacing(self:GetFacing() + elapsed);
end);
GameTooltipModel.Model:Hide();
local MAX_CREATURES_PER_ENCOUNTER = 9;
for i=1,MAX_CREATURES_PER_ENCOUNTER do
model = CreateFrame("DressUpModel", "ATTGameTooltipModel" .. i, GameTooltipModel);
model:SetPoint("TOPLEFT", GameTooltipModel ,"TOPLEFT", 4, -4);
model:SetPoint("BOTTOMRIGHT", GameTooltipModel ,"BOTTOMRIGHT", -4, 4);
model:SetCamDistanceScale(1.7);
model:SetDisplayInfo(987);
model:SetFacing(MODELFRAME_DEFAULT_ROTATION);
fi = math.floor(i / 2);
model:SetPosition(fi * -0.1, (fi * (i % 2 == 0 and -1 or 1)) * ((MAX_CREATURES_PER_ENCOUNTER - i) * 0.1), fi * 0.2 - 0.3);
if model.SetDepth then
model:SetDepth(i);
end
model:Hide();
tinsert(GameTooltipModel.Models, model);
end
GameTooltipModel.HideAllModels = function(self)
for i=1,MAX_CREATURES_PER_ENCOUNTER do
GameTooltipModel.Models[i]:Hide();
end
GameTooltipModel.Model:Hide();
end
GameTooltipModel.SetCreatureID = function(self, creatureID)
GameTooltipModel.HideAllModels(self);
if creatureID > 0 then
self.Model:SetUnit("none");
self.Model:SetCreature(creatureID);
local displayID = self.Model:GetDisplayInfo();
if not displayID then
app:StartATTCoroutine("SetCreatureID", function()
if self.lastModel == creatureID then
self:SetCreatureID(creatureID);
end
end);
end
end
self:Show();
end
GameTooltipModel.TrySetDisplayInfos = function(self, reference, displayInfos)
if displayInfos then
local count = #displayInfos;
if count > 0 then
local rotation = reference.modelRotation and ((reference.modelRotation * math.pi) / 180) or MODELFRAME_DEFAULT_ROTATION;
local scale = reference.modelScale or 1;
if count > 1 then
count = math.min(count, MAX_CREATURES_PER_ENCOUNTER);
local ratio = count / MAX_CREATURES_PER_ENCOUNTER;
if count < 3 then
for i=1,count do
model = self.Models[i];
model:SetDisplayInfo(displayInfos[i]);
model:SetCamDistanceScale(scale);
model:SetFacing(rotation);
model:SetPosition(0, (i % 2 == 0 and 0.5 or -0.5), 0);
model:Show();
end
else
scale = (1 + (ratio * 0.5)) * scale;
for i=1,count do
model = self.Models[i];
model:SetDisplayInfo(displayInfos[i]);
model:SetCamDistanceScale(scale);
model:SetFacing(rotation);
fi = math.floor(i / 2);
model:SetPosition(fi * -0.1, (fi * (i % 2 == 0 and -1 or 1)) * ((MAX_CREATURES_PER_ENCOUNTER - i) * 0.1), fi * 0.2 - (ratio * 0.15));
model:Show();
end
end
else
self.Model:SetFacing(rotation);
self.Model:SetCamDistanceScale(scale);
self.Model:SetDisplayInfo(displayInfos[1]);
self.Model:Show();
end
self:Show();
return true;
end
end
end
-- Attempts to return the displayID for the data, or every displayID if 'all' is specified
local function GetDisplayID(data, all)
-- don't create a displayID for groups with a sourceID/itemID/difficultyID/mapID
if data.s or data.itemID or data.difficultyID or data.mapID then return; end
if all then
local displayInfo, _ = {};
-- specific displayID
_ = data.displayID;
if _ then tinsert(displayInfo, _); data.displayInfo = displayInfo; return displayInfo; end
-- specific creatureID for displayID
_ = data.creatureID and app.NPCDisplayIDFromID[data.creatureID];
if _ then tinsert(displayInfo, _); data.displayInfo = displayInfo; return displayInfo; end
-- loop through "n" providers
if data.providers 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
_ = v[2] and app.NPCDisplayIDFromID[v[2]];
if _ then tinsert(displayInfo, _); end
end
end
end
if displayInfo[1] then data.displayInfo = displayInfo; return displayInfo; end
-- for quest givers
if data.qgs then
for k,v in pairs(data.qgs) do
_ = v and app.NPCDisplayIDFromID[v];
if _ then tinsert(displayInfo, _); end
end
end
if displayInfo[1] then data.displayInfo = displayInfo; return displayInfo; end
else
-- specific displayID
local _ = data.displayID or data.fetchedDisplayID;
if _ then return _; end
-- specific creatureID for displayID
_ = data.creatureID and app.NPCDisplayIDFromID[data.creatureID];
if _ then data.fetchedDisplayID = _; return _; end
-- loop through "n" providers
if data.providers 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
_ = v[2] and app.NPCDisplayIDFromID[v[2]];
if _ then data.fetchedDisplayID = _; return _; end
end
end
end
-- for quest givers
if data.qgs then
for k,v in pairs(data.qgs) do
_ = v and app.NPCDisplayIDFromID[v];
if _ then data.fetchedDisplayID = _; return _; end
end
end
end
end
GameTooltipModel.TrySetModel = function(self, reference)
GameTooltipModel.HideAllModels(self);
if app.Settings:GetTooltipSetting("Models") then
self.lastModel = reference;
local displayInfos = reference.displayInfo or GetDisplayID(reference, true);
if GameTooltipModel.TrySetDisplayInfos(self, reference, displayInfos) then
return true;
end
if reference.displayID then
self.Model:SetFacing(reference.modelRotation and ((reference.modelRotation * math.pi) / 180) or MODELFRAME_DEFAULT_ROTATION);
self.Model:SetCamDistanceScale(reference.modelScale or 1);
self.Model:SetDisplayInfo(reference.displayID);
self.Model:Show();
self:Show();
return true;
elseif reference.modelID then
self.Model:SetFacing(reference.modelRotation and ((reference.modelRotation * math.pi) / 180) or MODELFRAME_DEFAULT_ROTATION);
self.Model:SetCamDistanceScale(reference.modelScale or 1);
self.Model:SetDisplayInfo(reference.modelID);
self.Model:Show();
self:Show();
return true;
elseif reference.unit and not reference.icon then
self.Model:SetFacing(reference.modelRotation and ((reference.modelRotation * math.pi) / 180) or MODELFRAME_DEFAULT_ROTATION);
self.Model:SetCamDistanceScale(reference.modelScale or 1);
self.Model:SetUnit(reference.unit);
self.Model:Show();
self:Show();
end
local modelID = reference.model and tonumber(reference.model);
if modelID and modelID > 0 then
self.Model:SetFacing(reference.modelRotation and ((reference.modelRotation * math.pi) / 180) or MODELFRAME_DEFAULT_ROTATION);
self.Model:SetCamDistanceScale(reference.modelScale or 1);
self.Model:SetUnit("none");
self.Model:SetModel(modelID);
self.Model:Show();
self:Show();
return true;
elseif reference.creatureID and reference.creatureID > 0 then
self.Model:SetFacing(reference.modelRotation and ((reference.modelRotation * math.pi) / 180) or MODELFRAME_DEFAULT_ROTATION);
self.Model:SetCamDistanceScale(reference.modelScale or 1);
self:SetCreatureID(reference.creatureID);
self.Model:Show();
return true;
end
if reference.atlas then
GameTooltipIcon:SetSize(64,64);
GameTooltipIcon.icon:SetAtlas(reference.atlas);
GameTooltipIcon:Show();
if reference["atlas-background"] then
GameTooltipIcon.icon.Background:SetAtlas(reference["atlas-background"]);
GameTooltipIcon.icon.Background:Show();
end
if reference["atlas-border"] then
GameTooltipIcon.icon.Border:SetAtlas(reference["atlas-border"]);
GameTooltipIcon.icon.Border:Show();
if reference["atlas-color"] then
local swatches = reference["atlas-color"];
GameTooltipIcon.icon.Border:SetVertexColor(swatches[1], swatches[2], swatches[3], swatches[4] or 1.0);
else
GameTooltipIcon.icon.Border:SetVertexColor(1, 1, 1, 1.0);
end
end
return true;
end
end
end
GameTooltipModel:Hide();
app.AlwaysShowUpdate = function(data) data.visible = true; return true; end
app.AlwaysShowUpdateWithoutReturn = function(data) data.visible = true; end
app.print = function(...)
print(L["TITLE"], ...);
end
-- Color Lib
local CS = CreateFrame("ColorSelect", nil, app.frame);
local function Colorize(str, color)
return "|c" .. color .. str .. "|r";
end
local function HexToRGB(hex)
return tonumber("0x"..hex:sub(3,4)) / 255, tonumber("0x"..hex:sub(5,6)) / 255, tonumber("0x"..hex:sub(7,8)) / 255;
end
local function RGBToHex(r, g, b)
return sformat("ff%02x%02x%02x",
r <= 255 and r >= 0 and r or 0,
g <= 255 and g >= 0 and g or 0,
b <= 255 and b >= 0 and b or 0);
end
local function ConvertColorRgbToHsv(r, g, b)
CS:SetColorRGB(r, g, b);
local h,s,v = CS:GetColorHSV()
return {h=h,s=s,v=v}
end
local red, green = ConvertColorRgbToHsv(1,0,0), ConvertColorRgbToHsv(0,1,0);
local progress_colors = setmetatable({ [1] = app.Colors.Completed }, {
__index = function(t, p)
local h;
p = tonumber(p);
if abs(red.h - green.h) > 180 then
local angle = (360 - abs(red.h - green.h)) * p;
if red.h < green.h then
h = floor(red.h - angle);
if h < 0 then h = 360 + h end
else
h = floor(red.h + angle);
if h > 360 then h = h - 360 end
end
else
h = floor(red.h-(red.h-green.h)*p)
end
CS:SetColorHSV(h, red.s-(red.s-green.s)*p, red.v-(red.v-green.v)*p);
local r,g,b = CS:GetColorRGB();
local color = RGBToHex(r * 255, g * 255, b * 255);
rawset(t, p, color);
return color;
end
});
local function GetNumberWithZeros(number, desiredLength)
if desiredLength > 0 then
local str = tostring(number);
local length = string.len(str);
local pos = string.find(str,"[.]");
if not pos then
str = str .. ".";
for i=desiredLength,1,-1 do
str = str .. "0";
end
else
local totalExtra = desiredLength - (length - pos);
for i=totalExtra,1,-1 do
str = str .. "0";
end
if totalExtra < 1 then
str = string.sub(str, 1, pos + desiredLength);
end
end
return str;
else
return tostring(floor(number));
end
end
local function GetProgressTextDefault(progress, total)
return tostring(progress) .. " / " .. tostring(total);
end
local function GetProgressTextRemaining(progress, total)
return tostring(total - progress);
end
local function GetProgressColor(p)
return progress_colors[p];
end
local function GetProgressColorText(progress, total)
if total and total > 0 then
local percent = progress / total;
return "|c" .. GetProgressColor(percent) .. app.GetProgressText(progress, total) .. " (" .. GetNumberWithZeros(percent * 100, app.Settings:GetTooltipSetting("Precision")) .. "%) |r";
end
end
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)
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
local function GetAddedWithPatchString(awp, addedBack)
if awp then
awp = tonumber(awp);
local formatString = "ADDED";
if app.GameBuildVersion == awp then
formatString = "WAS_" .. formatString;
elseif app.GameBuildVersion > awp then
return nil; -- Don't want to show this at the moment, let's add a configuration first!
end
if addedBack then formatString = formatString .. "_BACK"; end
return sformat(L[formatString .. "_WITH_PATCH_FORMAT"],
math.floor(awp / 10000) .. "." .. (math.floor(awp / 100) % 10) .. "." .. (awp % 10));
end
end
local function GetRemovedWithPatchString(rwp)
if rwp then
rwp = tonumber(rwp);
return sformat(L.REMOVED_WITH_PATCH_FORMAT, math.floor(rwp / 10000) .. "." .. (math.floor(rwp / 100) % 10) .. "." .. (rwp % 10));
end
end
app.GetProgressText = GetProgressTextDefault;
app.GetProgressTextDefault = GetProgressTextDefault;
app.GetProgressTextRemaining = GetProgressTextRemaining;
app.GetProgressColorText = GetProgressColorText;
CS:Hide();
-- Source ID Harvesting Lib
local DressUpModel = CreateFrame('DressUpModel');
local NPCModelHarvester = CreateFrame('DressUpModel', nil, OffScreenFrame);
local inventorySlotsMap = { -- Taken directly from CanIMogIt (Thanks!)
["INVTYPE_HEAD"] = {1},
["INVTYPE_NECK"] = {2},
["INVTYPE_SHOULDER"] = {3},
["INVTYPE_BODY"] = {4},
["INVTYPE_CHEST"] = {5},
["INVTYPE_ROBE"] = {5},
["INVTYPE_WAIST"] = {6},
["INVTYPE_LEGS"] = {7},
["INVTYPE_FEET"] = {8},
["INVTYPE_WRIST"] = {9},
["INVTYPE_HAND"] = {10},
["INVTYPE_RING"] = {11},
["INVTYPE_TRINKET"] = {12},
["INVTYPE_CLOAK"] = {15},
["INVTYPE_WEAPON"] = {16, 17},
["INVTYPE_SHIELD"] = {17},
["INVTYPE_2HWEAPON"] = {16, 17},
["INVTYPE_WEAPONMAINHAND"] = {16},
["INVTYPE_RANGED"] = {16},
["INVTYPE_RANGEDRIGHT"] = {16},
["INVTYPE_WEAPONOFFHAND"] = {17},
["INVTYPE_HOLDABLE"] = {17},
["INVTYPE_TABARD"] = {19},
};
local achievementTooltipText = {
[17213] = "DPA", -- Defense Protocol Alpha: Utgarde Keep
[17283] = "DPA", -- Defense Protocol Alpha: The Nexus
[17285] = "DPA", -- Defense Protocol Alpha: Azjol-Nerub
[17291] = "DPA", -- Defense Protocol Alpha: Ahn'kahet: The Old Kingdom
[17292] = "DPA", -- Defense Protocol Alpha: Drak'Tharon Keep
[17293] = "DPA", -- Defense Protocol Alpha: The Violet Hold
[17295] = "DPA", -- Defense Protocol Alpha: Gundrak
[17297] = "DPA", -- Defense Protocol Alpha: Halls of Stone
[17299] = "DPA", -- Defense Protocol Alpha: Halls of Lightning
[17300] = "DPA", -- Defense Protocol Alpha: The Oculus
[17301] = "DPA", -- Defense Protocol Alpha: Utgarde Pinnacle
[17302] = "DPA", -- Defense Protocol Alpha: The Culling of Stratholme
[18590] = "DPB", -- Defense Protocol Beta: Utgarde Keep
[18591] = "DPB", -- Defense Protocol Beta: The Nexus
[18592] = "DPB", -- Defense Protocol Beta: Azjol-Nerub
[18593] = "DPB", -- Defense Protocol Beta: Ahn'kahet: The Old Kingdom
[18594] = "DPB", -- Defense Protocol Beta: Drak'Tharon Keep
[18595] = "DPB", -- Defense Protocol Beta: The Violet Hold
[18596] = "DPB", -- Defense Protocol Beta: Gundrak
[18597] = "DPB", -- Defense Protocol Beta: Halls of Stone
[18598] = "DPB", -- Defense Protocol Beta: Halls of Lightning
[18599] = "DPB", -- Defense Protocol Beta: The Oculus
[18600] = "DPB", -- Defense Protocol Beta: Utgarde Pinnacle
[18601] = "DPB", -- Defense Protocol Beta: The Culling of Stratholme
[18677] = "DPB", -- Defense Protocol Beta: Trial of the Champion (A)
[18678] = "DPB", -- Defense Protocol Beta: Trial of the Champion (H)
[19427] = "DPG", -- Defense Protocol Gamma: Utgarde Keep
[19428] = "DPG", -- Defense Protocol Gamma: The Nexus
[19429] = "DPG", -- Defense Protocol Gamma: Azjol-Nerub
[19430] = "DPG", -- Defense Protocol Gamma: Ahn'kahet: The Old Kingdom
[19431] = "DPG", -- Defense Protocol Gamma: Drak'Tharon Keep
[19432] = "DPG", -- Defense Protocol Gamma: The Violet Hold
[19433] = "DPG", -- Defense Protocol Gamma: Gundrak
[19434] = "DPG", -- Defense Protocol Gamma: Halls of Stone
[19435] = "DPG", -- Defense Protocol Gamma: Halls of Lightning
[19436] = "DPG", -- Defense Protocol Gamma: The Oculus
[19437] = "DPG", -- Defense Protocol Gamma: Utgarde Pinnacle
[19438] = "DPG", -- Defense Protocol Gamma: The Culling of Stratholme
[19426] = "DPG", -- Defense Protocol Gamma: Trial of the Champion (A)
[19425] = "DPG", -- Defense Protocol Gamma: Trial of the Champion (H)
};
local function BuildGroups(parent)
local g = parent.g;
if g then
-- Iterate through the groups
for i=1,#g,1 do
-- Set the group's parent
local group = g[i];
group.parent = parent;
-- Build the groups
BuildGroups(group);
end
end
end
local function BuildSourceText(group, l, skip)
if group then
local parent = group.parent;
if parent then
if not group.itemID and not skip and (parent.key == "filterID" or parent.key == "spellID" or ((parent.headerID or (parent.spellID and (group.categoryID or group.tierID)))
and ((parent.headerID == app.HeaderConstants.VENDORS or parent.headerID == app.HeaderConstants.QUESTS or parent.headerID == app.HeaderConstants.WORLD_BOSSES) or (parent.parent and parent.parent.parent)))) then
return BuildSourceText(parent.parent, 5, skip) .. DESCRIPTION_SEPARATOR .. (group.text or RETRIEVING_DATA) .. " (" .. (parent.text or RETRIEVING_DATA) .. ")";
end
if group.headerID then
if group.headerID == app.HeaderConstants.ZONE_DROPS then
if group.crs and #group.crs == 1 then
return BuildSourceText(parent, l + 1, skip) .. DESCRIPTION_SEPARATOR .. (app.NPCNameFromID[group.crs[1]] or RETRIEVING_DATA) .. " (Drop)";
end
return BuildSourceText(parent, l + 1, skip) .. DESCRIPTION_SEPARATOR .. (group.text or RETRIEVING_DATA);
end
if parent.difficultyID then
return BuildSourceText(parent, l + 1, skip);
end
if parent.parent then
return BuildSourceText(parent, l + 1, skip) .. DESCRIPTION_SEPARATOR .. (group.text or RETRIEVING_DATA);
end
end
if group.key == "criteriaID" and group.achievementID then
local tooltipText = achievementTooltipText[group.achievementID];
if tooltipText then
return BuildSourceText(parent, 5, group.itemID or skip) .. " (" .. tooltipText .. ")";
else
return BuildSourceText(parent, 5, group.itemID or skip);
end
end
if parent.key == "categoryID" or parent.key == "tierID" or group.key == "filterID" or group.key == "spellID" or group.key == "encounterID" or (parent.key == "mapID" and group.key == "npcID") then
return BuildSourceText(parent, 5, skip) .. DESCRIPTION_SEPARATOR .. (group.text or RETRIEVING_DATA);
end
if l < 1 then
return BuildSourceText(parent, l + 1, group.itemID or skip);
else
return BuildSourceText(parent, l + 1, group.itemID or skip) .. " > " .. (group.text or RETRIEVING_DATA);
end
end
return group.text or RETRIEVING_DATA;
end
return L.TITLE;
end
local function BuildSourceTextForChat(group, l)
if group.parent then
if l < 1 then
return BuildSourceTextForChat(group.parent, l + 1);
else
return BuildSourceTextForChat(group.parent, l + 1) .. " > " .. (group.text or "*");
end
return group.text or "*";
end
return "ATT";
end
local function BuildSourceTextForDynamicPath(group)
local parent = group.parent;
if parent then
return BuildSourceTextForDynamicPath(parent) .. ">" .. (group.hash or group.name or group.text);
else
return group.hash or group.name or group.text;
end
end
local function CloneData(group)
local clone = setmetatable({}, getmetatable(group));
for key,value in pairs(group) do
clone[key] = value;
end
if group.g then
local g = {};
for i,group in ipairs(group.g) do
local child = CloneData(group);
child.parent = clone;
tinsert(g, child);
end
clone.g = g;
end
return clone;
end
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
app.MaximumItemInfoRetries = 400;
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 GetBestMapForGroup(group, currentMapID)
if group then
local mapID = group.mapID;
if mapID and mapID == currentMapID then
return mapID;
end
local coords = group.coords;
if coords then
for i,coord in ipairs(coords) do
mapID = coord[3];
if mapID == currentMapID then
return mapID;
end
end
end
local maps = group.maps;
if maps then
for i,otherMapID in ipairs(maps) do
mapID = otherMapID;
if mapID == currentMapID then
return mapID;
end
end
end
return mapID or GetBestMapForGroup(group.parent, currentMapID);
end
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 GetRelativeMap(group, currentMapID)
if group then
return group.mapID or (group.maps and (contains(group.maps, currentMapID) and currentMapID or group.maps[1])) or GetRelativeMap(group.parent, currentMapID);
end
return currentMapID;
end
local function GetRelativeField(group, field, value)
if group then
return group[field] == value or GetRelativeField(group.parent, field, value);
end
end
local function GetDeepestRelativeValue(group, field)
if group then
return GetDeepestRelativeValue(group.parent, field) or group[field];
end
end
-- Quest Completion Lib
local CompletedQuests, DirtyQuests = {}, {};
local IsQuestFlaggedCompleted = function(questID)
return questID and CompletedQuests[questID];
end
local IsQuestFlaggedCompletedForObject = function(t)
if IsQuestFlaggedCompleted(t.questID) then return 1; end
if app.Settings.AccountWide.Quests and not t.repeatable then
if t.questID and ATTAccountWideData.Quests[t.questID] then
return 2;
end
end
local altQuests = t.altQuests;
if altQuests then
for i,questID in ipairs(altQuests) do
if IsQuestFlaggedCompleted(questID) then
return 2;
end
end
if app.Settings.AccountWide.Quests then
for i,questID in ipairs(altQuests) do
if ATTAccountWideData.Quests[questID] then
return 2;
end
end
end
end
end
-- Search Caching
local searchCache = {};
app.searchCache = searchCache;
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 CreateObject(t)
local s = {};
if t[1] then
-- array
for i,o in ipairs(t) do
tinsert(s, CreateObject(o));
end
return s;
else
for k,v in pairs(t) do
rawset(s, k, v);
end
if t.g then
s.g = {};
for i,o in ipairs(t.g) do
tinsert(s.g, CreateObject(o));
end
end
local meta = getmetatable(t);
if meta then
setmetatable(s, meta);
return s;
else
t = s;
if t.mapID then
t = app.CreateMap(t.mapID, t);
elseif t.currencyID then
t = app.CreateCurrencyClass(t.currencyID, t);
elseif t.achID then
t = app.CreateAchievement(t.achID, t);
elseif t.achievementID then
t = app.CreateAchievement(t.achievementID, t);
elseif t.objectID then
t = app.CreateObject(t.objectID, t);
elseif t.professionID then
t = app.CreateProfession(t.professionID, t);
elseif t.categoryID then
t = app.CreateCategory(t.categoryID, t);
elseif t.illusionID then
t = app.CreateIllusion(t.illusionID, t);
elseif t.recipeID then
t = app.CreateRecipe(t.recipeID, t);
elseif t.spellID then
if t.f == app.FilterConstants.RECIPES then
t = app.CreateRecipe(t.spellID, t);
else
t = app.CreateSpell(t.spellID, t);
end
elseif t.itemID then
if t.toyID then
t = app.CreateToy(t.itemID, t);
else
t = app.CreateItem(t.itemID, t);
end
elseif t.classID then
t = app.CreateCharacterClass(t.classID, t);
elseif t.npcID or t.creatureID then
t = app.CreateNPC(t.npcID or t.creatureID, t);
elseif t.headerID then
t = app.CreateNPC(t.headerID, t); -- For now.
elseif t.questID then
t = app.CreateQuest(t.questID, t);
elseif t.factionID then
t = app.CreateFaction(t.factionID, t);
else
t = setmetatable({}, { __index = t });
end
t.visible = true;
return t;
end
end
end
local MergeObject;
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 = CreateObject(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
if not o.itemID or o.b == 1 then
local r = GetRelativeValue(o, "r");
if r then
clone.r = r;
clone.races = nil;
else
local races = GetRelativeValue(o, "races");
if races then clone.races = CloneArray(races); end
end
local c = GetRelativeValue(o, "c");
if c then clone.c = CloneArray(c); end
end
return MergeObject(g, clone);
end
local function ExpandGroupsRecursively(group, expanded, manual)
if group.g and (not group.itemID or manual) then
group.expanded = expanded;
for i, subgroup in ipairs(group.g) do
ExpandGroupsRecursively(subgroup, expanded, manual);
end
end
end
local function ReapplyExpand(g, g2)
for j,p in ipairs(g2) do
local found = false;
local key = p.key;
local id = p[key];
for i,o in ipairs(g) do
if o[key] == id then
found = true;
if o.expanded then
if not p.expanded then
p.expanded = true;
if o.g and p.g then ReapplyExpand(o.g, p.g); end
end
end
break;
end
end
if not found then
ExpandGroupsRecursively(p, true);
end
end
end
app.MergeClone = MergeClone;
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(tierID, headerID1, headerID2)
local b = {
{ "select", "tierID", tierID }, -- 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,s in ipairs(cache) do
local ref = ResolveSymbolicLink(s);
if ref then
if s.g then
for i,m in ipairs(s.g) do
tinsert(searchResults, m);
end
end
for i,m in ipairs(ref) do
tinsert(searchResults, m);
end
else
tinsert(searchResults, s);
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(BuildSourceTextForDynamicPath(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];
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,s in ipairs(cache) do
if not s.symbolizing then
local ref = ResolveSymbolicLink(s);
if ref then
if s.g then
for i,m in ipairs(s.g) do
tinsert(searchResults, m);
end
end
for i,m in ipairs(ref) do
tinsert(searchResults, m);
end
else
tinsert(searchResults, s);
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,s in ipairs(orig) do
if s.g then
for l,t in ipairs(s.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 s = searchResults[k];
if not s[key] or s[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 s = orig[k];
if s.g and index <= #s.g then
tinsert(searchResults, s.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 s = searchResults[k];
local matched = true;
for key,value in pairs(dict) do
if not s[key] or s[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 s = searchResults[k];
if s[key] and s[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
local s = searchResults[k];
if not s[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
local s = searchResults[k];
if s[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 s = searchResults[k];
if not s[key] or not contains(clone, s[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 s = searchResults[k];
if s[key] and contains(clone, s[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,s in ipairs(searchResults) do
tinsert(finalized, s);
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,s in ipairs(searchResults) do
tinsert(finalized, s);
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 s = searchResults[k];
if s.itemID and not contains(types, select(4, GetItemInfoInstant(s.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,s in ipairs(results) do
tinsert(searchResults, s);
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,s in ipairs(results) do
tinsert(searchResults, s);
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 cache;
for criteriaID=1,GetAchievementNumCriteria(achievementID),1 do
local criteriaString, criteriaType, completed, quantity, reqQuantity, charName, flags, assetID, quantityString, uniqueID = GetAchievementCriteriaInfo(achievementID, criteriaID, true);
if not uniqueID or uniqueID <= 0 then uniqueID = criteriaID; end
local criteriaObject = app.CreateAchievementCriteria(uniqueID);
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
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,s in ipairs(cache) do
local ref = ResolveSymbolicLink(s);
if ref then
local cs = CloneReference(s);
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, s);
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 s = searchResults[k];
if s.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,s in ipairs(searchResults) do
tinsert(finalized, s);
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)();
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:IncompleteThings") 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 reason = L["UNOBTAINABLE_ITEM_REASONS"][group.u];
if reason and (not reason[5] or app.GameBuildVersion < reason[5]) then
o.texture = L["UNOBTAINABLE_ITEM_TEXTURES"][reason[1]];
end
elseif group.e then
o.texture = L["UNOBTAINABLE_ITEM_TEXTURES"][4];
end
if o.texture then
o.prefix = string.sub(o.prefix, 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 GetCachedSearchResults(search, method, paramA, paramB, ...)
if search then
local now = time();
local cache = searchCache[search];
if cache and (now - cache[1]) < cache[2] then return cache[3]; end
-- Determine if this tooltip needs more work the next time it refreshes.
if not paramA then paramA = ""; end
local working, info, crafted, recipes, mostAccessibleSource = false, {}, {}, {};
cache = { now, 100000000 };
searchCache[search] = cache;
-- Call to the method to search the database.
local group, a, b = method(paramA, paramB, ...);
if not group then
group = {};
elseif group.g then
group = group.g;
end
if a then paramA = a; end
if b then paramB = b; end
-- For Creatures that are inside of an instance, we only want the data relevant for the instance.
if paramA == "creatureID" or paramA == "encounterID" then
if group and #group > 0 then
local difficultyID = (IsInInstance() and select(3, GetInstanceInfo())) or (paramA == "encounterID" and EJ_GetDifficulty and EJ_GetDifficulty()) or 0;
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.Settings:Get("DebugMode") then
for i,j in ipairs(group) do
tinsert(regroup, j);
end
else
if app.Settings:Get("AccountMode") 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 IsQuestComplete(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.RecursiveClassAndRaceFilter(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 IsQuestComplete(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
if app.Settings:GetTooltipSetting("Lore") then
for i,j in ipairs(regroup) do
if j.lore and j[paramA] and j[paramA] == paramB then
tinsert(info, 1, { left = j.lore, wrap = true, color = app.Colors.TooltipLore });
end
end
end
if app.Settings:GetTooltipSetting("Descriptions") then
for i,j in ipairs(regroup) do
if j.description and j[paramA] and j[paramA] == paramB then
tinsert(info, 1, { left = j.description, wrap = true, color = app.Colors.TooltipDescription });
end
end
end
app.Sort(regroup, function(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);
end
group = regroup;
end
elseif paramA == "titleID" then
-- Don't do anything
local regroup = {};
if app.Settings:Get("AccountMode") 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.RecursiveClassAndRaceFilter(j) and app.RecursiveUnobtainableFilter(j) then
tinsert(regroup, setmetatable({["g"] = {}}, { __index = j }));
end
end
end
group = regroup;
else
-- Determine if this is a cache for an item
local itemID;
if not paramB then
local itemString = string.match(paramA, "item[%-?%d:]+");
if itemString then
local itemID2 = select(2, strsplit(":", itemString));
if itemID2 then
itemID = tonumber(itemID2);
paramA = "itemID";
paramB = itemID;
end
else
local kind, id = strsplit(":", paramA);
kind = string.lower(kind);
if id then id = tonumber(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
if itemID then
-- Show the unobtainable source text
local u, e = 99999999;
app.Sort(group, app.SortDefaults.Accessibility);
for i,j in ipairs(group) do
if j.itemID == itemID then
mostAccessibleSource = j;
if j.u and u > j.u and (not j.crs or paramA == "itemID") then
u = j.u;
end
if j.e then
e = j.e;
end
break;
end
end
if u < 99999999 then
local reason = L["UNOBTAINABLE_ITEM_REASONS"][u];
if reason and (not reason[5] or app.GameBuildVersion < reason[5]) then
tinsert(info, { left = reason[2], wrap = true });
end
end
if e then
local reason = app.Modules.Events.GetEventTooltipNoteForGroup({ e = e });
if reason then
local left, right = strsplit(DESCRIPTION_SEPARATOR, reason);
if right then
tinsert(info, { left = left, right = right, color = app.Colors.TooltipDescription });
else
tinsert(info, { left = left, color = app.Colors.TooltipDescription });
end
end
end
local itemName, itemLink = GameTooltip:GetItem();
if app.Settings:GetTooltipSetting("itemID") then tinsert(info, { left = L["ITEM_ID"], right = tostring(itemID) }); end
if app.Settings:GetTooltipSetting("itemLevel") then tinsert(info, { left = "Item Level", right = select(4, GetItemInfo(itemLink or itemID)) }); end
if app.Settings:GetTooltipSetting("itemString") and itemLink then tinsert(info, { left = "Item String", right = string.match(itemLink, "item[%-?%d:]+") }); end
app.ShowSoftReservesForItem(itemID, info);
local reagentCache = app.GetDataSubMember("Reagents", itemID);
if reagentCache then
for spellID,count in pairs(reagentCache[1]) do
MergeClone(recipes, { ["spellID"] = spellID, ["collectible"] = false, ["count"] = count });
end
for craftedItemID,count in pairs(reagentCache[2]) do
MergeClone(crafted, { ["itemID"] = craftedItemID, ["count"] = count });
local searchResults = SearchForField("itemID", craftedItemID);
if #searchResults > 0 then
for i,o in ipairs(searchResults) do
if not o.itemID and o.cost then
-- Reagent for something that crafts a thing required for something else.
MergeClone(group, { ["itemID"] = craftedItemID, ["count"] = count, ["g"] = { CreateObject(o) } });
end
end
end
end
end
end
end
-- Create a list of sources
if app.Settings:GetTooltipSetting("SourceLocations") and (not paramA or (app.Settings:GetTooltipSetting(paramA == "creatureID" and "SourceLocations:Creatures" or "SourceLocations:Things"))) then
local temp = {};
local unfiltered = {};
local abbrevs = L["ABBREVIATIONS"];
local abbrevs_post = L["ABBREVIATIONS_POST"];
if not abbrevs_post[" true "] then
abbrevs_post[" %> " .. app.GetMapName(947)] = "";
abbrevs_post[" %> " .. app.GetMapName(1415)] = "";
abbrevs_post[" %> " .. app.GetMapName(1414)] = "";
abbrevs_post[" false "] = " 0 ";
abbrevs_post[" true "] = " 1 ";
end
for i,j in ipairs(group) do
if j.parent and not GetRelativeValue(j, "hideText") and j.parent.parent
and (app.Settings:GetTooltipSetting("SourceLocations:Completed") or not app.IsComplete(j)) then
local text = BuildSourceText(paramA ~= "itemID" and j.parent or j, paramA ~= "itemID" and 1 or 0);
for source,replacement in pairs(abbrevs) do
text = string.gsub(text, source, replacement);
end
for source,replacement in pairs(abbrevs_post) do
text = string.gsub(text, source, replacement);
end
local right = " ";
if j.u then
local reason = L["UNOBTAINABLE_ITEM_REASONS"][j.u];
if reason and (not reason[5] or app.GameBuildVersion < reason[5]) then
right = "|T" .. L["UNOBTAINABLE_ITEM_TEXTURES"][reason[1]] .. ":0|t";
end
end
if j.rwp then right = right .. "|T" .. L["UNOBTAINABLE_ITEM_TEXTURES"][2] .. ":0|t"; end
if j.e then right = right .. "|T" .. L["UNOBTAINABLE_ITEM_TEXTURES"][4] .. ":0|t"; end
if not app.RecursiveClassAndRaceFilter(j.parent) then
tinsert(unfiltered, { text, right .. "|TInterface\\FriendsFrame\\StatusIcon-Away:0|t" });
elseif not app.RecursiveUnobtainableFilter(j.parent) then
tinsert(unfiltered, { text, right .. "|TInterface\\FriendsFrame\\StatusIcon-DnD:0|t" });
else
tinsert(temp, { text, right });
end
end
end
if (#temp < 1 and not (paramA == "creatureID")) or app.Settings:Get("DebugMode") then
for i,data in ipairs(unfiltered) do
tinsert(temp, data);
end
end
if #temp > 0 then
local listing, listingByText = {}, {};
local maximum = app.Settings:GetTooltipSetting("Locations");
for i,data in ipairs(temp) do
local text = data[1] or RETRIEVING_DATA;
if not listingByText[text] then
listingByText[text] = data;
tinsert(listing, 1, data);
if string.find(text, RETRIEVING_DATA) then working = true; end
end
end
local count, splitCounts, splitCount = 0, { };
for i,data in ipairs(listing) do
local left, right = strsplit(DESCRIPTION_SEPARATOR, data[1]);
left = left .. data[2];
splitCount = splitCounts[left];
if not splitCount then
splitCount = { count = 0, data=data, variants ={} };
splitCounts[left] = splitCount;
end
if right and not contains(splitCount.variants, right) then
tinsert(splitCount.variants, right);
if string.find(right, BATTLE_PET_SOURCE_2) then
splitCount.count = splitCount.count + 1;
end
end
end
for left,splitCount in pairs(splitCounts) do
if splitCount.count < 6 then
if #splitCount.variants < 1 then
tinsert(info, 1, { left = left, wrap = not string.find(left, " > ") });
count = count + 1;
else
for i,right in ipairs(splitCount.variants) do
tinsert(info, 1, { left = left, right = right, wrap = not string.find(left, " > ") });
count = count + 1;
end
end
else
tinsert(info, 1, { left = left, right = TRACKER_HEADER_QUESTS, wrap = not string.find(left, " > ") });
count = count + 1;
for i,right in ipairs(splitCount.variants) do
if not string.find(right, BATTLE_PET_SOURCE_2) then
tinsert(info, 1, { left = left, right = right, wrap = not string.find(left, " > ") });
count = count + 1;
end
end
end
end
if count > maximum + 1 then
for i=count,maximum + 1,-1 do
tremove(info, 1);
end
tinsert(info, 1, "And " .. (count - maximum) .. " other sources...");
end
end
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] = CreateObject(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] = CreateObject(symbolicLink[j]);
end
o.g = symbolicLink;
end
end
end
group = CreateObject({ [paramA] = paramB });
group.g = merged;
end
end
if mostAccessibleSource then
group.rwp = mostAccessibleSource.rwp;
group.e = mostAccessibleSource.e;
group.u = mostAccessibleSource.u;
end
-- Resolve Cost
--print("GetCachedSearchResults", 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 = CreateObject(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
if group.g then
group.total = 0;
group.progress = 0;
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.lore and app.Settings:GetTooltipSetting("Lore") and not (paramA == "titleID") then
tinsert(info, 1, { left = group.lore, wrap = true, color = app.Colors.TooltipLore });
end
if group.description and app.Settings:GetTooltipSetting("Descriptions") and not (paramA == "titleID") then
tinsert(info, 1, { left = group.description, wrap = true, color = app.Colors.TooltipDescription });
end
if group.nextEvent then
local timeStrings = app.Modules.Events.GetEventTimeStrings(group.nextEvent);
if timeStrings then
for i,timeString in ipairs(timeStrings) do
tinsert(info, 1, { left = timeString, wrap = true, color = app.Colors.TooltipDescription });
end
end
end
local awp, rwp = GetRelativeValue(group, "awp"), group.rwp;
local awpGreaterThanRWP = true;
if awp and ((rwp or (group.u and group.u < 3)) or awp >= app.GameBuildVersion) then
awpGreaterThanRWP = rwp and awp >= rwp;
local awpString = GetAddedWithPatchString(awp, awpGreaterThanRWP);
if awpString then
tinsert(info, 1, { left = awpString, wrap = true, color = app.Colors.AddedWithPatch });
else
awpGreaterThanRWP = true;
end
end
if rwp then
tinsert(info, awpGreaterThanRWP and 1 or 2, { left = GetRemovedWithPatchString(rwp), wrap = true, color = app.Colors.RemovedWithPatch });
end
if group.isLimited then
tinsert(info, 1, { left = L.LIMITED_QUANTITY, wrap = true, color = app.Colors.TooltipDescription });
end
if group.pvp then
tinsert(info, { left = L["REQUIRES_PVP"] });
end
local showOtherCharacterQuests = app.Settings:GetTooltipSetting("Show:OtherCharacterQuests");
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(info, { 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 = 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(info, { left = item.prefix .. left, right = item.right });
if item.group.questID and not item.group.repeatable and showOtherCharacterQuests then
local incompletes = {};
for guid,character in pairs(ATTCharacterData) do
if not character.ignored and character.realm == realmName
and (not item.group.r or (character.factionID and item.group.r == character.factionID))
and (not item.group.races or (character.raceID and contains(item.group.races, character.raceID)))
and (not item.group.c or (character.classID and contains(item.group.c, character.classID)))
and (character.Quests and not character.Quests[item.group.questID]) then
incompletes[guid] = character;
end
end
local desc, j = "", 0;
for guid,character in pairs(incompletes) do
if j > 0 then desc = desc .. ", "; end
desc = desc .. (character.text or guid);
j = j + 1;
end
if j > 0 then
tinsert(info, { left = " ", right = string.gsub(desc, "-" .. realmName, ""), hash = "HASH" .. item.group.questID });
end
end
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 = 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(info, { left = item.prefix .. left, right = item.right });
if item.group.questID and not item.group.repeatable and showOtherCharacterQuests then
local incompletes = {};
for guid,character in pairs(ATTCharacterData) do
if not character.ignored and character.realm == realmName and character.Quests and not character.Quests[item.group.questID] then
incompletes[guid] = character;
end
end
local desc, j = "", 0;
for guid,character in pairs(incompletes) do
if j > 0 then desc = desc .. ", "; end
desc = desc .. (character.text or guid);
j = j + 1;
end
tinsert(info, { left = " ", right = string.gsub(desc, "-" .. realmName, ""), hash = "HASH" .. item.group.questID });
end
end
local more = #entries - 25;
if more > 0 then tinsert(info, { left = "And " .. more .. " more..." }); end
end
end
end
-- Crafted Items
if crafted and #crafted > 0 then
if app.Settings:GetTooltipSetting("Show:CraftedItems") then
local entries = {};
BuildContainsInfo(crafted, entries, paramA, paramB, " ", app.noDepth and 99 or 1);
if #entries > 0 then
local left, right;
tinsert(info, { 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(info, { 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(info, { left = item.prefix .. left, right = item.right });
end
local more = #entries - 25;
if more > 0 then tinsert(info, { left = "And " .. more .. " more..." }); end
end
end
end
end
-- Recipes
if recipes and #recipes > 0 then
if app.Settings:GetTooltipSetting("Show:Recipes") then
local entries, left, right = {};
BuildContainsInfo(recipes, entries, paramA, paramB, " ", app.noDepth and 99 or 1);
if #entries > 0 then
tinsert(info, { left = "Used in Recipes:" });
if #entries < 25 then
app.Sort(entries, function(a, b)
if a and a.group.name then
if b and 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(info, { 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(info, { left = item.prefix .. left, right = item.right });
end
local more = #entries - 25;
if more > 0 then tinsert(info, { left = "And " .. more .. " more..." }); end
end
end
end
if app.Settings:GetTooltipSetting("Show:SpellRanks") then
if app.Settings:Get("AccountMode") or app.Settings:Get("DebugMode") then
-- Show all characters
else
-- Show only the current character
local nonTrivialRecipes = {};
for i, o in pairs(recipes) do
local craftTypeID = app.CurrentCharacter.SpellRanks[o.spellID];
if craftTypeID and craftTypeID > 0 then
o.craftTypeID = craftTypeID;
tinsert(nonTrivialRecipes, o);
end
end
local entries, left, right = {};
BuildContainsInfo(nonTrivialRecipes, entries, paramA, paramB, " ", app.noDepth and 99 or 1);
if #entries > 0 then
tinsert(info, { left = "Available Skill Ups:" });
if #entries < 25 then
app.Sort(entries, function(a, b)
if a.group.craftTypeID == b.group.craftTypeID then
if a.group.name then
if b.group.name then
return a.group.name <= b.group.name;
end
return true;
end
return false;
end
return a.group.craftTypeID > b.group.craftTypeID;
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(info, { 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(info, { left = item.prefix .. left, right = item.right });
end
local more = #entries - 25;
if more > 0 then tinsert(info, { left = "And " .. more .. " more..." }); end
end
end
end
end
end
end
-- If the item is a recipe, then show which characters know this recipe.
if group.collectible and app.Settings:GetTooltipSetting("KnownBy") then
local knownBy, kind = {}, nil;
if group.speciesID then
kind = "Owned by ";
for guid,character in pairs(ATTCharacterData) do
if character.BattlePets and character.BattlePets[group.speciesID] then
tinsert(knownBy, character);
end
end
elseif group.spellID then
kind = "Known by ";
for guid,character in pairs(ATTCharacterData) do
if character.Spells and character.Spells[group.spellID] then
tinsert(knownBy, character);
end
end
elseif group.toyID then
kind = "Owned by ";
for guid,character in pairs(ATTCharacterData) do
if character.Toys and character.Toys[group.itemID] then
tinsert(knownBy, character);
end
end
elseif group.itemID then
kind = "Owned by ";
for guid,character in pairs(ATTCharacterData) do
if (character.RWP and character.RWP[group.itemID]) then
tinsert(knownBy, character);
end
end
elseif group.achievementID then
kind = "Completed by ";
for guid,character in pairs(ATTCharacterData) do
if character.Achievements and character.Achievements[group.achievementID] then
tinsert(knownBy, character);
end
end
elseif group.questID then
kind = "Completed by ";
for guid,character in pairs(ATTCharacterData) do
if character.Quests and character.Quests[group.questID] then
tinsert(knownBy, character);
end
end
end
if #knownBy > 0 and kind then
app.Sort(knownBy, app.SortDefaults.Name);
local desc = kind;
for i,character in ipairs(knownBy) do
if i > 1 then desc = desc .. ", "; end
desc = desc .. (character.text or "???");
if group.itemID and character == app.CurrentCharacter then
local count = GetItemCount(group.itemID, true);
if count and count > 1 then
desc = desc .. " (x" .. count .. ")";
end
end
end
tinsert(info, { left = string.gsub(desc, "-" .. GetRealmName(), ""), wrap = true, color = app.Colors.TooltipDescription });
end
end
-- If the user wants to show the progress of this search result, do so.
if app.Settings:GetTooltipSetting("Progress") and (not group.spellID or #info > 0) then
group.collectionText = (app.Settings:GetTooltipSetting("ShowIconOnly") and GetProgressTextForRow or GetProgressTextForTooltip)(group);
-- add the progress as a new line for encounter tooltips instead of using right text since it can overlap the NPC name
if group.encounterID then tinsert(info, 1, { left = "Progress", right = group.collectionText }); end
end
-- If there was any informational text generated, then attach that info.
if #info > 0 then
local uniques, dupes, _ = {}, {};
for i,item in ipairs(info) do
_ = item.hash or item.left;
if not _ then
tinsert(uniques, item);
else
if item.right then _ = _ .. item.right; end
if not dupes[_] then
dupes[_] = true;
tinsert(uniques, item);
end
end
end
for i,item in ipairs(uniques) do
if item.color then item.r, item.g, item.b = HexToRGB(item.color); end
end
group.tooltipInfo = uniques;
end
-- Cache the result for a while depending on if there is more work to be done.
cache[2] = (working and 0.01) or 100000000;
cache[3] = group;
return group;
end
end
local function SendGroupChatMessage(msg)
if IsInRaid() then
SendChatMessage(msg, "RAID", nil, nil);
elseif IsInGroup(LE_PARTY_CATEGORY_HOME) then
SendChatMessage(msg, "PARTY", nil, nil);
end
end
local function SendGroupMessage(msg)
if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and IsInInstance() then
C_ChatInfo.SendAddonMessage("ATTC", msg, "INSTANCE_CHAT")
elseif IsInRaid() then
C_ChatInfo.SendAddonMessage("ATTC", msg, "RAID")
elseif IsInGroup(LE_PARTY_CATEGORY_HOME) then
C_ChatInfo.SendAddonMessage("ATTC", msg, "PARTY")
end
end
local function SendGuildMessage(msg)
if IsInGuild() then
C_ChatInfo.SendAddonMessage("ATTC", msg, "GUILD");
else
app.events.CHAT_MSG_ADDON("ATTC", msg, "WHISPER", "player");
end
end
local function SendResponseMessage(msg, player)
if UnitInRaid(player) or UnitInParty(player) then
SendGroupMessage("to\t" .. player .. "\t" .. msg);
else
C_ChatInfo.SendAddonMessage("ATTC", msg, "WHISPER", player);
end
end
-- Item Information Lib
local function SearchForLink(link)
if string.match(link, "item") then
-- Parse the link and get the itemID and bonus ids.
local itemString = string.match(link, "item[%-?%d:]+") or link;
if itemString then
-- Cache the Item ID and the Suffix ID.
local _, itemID, _, _, _, _, _, suffixID = strsplit(":", itemString);
if itemID then
-- Ensure that the itemID and suffixID are properly formatted.
itemID = tonumber(itemID) or 0;
if itemID > 0 then
if suffixID and suffixID ~= "" then
suffixID = tonumber(suffixID) or 0;
if suffixID > 0 then
-- Record the Suffix as valid for this itemID.
local suffixes = GetDataSubMember("ValidSuffixesPerItemID", itemID);
if not suffixes then
suffixes = {};
GetDataSubMember("ValidSuffixesPerItemID", itemID, suffixes);
end
if not suffixes[suffixID] then
suffixes[suffixID] = 1;
app.ClearItemCache();
end
end
end
return SearchForField("itemID", itemID), "itemID", itemID;
end
end
end
else
local kind, id = strsplit(":", link);
kind = string.gsub(string.lower(kind), "id", "ID");
if string.sub(kind,1,2) == "|c" then
kind = string.sub(kind,11);
end
if string.sub(kind,1,2) == "|h" then
kind = string.sub(kind,3);
end
if id then id = tonumber(strsplit("|[", id) or id); end
--print("SearchForLink A:", kind, id);
--print("SearchForLink B:", string.gsub(string.gsub(link, "|c", "c"), "|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";
end
local cache;
if id then
cache = SearchForField(kind, id);
if #cache == 0 then
local obj = CreateObject({
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
return {}, "", 0;
end
local function SearchForMissingItemsRecursively(group, listing)
if group.visible then
if group.itemID and (group.collectible or (group.total and group.total > 0)) and (not group.b or group.b == 2 or group.b == 3) then
tinsert(listing, group);
end
if group.g and group.expanded then
-- Go through the sub groups and determine if any of them have a response.
for i, subgroup in ipairs(group.g) do
SearchForMissingItemsRecursively(subgroup, listing);
end
end
end
end
local function SearchForMissingItems(group)
local listing = {};
SearchForMissingItemsRecursively(group, listing);
return listing;
end
local function SearchForMissingItemNames(group)
-- Auctionator needs unique Item Names. Nothing else.
local uniqueNames = {};
for i,group in ipairs(SearchForMissingItems(group)) do
local name = group.name;
if name then uniqueNames[name] = 1; end
end
-- Build the array of names.
local arr = {};
for key,value in pairs(uniqueNames) do
tinsert(arr, key);
end
return arr;
end
local function UpdateSearchResults(searchResults)
if searchResults and #searchResults > 0 then
-- Attempt to cleanly refresh the data.
local fresh = false;
-- Mark all results as marked. This prevents a double +1 on parents.
for i,result in ipairs(searchResults) do
if result.visible and result.parent and result.parent.total then
result.marked = true;
end
end
-- Only unmark and +1 marked search results.
for i,result in ipairs(searchResults) do
if result.marked then
result.marked = nil;
if result.total then
-- This is an item that has a relative set of groups
app.UpdateParentProgress(result);
-- If this is NOT a group...
if not result.g then
-- If we've collected the item, use the "Show Collected Items" filter.
result.visible = app.CollectedItemVisibilityFilter(result);
end
else
app.UpdateParentProgress(result.parent);
-- If we've collected the item, use the "Show Collected Items" filter.
result.visible = app.CollectedItemVisibilityFilter(result);
end
fresh = true;
end
end
-- If the data is fresh, don't force a refresh.
if fresh then
app:RefreshDataCompletely("UpdateSearchResults");
else
app:RefreshDataQuietly("UpdateSearchResults", true);
end
end
end
app.SearchForLink = SearchForLink;
-- Dynamic Categories
do
local onClickForDynamicCategory = function(row, button)
local window = row.ref.dynamicWindow;
if window then
if button == "RightButton" then
window:Toggle();
return true;
elseif not row.ref.g or #row.ref.g < 1 then
if #window.data.g < 1 then window:ForceRebuild(); end
local prime = app:GetWindow("Prime");
local primeData = prime.data;
if primeData then
local progress, total = window.data.progress or 0, window.data.total or 0;
local g = CloneReference(window.data).g;
for i,o in ipairs(g) do
o.parent = row.ref;
end
row.ref.g = g;
row.ref.progress = progress;
row.ref.total = total;
prime:Refresh();
end
end
end
end
local onUpdateForDynamicCategory = function(data)
data.progress = nil; data.total = nil;
data.dynamicWindow:ForceRebuild();
--print("onUpdateForDynamicCategory", data.text, data.progress, data.total);
local parent, total = data.parent, data.total;
if parent and total then
parent.progress = parent.progress + data.progress;
parent.total = parent.total + total;
data.visible = app.GroupVisibilityFilter(data);
else
data.visible = true;
end
return true;
end
app.CreateDynamicCategory = app.CreateClass("DynamicCategory", "suffix", {
["dynamicWindow"] = function(t)
local window = app:GetWindow(t.suffix);
if window then return window; end
end,
["dynamicWindowData"] = function(t)
local window = app:GetWindow(t.suffix);
if window and window.data then
return window.data;
end
return app.EmptyTable;
end,
["text"] = function(t)
return t.dynamicWindowData.text or ("Dynamic Category: " .. t.suffix);
end,
["icon"] = function(t)
return t.dynamicWindowData.icon or "Interface/ICONS/INV_Misc_Gear_02";
end,
["description"] = function(t)
return t.dynamicWindowData.description;
end,
["progress"] = function(t)
return t.dynamicWindowData.progress;
end,
["total"] = function(t)
return t.dynamicWindowData.total;
end,
["summary"] = function(t)
local total = t.total;
if not total or total < 1 then
return "[Click to Cache]";
end
end,
["OnClick"] = function(t)
return onClickForDynamicCategory;
end,
["OnUpdate"] = function(t)
return onUpdateForDynamicCategory;
end,
});
end
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 app.GetProgressColorText(primeData.progress, primeData.total);
end
end
return app.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.GetNumberOfItemsUntilNextPercentage(primeData.progress, primeData.total);
end
end
return app.GetNumberOfItemsUntilNextPercentage(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,
OnUpdate = function(t)
local now = C_DateAndTime_GetServerTimeLocal();
app.Sort(t.g, function(a, b)
return (a.nextEvent and a.nextEvent.start or 0) < (b.nextEvent and b.nextEvent.start or 0);
end);
local temp = {};
for i=#t.g,1,-1 do
local a = t.g[i];
if a and a.nextEvent and a.nextEvent["end"] < now then
tremove(t.g, i);
tinsert(temp, a);
end
end
for i=#temp,1,-1 do
tinsert(t.g, temp[i]);
end
end,
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
-- 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
-- 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
-- Now that we have all of the root data, cache it.
app.CacheFields(rootData);
-- The achievements window has a mix of dynamic and non-dynamic information.
local achievementDynamicCategory = app.CreateDynamicCategory("Achievements");
BuildGroups(achievementDynamicCategory.dynamicWindow.data);
tinsert(g, achievementDynamicCategory);
-- Dynamic Categories (Content generated and managed by a separate Window)
tinsert(g, app.CreateDynamicCategory("Battle Pets"));
tinsert(g, app.CreateDynamicCategory("Factions"));
tinsert(g, app.CreateDynamicCategory("Flight Paths"));
if C_Heirloom then tinsert(g, app.CreateDynamicCategory("Heirlooms")); end
tinsert(g, app.CreateDynamicCategory("Mounts"));
tinsert(g, app.CreateDynamicCategory("Titles"));
tinsert(g, app.CreateDynamicCategory("Toys"));
-- 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.Settings:Get("DebugMode") then
self.collectible = true;
else
self.collectible = false;
end
end
}));
-- Now assign the parent hierarchy for this cache.
BuildGroups(rootData);
-- Determine how many tierID instances could be found
local tierCounter = 0;
local tierCache = SearchForFieldContainer("tierID");
for key,value in pairs(tierCache) do
tierCounter = tierCounter + 1;
end
if tierCounter == 1 then
-- Purge the Tier Objects. This is the Classic Layout style.
for key,values in pairs(tierCache) do
for j,value in ipairs(values) do
local parent = value.parent;
if parent then
-- Remove the tier 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 tier object cache.
wipe(tierCache);
end
-- All future calls to this function will return the root data.
app.GetDataCache = function()
return rootData;
end
return rootData;
end
end
-- Tooltip Functions
local EXTERMINATOR = {
["Player-4372-00B131BB"] = true, -- Aivet
["Player-4372-004A0418"] = true, -- Jubilee
["Player-4372-00273DCA"] = true, -- Havadin
["Player-4372-00DED426"] = true, -- Krieve
["Player-4372-00862D32"] = true, -- Aethbric
["Player-4372-0128B376"] = true, -- Alizewsaur
["Player-4372-012A730E"] = true, -- Allysandra
["Player-4372-00FE5CE7"] = true, -- Amiera
["Player-4372-0073B95B"] = true, -- Amyralynn
["Player-4372-0087049A"] = true, -- Asandra
["Player-4372-003159A9"] = true, -- Astromarus
["Player-4372-006A97BA"] = true, -- Azwel
["Player-4372-0014521D"] = true, -- Bombeon
["Player-4372-00E86132"] = true, -- Borlemont
["Player-4372-010B9178"] = true, -- Braven
["Player-4372-0063664F"] = true, -- Brittbrat
["Player-4372-001BA8B1"] = true, -- Darkirontank
["Player-4372-0100DF23"] = true, -- Dizplaced
["Player-4372-01230376"] = true, -- Drixxtwo
["Player-4372-002719C4"] = true, -- Drunkninja
["Player-4372-0124174F"] = true, -- Dubsteve
["Player-4372-00BD6CC7"] = true, -- Enthira
["Player-4372-00A3A0FD"] = true, -- Fairplay
["Player-4372-0046DDA0"] = true, -- Firasia
["Player-4372-004A7A3F"] = true, -- Fortress
["Player-4372-00CF7821"] = true, -- Glas
["Player-4372-00E7DEC4"] = true, -- Gnubs
["Player-4372-0108DCC1"] = true, -- Grotesque
["Player-4372-00E8CC3C"] = true, -- Hairyplodder
["Player-4372-00D38E94"] = true, -- Havachant
["Player-4372-00312AD9"] = true, -- Hewn
["Player-4372-0046F7E8"] = true, -- Holochops
["Player-4372-007AF4B7"] = true, -- Intothefray
["Player-4372-01125B10"] = true, -- Jonaya
["Player-4372-011C1FE9"] = true, -- Katalysm
["Player-4372-00EBCC07"] = true, -- Lilithann
["Player-4372-0075A187"] = true, -- Loknido
["Player-4372-01390D2A"] = true, -- Manamontanna
["Player-4372-00FE5DA2"] = true, -- Mimico
["Player-4372-00D7B345"] = true, -- Narom
["Player-4372-01353958"] = true, -- Naromot
["Player-4372-01294037"] = true, -- Necrid
["Player-4372-00793732"] = true, -- Nirv
["Player-4372-01250D6D"] = true, -- Pewpeu
["Player-4372-0008B144"] = true, -- Pixl
["Player-4372-00C2F945"] = true, -- Rooni
["Player-4372-0058A418"] = true, -- Saitosan [Druid]
["Player-4372-0123A5BE"] = true, -- Sheisskopf
["Player-4372-00F82168"] = true, -- Semiha
["Player-4372-001F92DA"] = true, -- Shadrac
["Player-4372-00732218"] = true, -- Solow
["Player-4372-01091DE4"] = true, -- Tacolock
["Player-4372-00451B8E"] = true, -- Tinybit
["Player-4372-00E5AE25"] = true, -- Villeinia
["Player-4372-0066A25C"] = true, -- Viran
["Player-4372-00D96703"] = true, -- Worfin
};
local GOLD_TYCOON = {
["Player-4372-004A0418"] = true, -- Jubilee
["Player-4372-00273DCA"] = true, -- Havadin
["Player-4372-0068D548"] = true, -- Headphones
["Player-4372-00F2D620"] = true, -- Notloknido
["Player-4372-00FF84F0"] = true, -- Saitosan [Priest]
};
local SCARAB_LORD = {
["Player-4372-000B3C4D"] = true, -- Congelatore
["Player-4372-00A64EA0"] = true, -- Macpayn
};
local function AttachTooltipRawSearchResults(self, lineNumber, group)
if group then
-- If there was info text generated for this search result, then display that first.
if group.tooltipInfo and #group.tooltipInfo > 0 then
local left, right, o;
local leftname = self:GetName() .. "TextLeft";
local rightname = self:GetName() .. "TextRight";
for i,entry in ipairs(group.tooltipInfo) do
local found = false;
left = entry.left or " ";
for i=self:NumLines(),1,-1 do
if _G[leftname..i]:GetText() == left then
o = _G[rightname..i];
if o and o:GetText() == entry.right then
found = true;
break;
end
end
end
if not found then
right = entry.right;
if right then
if entry.r then
self:AddDoubleLine(left, right, entry.r, entry.g, entry.b, entry.r, entry.g, entry.b);
else
self:AddDoubleLine(left, right);
end
elseif entry.r then
if entry.wrap then
self:AddLine(left, entry.r, entry.g, entry.b, 1);
else
self:AddLine(left, entry.r, entry.g, entry.b);
end
else
if entry.wrap then
self:AddLine(left, 1, 1, 1, 1);
else
self:AddLine(left);
end
end
end
end
end
-- If the user has Show Collection Progress turned on.
if group.encounterID then
self:Show();
elseif group.collectionText and self:NumLines() > 0 then
local rightSide = _G[self:GetName() .. "TextRight" .. (lineNumber or 1)];
if rightSide then
rightSide:SetText(group.collectionText);
rightSide:Show();
end
end
end
end
local function AttachTooltipSearchResults(self, lineNumber, search, method, ...)
AttachTooltipRawSearchResults(self, lineNumber, GetCachedSearchResults(search, method, ...));
end
local function AttachTooltip(self)
if not self.ATTCProcessing then
self.ATTCProcessing = true;
if (not InCombatLockdown() or app.Settings:GetTooltipSetting("DisplayInCombat")) and app.Settings:GetTooltipSetting("Enabled") then
local numLines = self:NumLines();
if numLines > 0 then
--[[--
-- Debug all of the available fields on the tooltip.
for i,j in pairs(self) do
self:AddDoubleLine(tostring(i), tostring(j));
end
self:Show();
self:AddDoubleLine("GetItem", tostring(select(2, self:GetItem()) or "nil"));
self:AddDoubleLine("GetSpell", tostring(select(2, self:GetSpell()) or "nil"));
self:AddDoubleLine("GetUnit", tostring(select(2, self:GetUnit()) or "nil"));
--]]--
-- Does the tooltip have an owner?
local owner = self:GetOwner();
if owner then
if owner.SpellHighlightTexture and false then
-- Actionbars, don't want that.
return true;
end
end
-- Does the tooltip have a target?
local target = select(2, self:GetUnit());
if target then
-- Yes.
local guid = UnitGUID(target);
if guid then
local type, zero, server_id, instance_id, zone_uid, npcID, spawn_uid = strsplit("-",guid);
-- print(guid, type, npcID);
if type == "Player" then
if guid == "Player-4372-0000390A" then
local leftSide = _G[self:GetName() .. "TextLeft1"];
if leftSide then
leftSide:SetText("|c" .. app.Colors.Raid .. UnitName(target) .. " the Completionist|r");
end
local rightSide = _G[self:GetName() .. "TextRight2"];
leftSide = _G[self:GetName() .. "TextLeft2"];
if leftSide and rightSide then
leftSide:SetText(L["TITLE"]);
leftSide:Show();
rightSide:SetText("Author");
rightSide:Show();
else
self:AddDoubleLine(L["TITLE"], "Author");
end
elseif SCARAB_LORD[guid] then
local leftSide = _G[self:GetName() .. "TextLeft1"];
if leftSide then leftSide:SetText("|c" .. app.Colors.Raid .. "Scarab Lord " .. UnitName(target) .. "|r"); end
elseif GOLD_TYCOON[guid] then
local leftSide = _G[self:GetName() .. "TextLeft1"];
if leftSide then leftSide:SetText("|c" .. app.Colors.Raid .. "Gold Tycoon " .. UnitName(target) .. "|r"); end
elseif EXTERMINATOR[guid] then
local leftSide = _G[self:GetName() .. "TextLeft1"];
if leftSide then leftSide:SetText("|cffa335ee" .. UnitName(target) .. " the Exterminator|r"); end
elseif guid == "Player-4372-00006B41" then
local leftSide = _G[self:GetName() .. "TextLeft1"];
if leftSide then leftSide:SetText("|cffF58CBA" .. UnitName(target) .. " the Huggler|r"); end
elseif guid == "Player-4647-031D0890" then
local leftSide = _G[self:GetName() .. "TextLeft1"];
if leftSide then leftSide:SetText("|cff665a2c" .. UnitName(target) .. " the Time-Loser|r"); end
local rightSide = _G[self:GetName() .. "TextRight2"];
if rightSide then rightSide:SetText(GetCollectionIcon(0)); end
self:AddLine("This scumbag abused an auto-invite addon to steal the Time-Lost Proto Drake from a person that had them on their friends list. ATT has deemed this unacceptable behaviour and will forever stain this player's reputation so long as they remain on the server.", 0.4, 0.8, 1, true);
end
elseif type == "Creature" or type == "Vehicle" then
if app.Settings:GetTooltipSetting("creatureID") then self:AddDoubleLine(L["CREATURE_ID"], tostring(npcID)); end
AttachTooltipSearchResults(self, 1, "creatureID:" .. npcID, SearchForField, "creatureID", tonumber(npcID));
end
return true;
end
end
-- Does the tooltip have a spell? [Mount Journal, Action Bars, etc]
local spellID = select(2, self:GetSpell());
if spellID then
AttachTooltipSearchResults(self, 1, "spellID:" .. spellID, SearchForField, "spellID", spellID);
self:Show();
if owner and owner.ActiveTexture then
self.ATTCProcessing = nil;
end
return true;
end
-- Does the tooltip have an itemlink?
local itemName, link = self:GetItem();
if link then
if owner and owner.cooldownWrapper then
local parent = owner:GetParent();
if parent then
parent = parent:GetParent();
if parent and parent.fanfareToys then
-- Toy Box, it needs a Show call.
-- Also the ToyBox UI is broken and returns the wrong item information when you look at any other item's tooltip before looking at the toybox.
local leftSide = _G[self:GetName() .. "TextLeft1"]:GetText();
if itemName ~= leftSide then link = select(2, GetItemInfo(leftSide)); end
AttachTooltipSearchResults(self, 1, link, SearchForLink, link);
self:Show();
return true;
end
end
end
-- Normal item tooltip, not on the Toy Box.
AttachTooltipSearchResults(self, 1, link, SearchForLink, link);
end
-- If the owner has a ref, it's an ATT row. Ignore it.
if owner and owner.ref then return true; end
local objectID = app.GetBestObjectIDForName(_G[self:GetName() .. "TextLeft1"]:GetText());
if objectID then
AttachTooltipSearchResults(self, 1, "objectID:" .. objectID, SearchForField, "objectID", objectID);
self:Show();
return true;
end
end
end
end
end
local function ClearTooltip(self)
self.ATTCProcessing = nil;
end
local function ShowItemCompareTooltips(...)
local items = { ... };
local count = #items;
if count > 0 then
for i,item in ipairs(items) do
local shoppingTooltip = GameTooltip.shoppingTooltips[i];
if shoppingTooltip then
shoppingTooltip.attItem = type(item) == "number" and select(2, GetItemInfo(item)) or item;
pcall(shoppingTooltip.SetHyperlink, shoppingTooltip, shoppingTooltip.attItem);
else
break;
end
end
-- Quick maths
-- Taken from https://github.com/Ennie/wow-ui-source/blob/master/FrameXML/GameTooltip.lua
local shoppingTooltip1, shoppingTooltip2, shoppingTooltip3 = unpack(GameTooltip.shoppingTooltips);
local leftPos, rightPos = (GameTooltip:GetLeft() or 0), (GameTooltip:GetRight() or 0);
if GetScreenWidth() - rightPos < leftPos then
side = "left";
else
side = "right";
end
-- see if we should slide the tooltip
local anchorType = GameTooltip:GetAnchorType();
if anchorType and anchorType ~= "ANCHOR_PRESERVE" then
local totalWidth = shoppingTooltip1:GetWidth();
if count > 1 then totalWidth = totalWidth + shoppingTooltip2:GetWidth(); end
if count > 2 then totalWidth = totalWidth + shoppingTooltip3:GetWidth(); end
if ( (side == "left") and (totalWidth > leftPos) ) then
GameTooltip:SetAnchorType(anchorType, (totalWidth - leftPos), 0);
elseif ( (side == "right") and (rightPos + totalWidth) > GetScreenWidth() ) then
GameTooltip:SetAnchorType(anchorType, -((rightPos + totalWidth) - GetScreenWidth()), 0);
end
end
-- anchor the compare tooltips
if count > 2 then
shoppingTooltip3:SetOwner(GameTooltip, "ANCHOR_NONE");
shoppingTooltip3:ClearAllPoints();
if side == "left" then
shoppingTooltip3:SetPoint("TOPRIGHT", GameTooltip, "TOPLEFT", 0, -10);
else
shoppingTooltip3:SetPoint("TOPLEFT", GameTooltip, "TOPRIGHT", 0, -10);
end
pcall(shoppingTooltip3.SetHyperlink, shoppingTooltip3, shoppingTooltip3.attItem);
shoppingTooltip3:Show();
shoppingTooltip1:SetOwner(shoppingTooltip3, "ANCHOR_NONE");
else
shoppingTooltip1:SetOwner(GameTooltip, "ANCHOR_NONE");
end
shoppingTooltip1:ClearAllPoints();
if side == "left" then
if count > 2 then
shoppingTooltip1:SetPoint("TOPRIGHT", shoppingTooltip3, "TOPLEFT", 0, 0);
else
shoppingTooltip1:SetPoint("TOPRIGHT", GameTooltip, "TOPLEFT", 0, -10);
end
else
if count > 2 then
shoppingTooltip1:SetPoint("TOPLEFT", shoppingTooltip3, "TOPRIGHT", 0, 0);
else
shoppingTooltip1:SetPoint("TOPLEFT", GameTooltip, "TOPRIGHT", 0, -10);
end
end
pcall(shoppingTooltip1.SetHyperlink, shoppingTooltip1, shoppingTooltip1.attItem);
shoppingTooltip1:Show();
if count > 1 then
shoppingTooltip2:SetOwner(shoppingTooltip1, "ANCHOR_NONE");
shoppingTooltip2:ClearAllPoints();
if side == "left" then
shoppingTooltip2:SetPoint("TOPRIGHT", shoppingTooltip1, "TOPLEFT", 0, 0);
else
shoppingTooltip2:SetPoint("TOPLEFT", shoppingTooltip1, "TOPRIGHT", 0, 0);
end
pcall(shoppingTooltip2.SetHyperlink, shoppingTooltip2, shoppingTooltip2.attItem);
shoppingTooltip2:Show();
end
return shoppingTooltip1, shoppingTooltip2, shoppingTooltip3;
end
end
app.ShowItemCompareTooltips = ShowItemCompareTooltips;
-- 10.0.2
-- https://wowpedia.fandom.com/wiki/Patch_10.0.2/API_changes#Tooltip_Changes
if TooltipDataProcessor then
local Enum_TooltipDataType = Enum.TooltipDataType;
local TooltipTypes = {
[Enum_TooltipDataType.Toy] = "itemID",
[Enum_TooltipDataType.Item] = "itemID",
[Enum_TooltipDataType.Spell] = "spellID",
[Enum_TooltipDataType.UnitAura] = "spellID",
--[Enum_TooltipDataType.Mount] = "spellID",
--[Enum_TooltipDataType.Macro] = "spellID", -- Possibly?
[Enum_TooltipDataType.Achievement] = "achievementID",
[Enum_TooltipDataType.Quest] = "questID",
[Enum_TooltipDataType.QuestPartyProgress] = "questID",
--[Enum_TooltipDataType.BattlePet] = "speciesID",
--[Enum_TooltipDataType.CompanionPet] = "speciesID",
[Enum_TooltipDataType.Currency] = "currencyID",
[Enum_TooltipDataType.InstanceLock] = "instanceID",
};
--[[
for key,id in pairs(Enum_TooltipDataType) do
if not TooltipTypes[id] then
print("Not handling Tooltip Type", key, id);
end
end
]]--
TooltipDataProcessor.AddTooltipPostCall(TooltipDataProcessor.AllTypes, function(tooltip, data)
if InCombatLockdown() and not app.Settings:GetTooltipSetting("DisplayInCombat") then
return;
end
if not app.Settings:GetTooltipSetting("Enabled") then
return;
end
local enumType = data.type;
local key = TooltipTypes[enumType];
if key then
if key == "" then
-- Intentionally blacklisted.
return;
end
-- Try the default.
local id = data.id;
if id then
AttachTooltipSearchResults(tooltip, 1, key .. ":" .. id, SearchForField, key, id);
return;
end
local name, link, id = TooltipUtil.GetDisplayedItem(tooltip);
if link and id then
if id == 137642 then -- skip Mark of Honor for now
AttachTooltipSearchResults(self, 1, link, app.EmptyFunction, "itemID", 137642);
return true;
else
AttachTooltipSearchResults(tooltip, 1, link, SearchForLink, link);
return;
end
end
local name, spellID = TooltipUtil.GetDisplayedSpell(tooltip);
if spellID then
AttachTooltipSearchResults(tooltip, 1, "spellID:" .. spellID, SearchForField, "spellID", spellID);
return;
end
elseif enumType == 10 then
-- Mounts!
local spellID = select(2, C_MountJournal.GetMountInfoByID(data.id));
if spellID then
AttachTooltipSearchResults(tooltip, 1, "spellID:" .. spellID, SearchForField, "spellID", spellID);
return;
end
elseif enumType == 4 then
-- Objects!
local objectID = app.GetBestObjectIDForName(data.lines[1].leftText);
if objectID then
AttachTooltipSearchResults(tooltip, 1, "objectID:" .. objectID, SearchForField, "objectID", objectID);
return true;
end
elseif enumType == 21 then
-- Minimap Mouseover
local content = data.lines;
if content and #content > 0 then
local text = content[1].leftText;
local arr = { strsplit("|", text) };
if #arr == 3 then text = strsub(arr[3], 2); end
local objectID = app.GetBestObjectIDForName(text);
if objectID then
AttachTooltipSearchResults(tooltip, 1, "objectID:" .. objectID, SearchForField, "objectID", objectID);
return true;
end
end
elseif enumType == 2 then
-- Units!
local name, t, guid = TooltipUtil.GetDisplayedUnit(tooltip);
if guid then
local type, zero, server_id, instance_id, zone_uid, npcID, spawn_uid = strsplit("-",guid);
-- print(guid, type, npcID);
if type == "Player" then
if guid == "Player-4372-0000390A" or guid == "Player-76-0895E23B" then
local leftSide = _G[tooltip:GetName() .. "TextLeft1"];
if leftSide then
leftSide:SetText("|c" .. app.Colors.Raid .. name .. " the Completionist|r");
end
local rightSide = _G[tooltip:GetName() .. "TextRight2"];
leftSide = _G[tooltip:GetName() .. "TextLeft2"];
if leftSide and rightSide then
leftSide:SetText(L["TITLE"]);
leftSide:Show();
rightSide:SetText("Author");
rightSide:Show();
else
tooltip:AddDoubleLine(L["TITLE"], "Author");
end
elseif SCARAB_LORD[guid] then
local leftSide = _G[tooltip:GetName() .. "TextLeft1"];
if leftSide then leftSide:SetText("|c" .. app.Colors.Raid .. "Scarab Lord " .. name .. "|r"); end
elseif GOLD_TYCOON[guid] then
local leftSide = _G[tooltip:GetName() .. "TextLeft1"];
if leftSide then leftSide:SetText("|c" .. app.Colors.Raid .. "Gold Tycoon " .. name .. "|r"); end
elseif EXTERMINATOR[guid] then
local leftSide = _G[tooltip:GetName() .. "TextLeft1"];
if leftSide then leftSide:SetText("|cffa335ee" .. name .. " the Exterminator|r"); end
elseif guid == "Player-4372-00006B41" then
local leftSide = _G[tooltip:GetName() .. "TextLeft1"];
if leftSide then leftSide:SetText("|cffF58CBA" .. name .. " the Huggler|r"); end
elseif guid == "Player-4647-031D0890" then
local leftSide = _G[tooltip:GetName() .. "TextLeft1"];
if leftSide then leftSide:SetText("|cff665a2c" .. name .. " the Time-Loser|r"); end
local rightSide = _G[tooltip:GetName() .. "TextRight2"];
if rightSide then rightSide:SetText(GetCollectionIcon(0)); end
tooltip:AddLine("This scumbag abused an auto-invite addon to steal the Time-Lost Proto Drake from a person that had them on their friends list. ATT has deemed this unacceptable behaviour and will forever stain this player's reputation so long as they remain on the server.", 0.4, 0.8, 1, true);
end
elseif type == "Creature" or type == "Vehicle" then
if app.Settings:GetTooltipSetting("creatureID") then tooltip:AddDoubleLine(L["CREATURE_ID"], tostring(npcID)); end
AttachTooltipSearchResults(tooltip, 1, "creatureID:" .. npcID, SearchForField, "creatureID", tonumber(npcID));
end
return true;
end
elseif enumType == 25 then
-- Macro!
local content = data.lines;
if content and #content > 1 then
local tooltipType = content[2].type;
if tooltipType == 13 then
local spellID = content[1].tooltipID;
if spellID then
AttachTooltipSearchResults(tooltip, 1, "spellID:" .. spellID, SearchForField, "spellID", spellID);
return;
end
elseif tooltipType == 29 then
local itemID = content[1].tooltipID;
if itemID then
AttachTooltipSearchResults(tooltip, 1, "itemID:" .. itemID, SearchForField, "itemID", itemID);
return;
end
end
end
else
--[[
for key,value in pairs(data) do
if type(value) == "table" then
tooltip:AddLine(key);
for key2,value2 in pairs(value) do
tooltip:AddLine(" " .. key2);
if type(value2) == "table" then
for key3,value3 in pairs(value2) do
tooltip:AddDoubleLine(" " .. key3, tostring(value3));
end
else
tooltip:AddDoubleLine(" " .. key2, tostring(value2));
end
end
else
tooltip:AddDoubleLine(key, tostring(value));
end
end
-- Report unhandled types.
for key,id in pairs(Enum_TooltipDataType) do
if id == enumType then
print("Unhandled Tooltip Type", key, enumType);
return;
end
end
print("Unhandled Tooltip Type", enumType);
]]--
end
end)
else
GameTooltip:HookScript("OnTooltipSetQuest", AttachTooltip);
GameTooltip:HookScript("OnTooltipSetItem", AttachTooltip);
GameTooltip:HookScript("OnTooltipSetUnit", AttachTooltip);
GameTooltip:HookScript("OnTooltipCleared", ClearTooltip);
GameTooltip:HookScript("OnShow", AttachTooltip);
GameTooltip:HookScript("OnUpdate", AttachTooltip);
GameTooltip:HookScript("OnHide", ClearTooltip);
ItemRefTooltip:HookScript("OnShow", AttachTooltip);
ItemRefTooltip:HookScript("OnTooltipSetQuest", AttachTooltip);
ItemRefTooltip:HookScript("OnTooltipSetItem", AttachTooltip);
ItemRefTooltip:HookScript("OnTooltipCleared", ClearTooltip);
ItemRefShoppingTooltip1:HookScript("OnShow", AttachTooltip);
ItemRefShoppingTooltip1:HookScript("OnTooltipSetQuest", AttachTooltip);
ItemRefShoppingTooltip1:HookScript("OnTooltipSetItem", AttachTooltip);
ItemRefShoppingTooltip1:HookScript("OnTooltipCleared", ClearTooltip);
ItemRefShoppingTooltip2:HookScript("OnShow", AttachTooltip);
ItemRefShoppingTooltip2:HookScript("OnTooltipSetQuest", AttachTooltip);
ItemRefShoppingTooltip2:HookScript("OnTooltipSetItem", AttachTooltip);
ItemRefShoppingTooltip2:HookScript("OnTooltipCleared", ClearTooltip);
WorldMapTooltip.ItemTooltip.Tooltip:HookScript("OnTooltipSetQuest", AttachTooltip);
WorldMapTooltip.ItemTooltip.Tooltip:HookScript("OnTooltipSetItem", AttachTooltip);
WorldMapTooltip.ItemTooltip.Tooltip:HookScript("OnTooltipSetUnit", AttachTooltip);
WorldMapTooltip.ItemTooltip.Tooltip:HookScript("OnTooltipCleared", ClearTooltip);
WorldMapTooltip:HookScript("OnTooltipSetItem", AttachTooltip);
WorldMapTooltip:HookScript("OnTooltipSetQuest", AttachTooltip);
WorldMapTooltip:HookScript("OnTooltipCleared", ClearTooltip);
WorldMapTooltip:HookScript("OnShow", AttachTooltip);
tinsert(app.EventHandlers.OnReady, function()
local GameTooltip_SetLFGDungeonReward = GameTooltip.SetLFGDungeonReward;
if GameTooltip_SetLFGDungeonReward then
GameTooltip.SetLFGDungeonReward = function(self, dungeonID, rewardIndex)
GameTooltip_SetLFGDungeonReward(self, dungeonID, rewardIndex);
local name, texturePath, quantity, isBonusReward, spec, itemID = GetLFGDungeonRewardInfo(dungeonID, rewardIndex);
if itemID then
if spec == "item" then
AttachTooltipSearchResults(self, 1, "itemID:" .. itemID, SearchForField, "itemID", itemID);
self:Show();
elseif spec == "currency" then
AttachTooltipSearchResults(self, 1, "currencyID:" .. itemID, SearchForField, "currencyID", itemID);
self:Show();
end
end
end
end
local GameTooltip_SetLFGDungeonShortageReward = GameTooltip.SetLFGDungeonShortageReward;
if GameTooltip_SetLFGDungeonShortageReward then
GameTooltip.SetLFGDungeonShortageReward = function(self, dungeonID, shortageSeverity, lootIndex)
GameTooltip_SetLFGDungeonShortageReward(self, dungeonID, shortageSeverity, lootIndex);
local name, texturePath, quantity, isBonusReward, spec, itemID = GetLFGDungeonShortageRewardInfo(dungeonID, shortageSeverity, lootIndex);
if itemID then
if spec == "item" then
AttachTooltipSearchResults(self, 1, "itemID:" .. itemID, SearchForField, "itemID", itemID);
self:Show();
elseif spec == "currency" then
AttachTooltipSearchResults(self, 1, "currencyID:" .. itemID, SearchForField, "currencyID", itemID);
self:Show();
end
end
end
end
end);
end
if app.GameBuildVersion > 11403 then
app:RegisterEvent("CURSOR_CHANGED");
app.events.CURSOR_CHANGED = function()
app:StartATTCoroutine("UpdateTooltip", function()
while not GameTooltip:IsShown() do
coroutine.yield();
end
AttachTooltip(GameTooltip);
end);
end
end
local oldItemSetHyperlink = ItemRefTooltip.SetHyperlink;
function ItemRefTooltip:SetHyperlink(link, a, b, c, d, e, f)
-- Make sure to call the default function, but with a try-catch.
local status, err = pcall(function () oldItemSetHyperlink(self, link, a, b, c, d, e, f) end)
if not status then
-- Search for the Link in the database
local linkType, id, params = strsplit(':', link)
local cmd = linkType .. "ID" .. ":" .. id;
local group = GetCachedSearchResults(cmd, SearchForLink, cmd);
if group then app:CreateMiniListForGroup(group); end
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.Settings:Get("DebugMode") or app.Settings:Get("AccountMode") 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.Settings:Get("DebugMode") or app.Settings:Get("AccountMode") 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 = CloneData(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)
if t.collectible then
local r = t.rep or (t.BuildReputation and t:BuildReputation());
if r then
GameTooltip:AddLine(" ");
GameTooltip:AddDoubleLine(" |T" .. r.icon .. ":0|t " .. r.text, app.L[r.standing == 8 and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"], 1, 1, 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 = CloneData(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)
if t.collectible then
local reps = t.reps or (t.BuildReputations and t:BuildReputations());
if reps then
GameTooltip:AddLine(" ");
for i,faction in ipairs(reps) do
GameTooltip:AddDoubleLine(" |T" .. faction.icon .. ":0|t " .. faction.text, app.L[faction.standing == 8 and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"], 1, 1, 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,
};
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
if t.providers then
for k,v in ipairs(t.providers) do
if v[2] > 0 then
if v[1] == "o" then
return app.ObjectNames[v[2]];
elseif v[1] == "i" then
return GetItemInfo(v[2]);
end
end
end
end
if t.spellID then return GetSpellInfo(t.spellID); end
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
if t.providers then
for k,v in ipairs(t.providers) do
if v[2] > 0 then
if v[1] == "o" then
local icon = app.ObjectIcons[v[2]];
if icon then return icon; end
elseif v[1] == "i" then
return select(5, GetItemInfoInstant(v[2])) or "Interface\\Worldmap\\Gear_64Grey";
end
end
end
end
if t.spellID then return select(3, GetSpellInfo(t.spellID)); end
return t.parent.icon or "Interface\\Worldmap\\Gear_64Grey";
end
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)
local achievementID = t.achievementID;
if achievementID and IsShiftKeyDown() then
local criteriaDatas,criteriaDatasByUID = {}, {};
for criteriaID=1,99999,1 do
local criteriaString, criteriaType, completed, quantity, reqQuantity, charName, flags, assetID, quantityString, criteriaUID = GetAchievementCriteriaInfoByID(achievementID, criteriaID);
if criteriaString 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
local criteriaString, criteriaType, completed, quantity, reqQuantity, charName, flags, 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
GameTooltip:AddLine(" ", 1, 1, 1);
GameTooltip:AddDoubleLine("Total Criteria", tostring(#criteriaDatas), 0.8, 0.8, 1);
for i,criteriaData in ipairs(criteriaDatas) do
GameTooltip:AddDoubleLine(criteriaData[1], criteriaData[2], 1, 1, 1, 1, 1, 1);
end
end
end
end
local onTooltipForAchievementCriteria = function(t)
local achievementID = t.achievementID;
if achievementID then
GameTooltip:AddDoubleLine(L.CRITERIA_FOR, "|cffffff00[" .. (select(2, GetAchievementInfo(achievementID)) or RETRIEVING_DATA) .. "]|r");
if IsShiftKeyDown() then
local criteriaID = t.criteriaID;
if criteriaID then
GameTooltip:AddLine(" ", 1, 1, 1);
local criteriaString, criteriaType, completed, quantity, reqQuantity, charName, flags, assetID, quantityString, criteriaUID = t.GetInfo(achievementID, criteriaID, true)
if criteriaString then
GameTooltip:AddDoubleLine(" [" .. criteriaUID .. "]: " .. tostring(criteriaString),
"(" .. tostring(assetID) .. " @ " .. tostring(criteriaType) .. ") " .. tostring(quantityString) .. " " .. GetCompletionIcon(completed), 1, 1, 1, 1, 1, 1);
end
end
end
end
end
fields.OnTooltip = function(t)
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.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);
if not IsRetrieving(name) then return name; end
local providers = t.providers;
if providers then
for k,v in ipairs(providers) do
if v[2] > 0 then
if v[1] == "o" then
return app.ObjectNames[v[2]];
elseif v[1] == "i" then
return GetItemInfo(v[2]);
elseif v[1] == "n" then
return app.NPCNameFromID[v[2]];
end
end
end
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)
if t.providers then
for k,v in ipairs(t.providers) do
if v[2] > 0 then
if v[1] == "o" then
local icon = app.ObjectIcons[v[2]];
if icon then return icon; end
elseif v[1] == "i" then
return select(5, GetItemInfoInstant(v[2])) or "Interface\\Icons\\INV_Misc_Bag_10";
end
end
end
end
local achievementID = t.achievementID;
if achievementID then
return select(10, GetAchievementInfo(achievementID));
end
end,
["model"] = function(t)
if t.providers then
for k,v in ipairs(t.providers) do
if v[2] > 0 then
if v[1] == "o" then
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));
local function CheckAchievementCollectionStatus(achievementID)
achievementID = tonumber(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);
if achievementID ~= 5788 then
SetAchievementCollected(container[1], id, select(13, GetAchievementInfo(id)));
end
end
end
end
tinsert(app.EventHandlers.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 or RETRIEVING_DATA) .. "]|r";
end
fields.name = function(t)
local data = L.ACHIEVEMENT_DATA[t.achievementID];
if data and data[2] then return data[2]; end
if t.providers then
for k,v in ipairs(t.providers) do
if v[2] > 0 then
if v[1] == "o" then
return app.ObjectNames[v[2]];
elseif v[1] == "i" then
return GetItemInfo(v[2]);
end
end
end
end
if t.spellID then return GetSpellInfo(t.spellID); end
end
fields.icon = function(t)
local data = L.ACHIEVEMENT_DATA[t.achievementID];
if data and data[3] then return data[3]; end
if t.providers then
for k,v in ipairs(t.providers) do
if v[2] > 0 then
if v[1] == "o" then
local icon = app.ObjectIcons[v[2]];
if icon then return icon; end
elseif v[1] == "i" then
return select(5, GetItemInfoInstant(v[2])) or "Interface\\Worldmap\\Gear_64Grey";
end
end
end
end
if t.spellID then return select(3, GetSpellInfo(t.spellID)); end
return t.parent.icon or "Interface\\Worldmap\\Gear_64Grey";
end
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;
if app.FilterItemClass_RequireRaces(t) then
for i,o in ipairs(quests) do
if app.FilterItemClass(o) then
if o.collected == 1 then
p = p + 1;
end
end
end
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 C_QuestLog.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 C_QuestLog.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 = collectible;
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)
GameTooltip:AddLine("Collect " .. t.rank .. " companion pets.");
if t.total and t.progress < t.total and t.rank >= 25 then
GameTooltip:AddLine(" ");
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
GameTooltip:AddDoubleLine(" |T" .. p.icon .. ":0|t " .. p.text, app.L[p.collected and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"], 1, 1, 1);
end
end
end
if c > 15 then GameTooltip:AddLine(" 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.FilterItemClass_UnobtainableItem(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)
if t.collectible and t.spells then
GameTooltip:AddLine(" ");
for i,spell in ipairs(t.spells) do
GameTooltip:AddDoubleLine(" |T" .. spell.icon .. ":0|t " .. spell.text, app.L[spell.collected and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"], 1, 1, 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)
if t.collectible and t.p and not t.collected then
GameTooltip:AddLine(" ");
GameTooltip:AddDoubleLine(" ", app.GetProgressText(min(t.rank, t.p),t.rank), 1, 1, 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.FilterItemClass_UnobtainableItem(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)
if t.collectible and t.achievements then
GameTooltip:AddLine(" ");
for i,achievement in ipairs(t.achievements) do
GameTooltip:AddDoubleLine(" |T" .. achievement.icon .. ":0|t " .. achievement.text, app.L[achievement.collected and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"], 1, 1, 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 = collectible;
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)
GameTooltip:AddLine("Collect " .. t.rank .. " mounts.");
if t.total and t.progress < t.total and t.rank >= 25 then
GameTooltip:AddLine(" ");
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
GameTooltip:AddDoubleLine(" |T" .. p.icon .. ":0|t " .. p.text, app.L[p.collected and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"], 1, 1, 1);
end
end
end
end
if c > 15 then GameTooltip:AddLine(" 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 = collectible;
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)
GameTooltip:AddLine("Raise " .. t.rank .. " reputations to Exalted.");
if t.total and t.progress < t.total and t.rank >= 25 then
GameTooltip:AddLine(" ");
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
GameTooltip:AddDoubleLine(" |T" .. p.icon .. ":0|t " .. p.text, app.L[p.standing >= 8 and "COLLECTED_ICON" or "NOT_COLLECTED_ICON"], 1, 1, 1);
end
end
end
end
end
end
end
app.CreateAchievementCategory = app.CreateClass("AchievementCategory", "achievementCategoryID", categoryFields);
end)();
-- Category Lib
(function()
local defaultIcon = "Interface/ICONS/INV_Misc_Gear_02";
if app.GameBuildVersion > 60001 then
defaultIcon = "Interface/ICONS/INV_Garrison_Blueprints1";
end
app.CreateCategory = app.CreateClass("Category", "categoryID", {
["text"] = function(t)
return AllTheThingsAD.LocalizedCategoryNames[t.categoryID] or ("Unknown Category #" .. t.categoryID);
end,
["icon"] = function(t)
return app.CategoryIcons[t.categoryID] or defaultIcon;
end,
});
end)();
-- Character Class Lib
(function()
local classIcons = {
[1] = app.asset("ClassIcon_Warrior"),
[2] = app.asset("ClassIcon_Paladin"),
[3] = app.asset("ClassIcon_Hunter"),
[4] = app.asset("ClassIcon_Rogue"),
[5] = app.asset("ClassIcon_Priest"),
[6] = app.asset("ClassIcon_DeathKnight"),
[7] = app.asset("ClassIcon_Shaman"),
[8] = app.asset("ClassIcon_Mage"),
[9] = app.asset("ClassIcon_Warlock"),
[10] = app.asset("ClassIcon_Monk"),
[11] = app.asset("ClassIcon_Druid"),
[12] = app.asset("ClassIcon_DemonHunter"),
};
local GetClassIDFromClassFile = function(classFile)
for i,icon in pairs(classIcons) do
local info = C_CreatureInfo.GetClassInfo(i);
if info and info.classFile == classFile then
return i;
end
end
end
app.ClassDB = setmetatable({}, { __index = function(t, className)
for i,_ in pairs(classIcons) do
local info = C_CreatureInfo.GetClassInfo(i);
if info and info.className == className then
rawset(t, className, i);
return i;
end
end
end });
app.CreateCharacterClass, app.BaseCharacterClass = app.CreateClass("CharacterClass", "classID", {
["text"] = function(t)
local text = "|c" .. t.classColors.colorStr .. t.name .. "|r";
rawset(t, "text", text);
return text;
end,
["icon"] = function(t)
return classIcons[t.classID];
end,
["name"] = function(t)
return C_CreatureInfo.GetClassInfo(t.classID).className;
end,
["c"] = function(t)
local c = { t.classID };
rawset(t, "c", c);
return c;
end,
["nmc"] = function(t)
return t.classID ~= app.ClassIndex;
end,
["classColors"] = function(t)
return RAID_CLASS_COLORS[C_CreatureInfo.GetClassInfo(t.classID).classFile];
end,
});
app.CreateUnit = app.CreateClass("Unit", "unit", {
["text"] = function(t)
return t.classText;
end,
["info"] = function(t)
local unit = t.unit;
for guid,character in pairs(ATTCharacterData) do
if guid == unit or character.name == unit then
rawset(t, "guid", character.guid);
rawset(t, "name", character.name);
rawset(t, "lvl", character.lvl);
if character.classID then
rawset(t, "classID", character.classID);
rawset(t, "classes", { character.classID });
local classInfo = C_CreatureInfo.GetClassInfo(character.classID);
if classInfo then
rawset(t, "className", classInfo.className);
rawset(t, "classFile", classInfo.classFile);
end
end
if character.raceID then
rawset(t, "raceID", character.raceID);
rawset(t, "races", { character.raceID });
rawset(t, "race", C_CreatureInfo.GetRaceInfo(character.raceID).raceName);
end
return t;
end
end
local name, guid, className, classFile, classID, raceName, raceFile, raceID;
if #{strsplit("-", unit)} > 1 then
-- It's a GUID.
guid = unit;
className, classFile, raceName, raceFile, raceID, name = GetPlayerInfoByGUID(guid);
classID = GetClassIDFromClassFile(classFile);
else
name = UnitName(unit);
if name then
guid = UnitGUID(unit);
className, classFile, classID = UnitClass(unit);
raceName, raceFile, raceID = UnitRace(unit);
else
rawset(t, "name", unit);
return t;
end
end
if name then
rawset(t, "name", name);
rawset(t, "guid", guid);
if classID then
rawset(t, "className", className);
rawset(t, "classFile", classFile);
rawset(t, "classID", classID);
end
if raceID then
rawset(t, "raceID", raceID);
rawset(t, "races", { raceID });
rawset(t, "race", C_CreatureInfo.GetRaceInfo(raceID).raceName);
end
end
return t;
end,
["name"] = function(t)
return rawget(t.info, "name");
end,
["icon"] = function(t)
local classID = rawget(t.info, "classID");
if classID then return classIcons[classID]; end
end,
["guid"] = function(t)
local guid = rawget(t.info, "guid");
if guid then return guid; end
end,
["title"] = function(t)
if IsInGroup() then
if rawget(t, "isML") then return MASTER_LOOTER; end
if UnitIsGroupLeader(t.unit, "raid") then return RAID_LEADER; end
end
end,
["lvl"] = function(t)
return UnitLevel(t.unit);
end,
["race"] = function(t)
return rawget(t.info, "race");
end,
["className"] = function(t)
return rawget(t.info, "className");
end,
["classFile"] = function(t)
return rawget(t.info, "classFile");
end,
["classText"] = function(t)
local classFile = t.classFile;
if classFile then return "|c" .. RAID_CLASS_COLORS[classFile].colorStr .. t.name .. "|r"; end
return t.name;
end,
["tooltipText"] = function(t)
local text = t.text;
local icon = t.icon;
if icon then text = "|T" .. icon .. ":0|t " .. text; end
return text;
end,
});
app.CreateQuestUnit = app.ExtendClass("Unit", "QuestUnit", "unit", {
["visible"] = app.ReturnTrue,
["collectible"] = app.ReturnTrue,
["trackable"] = app.ReturnTrue,
["collected"] = function(t)
return t.saved;
end,
["OnClick"] = function(t)
return app.NoFilter;
end,
["OnUpdate"] = function(t)
return app.AlwaysShowUpdateWithoutReturn;
end,
["saved"] = function(t)
local questID = GetRelativeValue(t, "questID");
if questID then
local guid = t.guid;
if guid and questID then
if guid == app.GUID then
return IsQuestFlaggedCompleted(questID);
else
local questsForGUID = GetDataMember("GroupQuestsByGUID")[guid] or (ATTCharacterData[guid] and ATTCharacterData[guid].Quests);
return questsForGUID and questsForGUID[questID];
end
end
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 then
t.link = link;
return link;
end
end
end,
["tsm"] = function(t)
if t.itemID then return sformat("i:%d", t.itemID); end
return sformat("p:%d:1:3", t.speciesID);
end,
};
local mountFields = {
["text"] = function(t)
return "|cffb19cd9" .. t.name .. "|r";
end,
["icon"] = function(t)
return select(3, GetSpellInfo(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 GetSpellInfo(t.spellID) or RETRIEVING_DATA;
end,
["tsmForItem"] = function(t)
if t.itemID then return sformat("i:%d", t.itemID); end
if t.parent and t.parent.itemID then return sformat("i:%d", t.parent.itemID); end
end,
["linkForItem"] = function(t)
return select(2, GetItemInfo(t.itemID)) or GetSpellLink(t.spellID);
end,
};
if C_PetJournal then
-- Once the Pet Journal API is available, then all pets become account wide.
SetBattlePetCollected = function(t, speciesID, collected)
return app.SetAccountCollected(t, "BattlePets", speciesID, collected);
end
speciesFields.icon = function(t)
return select(2, C_PetJournal.GetPetInfoBySpeciesID(t.speciesID));
end
speciesFields.name = function(t)
return C_PetJournal.GetPetInfoBySpeciesID(t.speciesID);
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 GetSpellInfo(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 GetSpellInfo(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 select(5, GetItemInfoInstant(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 GetSpellInfo(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)();
-- Currency Lib
(function()
local CurrencyInfo = {};
local GetCurrencyCount;
local GetCurrencyLink = GetCurrencyLink;
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
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.RecursiveDefaultClassAndRaceFilter(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 });
tinsert(app.EventHandlers.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,
});
if not TooltipDataProcessor then
local GameTooltip_SetCurrencyByID = GameTooltip.SetCurrencyByID;
GameTooltip.SetCurrencyByID = function(self, currencyID, count)
-- Make sure to call to base functionality
if GameTooltip_SetCurrencyByID then
GameTooltip_SetCurrencyByID(self, currencyID, count);
else
local results = SearchForField("currencyID", currencyID);
if #results > 0 then
GameTooltip:AddLine(results[1].text or RETRIEVING_DATA, 1, 1, 1);
end
end
if (not InCombatLockdown() or app.Settings:GetTooltipSetting("DisplayInCombat")) and app.Settings:GetTooltipSetting("Enabled") then
AttachTooltipSearchResults(self, 1, "currencyID:" .. currencyID, SearchForField, "currencyID", currencyID);
if app.Settings:GetTooltipSetting("currencyID") then self:AddDoubleLine(L["CURRENCY_ID"], tostring(currencyID)); end
self:Show();
end
end
local GameTooltip_SetCurrencyToken = GameTooltip.SetCurrencyToken;
GameTooltip.SetCurrencyToken = function(self, tokenID)
-- Make sure to call to base functionality
if GameTooltip_SetCurrencyToken then GameTooltip_SetCurrencyToken(self, tokenID); end
if (not InCombatLockdown() or app.Settings:GetTooltipSetting("DisplayInCombat")) and app.Settings:GetTooltipSetting("Enabled") then
-- Determine what kind of list data this is. (Blizzard is whack and using this API call for headers too...)
local name, isHeader = GetCurrencyListInfo(tokenID);
if not isHeader then
-- Determine which currencyID is the one that we're dealing with.
for currencyID,results in pairs(SearchForFieldContainer("currencyID")) do
-- Compare the name of the currency vs the name of the token
if #results > 0 and results[1].text == name then
if not GameTooltip_SetCurrencyToken then
GameTooltip:AddLine(results[1].text or RETRIEVING_DATA, 1, 1, 1);
end
AttachTooltipSearchResults(self, 1, "currencyID:" .. currencyID, SearchForField, "currencyID", currencyID);
if app.Settings:GetTooltipSetting("currencyID") then self:AddDoubleLine(L["CURRENCY_ID"], tostring(currencyID)); end
self:Show();
break;
end
end
end
end
end
end
end)();
-- Death Tracker Lib
(function()
local fields = {
["text"] = function(t)
return "Total Deaths";
end,
["icon"] = function(t)
return app.asset("Category_Deaths");
end,
["hash"] = function(t)
return "deathtracker";
end,
["progress"] = function(t)
return math.min(t.total, app.Settings.AccountWide.Deaths and ATTAccountWideData.Deaths or app.CurrentCharacter.Deaths);
end,
["OnTooltip"] = function(t)
local c = {};
for _,character in pairs(ATTCharacterData) do
if character and character.Deaths and character.Deaths > 0 then
tinsert(c, character);
end
end
if #c > 0 then
GameTooltip:AddLine(" ");
GameTooltip:AddLine("Deaths Per Character:");
app.Sort(c, function(a, b)
return a.Deaths > b.Deaths;
end);
for i,character in ipairs(c) do
GameTooltip:AddDoubleLine(" " .. string.gsub(character.text, "-" .. GetRealmName(), ""), character.Deaths, 1, 1, 1);
end
end
end,
};
if C_GameRules and C_GameRules.IsHardcoreActive() then
fields.description = function(t)
return "The ATT Gods must be sated. Go forth and attempt to level, mortal!\n\n 'Live! Die! Try Again!'\n";
end;
fields.total = function(t) return 1; end
else
fields.description = function(t)
return "The ATT Gods must be sated. Go forth and attempt to level, mortal!\n\n 'Live! Die! Live Again!'\n";
end;
fields.total = function(t) return 1000; end
end
if GetStatistic and GetStatistic(60) then
-- Statistics are available, this means we can get the actual statistic from the server's database.
local OnUpdateForDeathTrackerLib = function(t)
if app.Settings:Get("Thing:Deaths") then
local stat = GetStatistic(60) or "0";
if stat == "--" then stat = "0"; end
local deaths = tonumber(stat);
if deaths > 0 and deaths > app.CurrentCharacter.Deaths then
ATTAccountWideData.Deaths = ATTAccountWideData.Deaths + (deaths - app.CurrentCharacter.Deaths);
app.CurrentCharacter.Deaths = deaths;
end
t.parent.progress = t.parent.progress + t.progress;
t.parent.total = t.parent.total + t.total;
t.visible = app.GroupVisibilityFilter(t);
else
t.visible = false;
end
return true;
end
fields.OnUpdate = function(t)
return OnUpdateForDeathTrackerLib;
end
app.events.PLAYER_DEAD = function()
app:PlayDeathSound();
end
else
-- Oh boy, we have to track it ourselves!
local OnUpdateForDeathTrackerLib = function(t)
if app.Settings:Get("Thing:Deaths") then
t.parent.progress = t.parent.progress + t.progress;
t.parent.total = t.parent.total + t.total;
t.visible = app.GroupVisibilityFilter(t);
else
t.visible = false;
end
return true;
end
fields.OnUpdate = function(t)
return OnUpdateForDeathTrackerLib;
end
app.events.PLAYER_DEAD = function()
ATTAccountWideData.Deaths = ATTAccountWideData.Deaths + 1;
app.CurrentCharacter.Deaths = app.CurrentCharacter.Deaths + 1;
app:PlayDeathSound();
app:RefreshDataQuietly("PLAYER_DEAD");
end
end
app:RegisterEvent("PLAYER_DEAD");
app.CreateDeathClass = app.CreateClass("DeathsTracker", "deaths", fields);
end)();
-- Difficulty Lib
(function()
local difficulties = {
[1] = { 9, 148, 173 },
[2] = { 174 },
[3] = { 175 },
[4] = { 176 },
[9] = { 1 },
[148] = { 1 },
[173] = { 1 },
[174] = { 2 },
[175] = { 3 },
[176] = { 4 },
};
app.DifficultyColors = {
[2] = "ff0070dd",
[5] = "ff0070dd",
[6] = "ff0070dd",
[7] = "ff9d9d9d",
[15] = "ff0070dd",
[16] = "ffa335ee",
[17] = "ff9d9d9d",
[23] = "ffa335ee",
[24] = "ffe6cc80",
[33] = "ffe6cc80",
};
app.DifficultyIcons = {
[1] = app.asset("Difficulty_Normal"),
[2] = app.asset("Difficulty_Heroic"),
[3] = app.asset("Difficulty_Normal"),
[4] = app.asset("Difficulty_Normal"),
[5] = app.asset("Difficulty_Heroic"),
[6] = app.asset("Difficulty_Heroic"),
[7] = app.asset("Difficulty_LFR"),
[9] = app.asset("Difficulty_Mythic"),
[11] = app.asset("Difficulty_Normal"),
[12] = app.asset("Difficulty_Heroic"),
[14] = app.asset("Difficulty_Normal"),
[15] = app.asset("Difficulty_Heroic"),
[16] = app.asset("Difficulty_Mythic"),
[17] = app.asset("Difficulty_LFR"),
[18] = app.asset("Category_Event"),
[23] = app.asset("Difficulty_Mythic"),
[24] = app.asset("Difficulty_Timewalking"),
[33] = app.asset("Difficulty_Timewalking"),
};
app.CreateDifficulty = app.CreateClass("Difficulty", "difficultyID", {
["text"] = function(t)
return t.sourceParent and sformat("%s [%s]", t.name, t.sourceParent.text or UNKNOWN) or t.name;
end,
["name"] = function(t)
return GetDifficultyInfo(t.difficultyID) or "Unknown Difficulty";
end,
["icon"] = function(t)
return app.DifficultyIcons[t.difficultyID];
end,
["saved"] = function(t)
return t.locks;
end,
["locks"] = function(t)
local locks = t.parent and t.parent.locks;
if locks then
if t.parent.isLockoutShared and not (t.difficultyID == 7 or t.difficultyID == 17) then
rawset(t, "locks", locks.shared);
return locks.shared;
else
-- Look for this difficulty's lockout.
for difficultyKey, lock in pairs(locks) do
if difficultyKey == "shared" then
-- ignore this one
elseif difficultyKey == t.difficultyID then
rawset(t, "locks", lock);
return lock;
end
end
end
end
end,
["difficulties"] = function(t)
return difficulties[t.difficultyID];
end,
["u"] = function(t)
if t.difficultyID == 24 or t.difficultyID == 33 then
return 42;
end
end,
["description"] = function(t)
if t.difficultyID == 24 or t.difficultyID == 33 then
return L["WE_JUST_HATE_TIMEWALKING"];
end
end,
["hash"] = function(t)
if t.parent then return t.key .. t[t.key] .. "~" .. t.parent.key .. t.parent[t.parent.key]; end
end,
});
end)();
-- Encounter Lib
(function()
if EJ_GetEncounterInfo then
app.CreateEncounter = app.CreateClass("Encounter", "encounterID", {
["name"] = function(t)
return EJ_GetEncounterInfo(t.encounterID);
end,
["lore"] = function(t)
return select(2, EJ_GetEncounterInfo(t.encounterID));
end,
["displayID"] = function(t)
return select(4, EJ_GetCreatureInfo(1, t.encounterID));
end,
["displayInfo"] = function(t)
local displayInfos, id, displayInfo = {}, t.encounterID;
for i=1,MAX_CREATURES_PER_ENCOUNTER do
displayInfo = select(4, EJ_GetCreatureInfo(i, id));
if displayInfo then
tinsert(displayInfos, displayInfo);
else
break;
end
end
rawset(t, "displayInfo", displayInfos);
return displayInfos;
end,
["icon"] = function(t)
return app.DifficultyIcons[GetRelativeValue(t, "difficultyID") or 1];
end,
},
"WithQuest", {
trackable = app.ReturnTrue,
saved = function(t)
return IsQuestFlaggedCompletedForObject(t) == 1;
end
}, (function(t) return t.questID; end));
else
app.CreateEncounter = function(id, t)
local npcID = t.creatureID or (t.crs and t.crs[1]) or t.npcID or (t.qgs and t.qgs[1]);
if npcID then
t = app.CreateNPC(npcID, t);
t.encounterID = id;
return t;
else
t = constructor(id, t, "encounterID");
t.text = "INVALID ENCOUNTER " .. id;
print(t.text);
return t;
end
end
end
end)();
-- Faction Lib
(function()
local setFactionCollected = function(t, factionID, collected)
return app.SetCollectedForSubType(t, "Factions", "Reputations", factionID, collected);
end
local StandingByID = {
{ -- 1: HATED
["color"] = GetProgressColor(0),
["threshold"] = -42000,
},
{ -- 2: HOSTILE
["color"] = "00FF0000",
["threshold"] = -6000,
},
{ -- 3: UNFRIENDLY
["color"] = "00EE6622",
["threshold"] = -3000,
},
{ -- 4: NEUTRAL
["color"] = "00FFFF00",
["threshold"] = 0,
},
{ -- 5: FRIENDLY
["color"] = "0000FF00",
["threshold"] = 3000,
},
{ -- 6: HONORED
["color"] = "0000FF88",
["threshold"] = 9000,
},
{ -- 7: REVERED
["color"] = "0000FFCC",
["threshold"] = 21000,
},
{ -- 8: EXALTED
["color"] = GetProgressColor(1),
["threshold"] = 42000,
},
};
app.FactionNameByID = setmetatable({}, { __index = function(t, id)
local name = GetFactionInfoByID(id);
if name then
rawset(t, id, name);
rawset(app.FactionIDByName, name, id);
return name;
end
end });
app.FactionIDByName = setmetatable({}, { __index = function(t, name)
for i=1,3000,1 do
if app.FactionNameByID[i] == name then
return i;
end
end
end });
app.ColorizeStandingText = function(standingID, text)
local standing = StandingByID[standingID];
if standing then
return Colorize(text, standing.color);
else
local rgb = FACTION_BAR_COLORS[standingID];
return Colorize(text, RGBToHex(rgb.r * 255, rgb.g * 255, rgb.b * 255));
end
end
app.GetFactionIDByName = function(name)
name = strtrim(name);
return app.FactionIDByName[name] or name;
end
app.GetFactionStanding = function(reputation)
-- Total earned rep from GetFactionInfoByID is a value AWAY FROM ZERO, not a value within the standing bracket.
if reputation then
for i=#StandingByID,1,-1 do
local threshold = StandingByID[i].threshold;
if reputation >= threshold then
return i, threshold < 0 and (threshold - reputation) or (reputation - threshold);
end
end
end
return 1, 0
end
app.GetFactionStandingText = function(standingID)
return app.ColorizeStandingText(standingID, _G["FACTION_STANDING_LABEL" .. standingID] or UNKNOWN);
end
app.GetFactionStandingThresholdFromString = function(replevel)
replevel = strtrim(replevel);
for standing=1,8,1 do
if _G["FACTION_STANDING_LABEL" .. standing] == replevel then
return StandingByID[standing].threshold;
end
end
end
app.IsFactionExclusive = function(factionID)
return factionID == 934 or factionID == 932 or factionID == 1104 or factionID == 1105;
end
local fields = {
["text"] = function(t)
return app.ColorizeStandingText(t.standing, t.name);
end,
["name"] = function(t)
return app.FactionNameByID[t.factionID] or (t.creatureID and app.NPCNameFromID[t.creatureID]) or (FACTION .. " #" .. t.factionID);
end,
["icon"] = function(t)
return app.asset("Category_Factions");
end,
["trackable"] = app.ReturnTrue,
["collectible"] = function(t)
if app.Settings.Collectibles.Reputations then
-- If your reputation is higher than the maximum for a different faction, return partial completion.
if not app.Settings.AccountWide.Reputations and t.maxReputation and t.maxReputation[1] ~= t.factionID and (select(3, GetFactionInfoByID(t.maxReputation[1])) or 4) >= app.GetFactionStanding(t.maxReputation[2]) then
return false;
end
return true;
end
return false;
end,
["saved"] = function(t)
local factionID = t.factionID;
local minReputation = t.minReputation;
if minReputation and minReputation[1] == factionID then
return setFactionCollected(t, factionID, (select(6, GetFactionInfoByID(factionID)) or 0) >= minReputation[2]);
else
return setFactionCollected(t, factionID, t.standing >= t.maxstanding);
end
end,
["title"] = function(t)
local reputation = t.reputation;
local amount, ceiling = select(2, app.GetFactionStanding(reputation)), t.ceiling;
local title = _G["FACTION_STANDING_LABEL" .. t.standing];
if ceiling then
title = title .. DESCRIPTION_SEPARATOR .. amount .. " / " .. ceiling;
if reputation < 42000 then
return title .. " (" .. (42000 - reputation) .. " to " .. _G["FACTION_STANDING_LABEL8"] .. ")";
end
end
return title;
end,
["reputation"] = function(t)
return select(6, GetFactionInfoByID(t.factionID));
end,
["ceiling"] = function(t)
local _, _, _, m, ma = GetFactionInfoByID(t.factionID);
return ma and m and (ma - m);
end,
["standing"] = function(t)
return select(3, GetFactionInfoByID(t.factionID)) or 1;
end,
["maxstanding"] = function(t)
if t.minReputation and t.minReputation[1] == t.factionID then
return app.GetFactionStanding(t.minReputation[2]);
end
return 8;
end,
["description"] = function(t)
return select(2, GetFactionInfoByID(t.factionID)) or "Not all reputations can be viewed on a single character. IE: Warsong Outriders cannot be viewed by an Alliance Player and Silverwing Sentinels cannot be viewed by a Horde Player.";
end,
};
fields.collected = fields.saved;
app.CreateFaction = app.CreateClass("Faction", "factionID", fields);
app.OnUpdateReputationRequired = function(t)
if app.Settings:Get("DebugMode") or app.Settings:Get("AccountMode") then
t.visible = true;
return false;
else
local reputationID = t.minReputation[1];
t.visible = (select(3, GetFactionInfoByID(reputationID)) or 1) >= 4;
return true;
end
end
end)();
-- Flight Path Lib
(function()
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.CacheFlightPathData = function()
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
local type, zero, server_id, instance_id, zone_uid, npcID, spawn_uid = strsplit("-",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);
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)();
-- Garrison Lib
(function()
if C_Garrison then
local C_Garrison_GetBuildingInfo = C_Garrison.GetBuildingInfo;
local GarrisonBuildingInfoMeta = { __index = function(t, key)
local _, name, _, icon, lore, _, _, _, _, _, uncollected = C_Garrison_GetBuildingInfo(t.garrisonBuildingID);
if not name then return nil; end
t.name = name;
t.icon = icon;
t.lore = lore;
setmetatable(t, nil);
return t[key];
end };
local GarrisonBuildingInfoByID = setmetatable({}, { __index = function(t, id)
local info = setmetatable({ garrisonBuildingID = id }, GarrisonBuildingInfoMeta);
t[id] = info;
return info;
end });
app.CreateGarrisonBuilding = app.CreateClass("GarrisonBuilding", "garrisonBuildingID", {
info = function(t)
return GarrisonBuildingInfoByID[t.garrisonBuildingID];
end,
name = function(t)
return t.info.name;
end,
icon = function(t)
return t.info.icon;
end,
lore = function(t)
return t.info.lore;
end,
},
"WithItem", {
icon = function(t)
return select(5, GetItemInfoInstant(t.itemID)) or t.info.icon;
end,
link = function(t)
return select(2, GetItemInfo(t.itemID)) or RETRIEVING_DATA;
end,
name = function(t)
return GetItemInfo(t.itemID) or t.info.name;
end,
tsm = function(t)
return sformat("i:%d", t.itemID);
end,
f = function(t)
return app.FilterConstants.RECIPES;
end,
collectible = function(t)
return app.Settings.Collectibles.Recipes;
end,
collected = function(t)
local id = t.garrisonBuildingID;
if app.CurrentCharacter.GarrisonBuildings[id] then return 1; end
return app.SetCollected(t, "GarrisonBuildings", id, not select(11, C_Garrison_GetBuildingInfo(id)));
end,
}, (function(t) return t.itemID; end));
local C_Garrison_GetMissionName = C_Garrison.GetMissionName;
app.CreateGarrisonMission = app.CreateClass("GarrisonMission", "garrisonMissionID", {
name = function(t)
return C_Garrison_GetMissionName(t.missionID);
end,
icon = function(t)
return "Interface/ICONS/INV_Icon_Mission_Complete_Order";
end,
});
local C_Garrison_GetTalentInfo = C_Garrison.GetTalentInfo;
local GarrisonTalentInfoMeta = { __index = function(t, key)
local info = C_Garrison_GetTalentInfo(t.garrisonTalentID);
if not info then return nil; end
t.name = info.name;
t.icon = info.icon or "Interface/ICONS/INV_Icon_Mission_Complete_Order";
t.description = info.description;
setmetatable(t, nil);
return t[key];
end };
local GarrisonTalentInfoByID = setmetatable({}, { __index = function(t, id)
local info = setmetatable({ garrisonTalentID = id }, GarrisonTalentInfoMeta);
t[id] = info;
return info;
end });
app.CreateGarrisonTalent = app.CreateClass("GarrisonTalent", "garrisonTalentID", {
info = function(t)
return GarrisonTalentInfoByID[t.garrisonTalentID];
end,
name = function(t)
return t.info.name;
end,
icon = function(t)
return t.info.icon;
end,
description = function(t)
return t.info.description;
end,
trackable = app.ReturnTrue,
saved = function(t)
return C_Garrison_GetTalentInfo(t.garrisonTalentID).researched;
end,
});
else
app.CreateGarrisonBuilding = function(id, t)
return { text = "GarrisonBuilding #" .. id, description = "This data type is not supported at this time." };
end
app.CreateGarrisonMission = function(id, t)
return { text = "GarrisonMission #" .. id, description = "This data type is not supported at this time." };
end
app.CreateGarrisonTalent = function(id, t)
return { text = "GarrisonTalent #" .. id, description = "This data type is not supported at this time." };
end
end
end)();
-- Item Lib
(function()
local BestSuffixPerItemID = setmetatable({}, { __index = function(t, id)
local suffixes = GetDataSubMember("ValidSuffixesPerItemID", id);
if suffixes then
for suffixID,_ in pairs(suffixes) do
rawset(t, id, suffixID);
return suffixID;
end
else
-- No valid suffixes
rawset(t, id, 0);
return 0;
end
end });
local TotalRetriesPerItemID = setmetatable({}, { __index = function(t, id)
return 0;
end });
local BestItemLinkPerItemID = setmetatable({}, { __index = function(t, id)
local suffixID = BestSuffixPerItemID[id];
local link = select(2, GetItemInfo(suffixID > 0 and sformat("item:%d:0:0:0:0:0:%d", id, suffixID) or id));
if link then
rawset(t, id, link);
return link;
end
end });
local BlacklistedRWPItems = {
[22736] = true, -- Andonisus, Reaper of Souls
};
local baseGetItemCount = function(t)
return GetItemCount(t.itemID, true);
end;
app.ParseItemID = function(itemName)
if type(itemName) == "number" then
return itemName;
else
local itemID = tonumber(itemName);
if string.match(tostring(itemID), itemName) then
-- This was actually an item ID.
return itemID;
else
-- The itemID given was actually the name or a link.
itemID = GetItemInfoInstant(itemName);
if itemID then
-- Oh good, it was cached by WoW.
return itemID;
else
-- Oh no, gonna need to work for it.
for id,_ in pairs(SearchForFieldContainer("itemID")) do
local text = BestItemLinkPerItemID[id];
if text and string.match(text, itemName) then
return id;
end
end
end
end
end
end
app.ClearItemCache = function()
wipe(BestSuffixPerItemID);
wipe(BestItemLinkPerItemID);
end
local collectibleAsCostForItem = function(t)
local id = t.itemID;
local results = SearchForField("itemIDAsCost", id);
if #results > 0 then
local costTotal = 0;
if not t.parent or not t.parent.saved then
for _,ref in pairs(results) do
if ref.itemID ~= id and app.RecursiveGroupRequirementsFilter(ref) then
if ref.key == "instanceID" or ((ref.key == "difficultyID" or ref.key == "mapID" or ref.key == "headerID") and (ref.parent and GetRelativeValue(ref.parent, "instanceID"))) then
if costTotal < 1 then -- This is for Keys
costTotal = costTotal + 1;
end
elseif (ref.collectible and not ref.collected) or (ref.total and ref.total > ref.progress) then
if ref.cost then
for k,v in ipairs(ref.cost) do
if v[2] == id and v[1] == "i" then
costTotal = costTotal + (v[3] or 1);
end
end
end
if ref.providers then
for k,v in ipairs(ref.providers) do
if v[2] == id and v[1] == "i" then
if ref.objectiveID then
costTotal = costTotal + (t.objectiveCost or 0);
else
costTotal = costTotal + 1;
end
end
end
end
end
end
end
end
t.costTotal = costTotal;
return costTotal > 0;
elseif t.simplemeta then
-- If no references to the item were used as a cost evaluation, then simplify the meta.
setmetatable(t, t.simplemeta);
return false;
end
end;
local collectedAsCostForItem = function(t)
if t.costTotal and t.costTotal > 0 then
return t.GetItemCount(t) >= t.costTotal;
end
end;
local collectibleAsQuest = function(t)
if app.Settings.Collectibles.Quests then
if (not t.repeatable and not t.isBreadcrumb) or C_QuestLog_IsOnQuest(t.questID) then
return true;
end
end
end
local isCollectibleRWP = function(t)
return t.f and (t.rwp or (t.u and (t.u == 2 or t.u == 3 or t.u == 4))) and not BlacklistedRWPItems[t.itemID] and app.Settings:GetFilterForRWPBase(t.f);
end
local collectedAsRWP = function(t)
if app.Settings.Collectibles.RWP then
-- If it's a BOE we can collect it on this character.
local id, b = t.itemID, t.b;
if not b or b == 2 or b == 3 then
-- This item is BOE. You CAN collect this on this character! (but not from a quest)
return app.SetCollected(t, "RWP", id, GetItemCount(id, true) > 0);
elseif app.Settings:GetFilterForRWP(t.f) or (t.filterForRWP and app.Settings:GetFilterForRWP(t.filterForRWP)) then
-- This character matches requirements
if GetItemCount(id, true) > 0 then
-- You kept this item. Nice!
return app.SetCollected(t, "RWP", id, true);
else
-- Check to see if this item was a quest reward.
local searchResults = SearchForField("itemID", id);
if #searchResults > 0 then
for i,o in ipairs(searchResults) do
if ((o.key == "questID" and o.saved) or (o.parent and o.parent.key == "questID" and o.parent.saved)) and app.RecursiveDefaultClassAndRaceFilter(o) then
return app.SetCollected(t, "RWP", id, true);
end
end
return app.SetCollected(t, "RWP", id, false);
end
end
else
-- This character does NOT match requirements and the item is BOP. You can't collect these on this character. :(
return app.SetCollected(t, "RWP", id, false);
end
end
end;
local itemFields = {
["text"] = function(t)
return t.link;
end,
["icon"] = function(t)
return select(5, GetItemInfoInstant(t.itemID)) or "Interface\\Icons\\INV_Misc_QuestionMark";
end,
["link"] = function(t)
return BestItemLinkPerItemID[t.itemID];
end,
["name"] = function(t)
local link = t.link;
return link and GetItemInfo(link);
end,
["b"] = function(t)
return 2;
end,
["f"] = function(t)
if t.questID then return app.FilterConstants.QUEST_ITEMS; end
if #SearchForField("itemIDAsCost", t.itemID) > 0 then
return app.FilterConstants.QUEST_ITEMS;
end
end,
["tsm"] = function(t)
return sformat("i:%d", t.itemID);
end,
["GetItemCount"] = function(t)
return baseGetItemCount;
end,
["collectible"] = function(t)
return t.collectibleAsCost;
end,
["collected"] = function(t)
return t.collectedAsCost;
end,
["collectibleAsCost"] = collectibleAsCostForItem, -- These two references can get replaced/removed
["collectedAsCost"] = collectedAsCostForItem,
};
app.CreateItem = app.CreateClass("Item", "itemID", itemFields,
"AsRWP", {
collectible = function(t)
return t.collectibleAsCost or app.Settings.Collectibles.RWP;
end,
collected = function(t)
if t.collectedAsCost == false then
return;
end
return collectedAsRWP(t);
end,
}, isCollectibleRWP,
"WithFactionAndQuest", {
collectible = function(t)
return t.collectibleAsCost or collectibleAsQuest(t) or app.Settings.Collectibles.Reputations;
end,
collected = function(t)
if t.collectedAsCost == false then
return 0;
end
if not IsQuestFlaggedCompletedForObject(t) then
return 0;
end
-- This is used by reputation tokens. (turn in items)
if app.CurrentCharacter.Factions[t.factionID] then return 1; end
if app.Settings.AccountWide.Reputations and ATTAccountWideData.Factions[t.factionID] then return 2; end
end,
trackable = app.ReturnTrue,
saved = function(t)
return IsQuestFlaggedCompletedForObject(t) == 1;
end
}, (function(t) return t.factionID and t.questID; end),
"WithQuest", {
collectible = function(t)
return t.collectibleAsCost or collectibleAsQuest(t);
end,
collected = function(t)
return IsQuestFlaggedCompletedForObject(t) or t.collectedAsCost;
end,
trackable = app.ReturnTrue,
saved = function(t)
return IsQuestFlaggedCompletedForObject(t) == 1;
end
}, (function(t) return t.questID; end),
"WithFaction", {
collectible = function(t)
return t.collectibleAsCost or app.Settings.Collectibles.Reputations;
end,
collected = function(t)
if t.collectedAsCost == false then
return 0;
end
-- This is used by reputation tokens. (turn in items)
if app.CurrentCharacter.Factions[t.factionID] then return 1; end
if app.Settings.AccountWide.Reputations and ATTAccountWideData.Factions[t.factionID] then return 2; end
end,
}, (function(t) return t.factionID; end));
-- Heirloom Lib
if C_Heirloom then
-- Heirloom API is available. Awesome!
local C_Heirloom_GetHeirloomInfo = C_Heirloom.GetHeirloomInfo;
local C_Heirloom_GetHeirloomLink = C_Heirloom.GetHeirloomLink;
local C_Heirloom_PlayerHasHeirloom = C_Heirloom.PlayerHasHeirloom;
local C_Heirloom_GetHeirloomMaxUpgradeLevel = C_Heirloom.GetHeirloomMaxUpgradeLevel;
local heirloomIDs = {};
local CreateHeirloomUnlock = app.CreateClass("HeirloomUnlock", "heirloomUnlockID", {
name = function(t)
return L["HEIRLOOM_TEXT"];
end,
icon = function(t)
return app.asset("Weapon_Type_Heirloom");
end,
description = function(t)
return L["HEIRLOOM_TEXT_DESC"];
end,
collectible = function(t)
return app.Settings.Collectibles.Heirlooms;
end,
collected = function(t)
return C_Heirloom_PlayerHasHeirloom(t.heirloomUnlockID);
end,
});
-- Clone base item fields and extend the properties.
local heirloomFields = {
icon = function(t)
return select(4, C_Heirloom_GetHeirloomInfo(t.itemID)) or select(5, GetItemInfoInstant(t.itemID));
end,
link = function(t)
return C_Heirloom_GetHeirloomLink(t.itemID) or select(2, GetItemInfo(t.itemID));
end,
};
-- Are heirloom upgrades available? (6.1.0.19445)
local gameBuildVersion = app.GameBuildVersion;
if gameBuildVersion > 60100 then
-- Extend the heirloom lib to account for upgrade levels.
local armorTextures = {
"Interface/ICONS/INV_Icon_HeirloomToken_Armor01",
"Interface/ICONS/INV_Icon_HeirloomToken_Armor02",
"Interface/ICONS/Inv_leather_draenordungeon_c_01shoulder",
"Interface/ICONS/inv_mail_draenorquest90_b_01shoulder",
"Interface/ICONS/inv_leather_warfrontsalliance_c_01_shoulder",
"Interface/ICONS/inv_shoulder_armor_dragonspawn_c_02",
};
local weaponTextures = {
"Interface/ICONS/INV_Icon_HeirloomToken_Weapon01",
"Interface/ICONS/INV_Icon_HeirloomToken_Weapon02",
"Interface/ICONS/inv_weapon_shortblade_112",
"Interface/ICONS/inv_weapon_shortblade_111",
"Interface/ICONS/inv_weapon_shortblade_102",
"Interface/ICONS/inv_weapon_shortblade_84",
};
local weaponFilterIDs = { 20, 29, 28, 21, 22, 23, 24, 25, 26, 50, 57, 34, 35, 27, 33, 32, 31 };
local hierloomLevelFields = {
["key"] = function(t)
return "heirloomLevelID";
end,
["level"] = function(t)
return 1;
end,
["name"] = function(t)
t.name = sformat(HEIRLOOM_UPGRADE_TOOLTIP_FORMAT, t.level, t.levelMax);
return t.name;
end,
["icon"] = function(t)
return t.isWeapon and weaponTextures[t.level] or armorTextures[t.level];
end,
["description"] = function(t)
return L["HEIRLOOMS_UPGRADES_DESC"];
end,
["collectible"] = function(t)
return app.Settings.Collectibles.Heirlooms and app.Settings.Collectibles.HeirloomUpgrades;
end,
["collected"] = function(t)
local itemID = t.heirloomLevelID;
if itemID then
if t.level <= (ATTAccountWideData.HeirloomRanks[itemID] or 0) then return true; end
local level = select(5, C_Heirloom_GetHeirloomInfo(itemID));
if level then
ATTAccountWideData.HeirloomRanks[itemID] = level;
if t.level <= level then return true; end
end
end
end,
["trackable"] = app.ReturnTrue,
["isWeapon"] = function(t)
local isWeapon = t.f and contains(weaponFilterIDs, t.f);
t.isWeapon = isWeapon;
return isWeapon;
end,
};
local CreateHeirloomLevel = app.CreateClass("HeirloomLevel", "heirloomLevelID", hierloomLevelFields);
heirloomFields.isWeapon = hierloomLevelFields.isWeapon;
heirloomFields.saved = function(t)
return t.collected == 1;
end
-- Will retrieve all the cached entries by itemID for existing heirlooms and generate their
-- upgrade levels into the respective upgrade tokens
app.CacheHeirlooms = function()
-- app.PrintDebug("CacheHeirlooms",#heirloomIDs)
if #heirloomIDs < 1 then return; end
-- Setup upgrade tokens that contain levels for the heirlooms. Order matters.
-- Ranks 1 & 2 were added with WOD (6.1.0.19445)
local armorTokenItemIDs = {
122338, -- Rank 1: Ancient Heirloom Armor Casing
122340, -- Rank 2: Timeworn Heirloom Armor Casing
};
local weaponTokenItemIDs = {
122339, -- Rank 1: Ancient Heirloom Scabbard
122341, -- Rank 2: Timeworn Heirloom Scabbard
};
-- Rank 3 was added with Legion (7.2.5.24076)
if gameBuildVersion > 70205 then
tinsert(armorTokenItemIDs, 151614); -- Weathered Heirloom Armor Casing
tinsert(weaponTokenItemIDs, 151615); -- Weathered Heirloom Scabbard
-- Rank 4 was added with BFA (8.1.5.29701)
if gameBuildVersion > 80105 then
tinsert(armorTokenItemIDs, 167731); -- Battle-Hardened Heirloom Armor Casing
tinsert(weaponTokenItemIDs, 167732); -- Battle-Hardened Heirloom Scabbard
-- Rank 5 was added with Shadowlands (9.1.5.40871)
if gameBuildVersion > 90105 then
tinsert(armorTokenItemIDs, 187997); -- Eternal Heirloom Armor Casing
tinsert(weaponTokenItemIDs, 187998); -- Eternal Heirloom Scabbard
-- Rank 6 was added with Dragonflight (10.1.0.49407)
if gameBuildVersion > 100100 then
tinsert(armorTokenItemIDs, 204336); -- Awakened Heirloom Armor Casing
tinsert(weaponTokenItemIDs, 204337); -- Awakened Heirloom Scabbard
end
end
end
end
-- Build headers that will contain each type.
local armorTokens, weaponTokens = {}, {};
for i=#armorTokenItemIDs,1,-1 do
tinsert(armorTokens, app.CreateItem(armorTokenItemIDs[i], {
collectible = false,
g = {},
}));
tinsert(weaponTokens, app.CreateItem(weaponTokenItemIDs[i], {
collectible = false,
g = {},
}));
end
-- for each cached heirloom, push a copy of itself with respective upgrade level under the respective upgrade token
local Search = app.SearchForObject;
local uniques, heirloom, upgrades = {};
for _,itemID in ipairs(heirloomIDs) do
if not uniques[itemID] then
uniques[itemID] = true;
heirloom = Search("itemID", itemID, "field");
if heirloom then
upgrades = C_Heirloom_GetHeirloomMaxUpgradeLevel(itemID);
if upgrades and upgrades > 0 then
local meta = { __index = heirloom };
local tokenType = heirloom.isWeapon and weaponTokens or armorTokens;
for i=1,upgrades,1 do
-- Create a non-collectible version of the heirloom item itself to hold the upgrade within the token
tinsert(tokenType[upgrades + 1 - i].g,
setmetatable({ collectible = false, g = {
CreateHeirloomLevel({
heirloomLevelID = itemID,
levelMax = upgrades,
level = i,
f = heirloom.f,
e = heirloom.e,
u = heirloom.u,
})
}}, meta));
end
end
end
end
end
-- build groups for each upgrade token
-- and copy the set of upgrades into the cached versions of the upgrade tokens so they therefore exist in the main list
-- where the sources of the upgrade tokens exist
for i,item in ipairs(armorTokens) do
for _,token in ipairs(SearchForField("itemID", item.itemID)) do
-- ensure the tokens do not have a modID attached
token.modID = nil;
token.modItemID = nil;
if not token.sym then
for _,heirloom in ipairs(item.g) do
NestObject(token, heirloom, true);
end
BuildGroups(token);
end
end
end
for i,item in ipairs(weaponTokens) do
for _,token in ipairs(SearchForField("itemID", item.itemID)) do
-- ensure the tokens do not have a modID attached
token.modID = nil;
token.modItemID = nil;
if not token.sym then
for _,heirloom in ipairs(item.g) do
NestObject(token, heirloom, true);
end
BuildGroups(token);
end
end
end
wipe(heirloomIDs);
end
end
local CreateHeirloom = app.ExtendClass("Item", "Heirloom", "itemID", heirloomFields,
"AsRWP", {
collectible = function(t)
return t.collectibleAsCost or app.Settings.Collectibles.RWP;
end,
collected = function(t)
if t.collectedAsCost == false then
return;
end
return collectedAsRWP(t);
end,
description = function()
return "This item also has an RWP sourceID with it, keep at least one somewhere on your account. I'm not sure if Blizzard is planning on deprecating this completely before transmog comes out or not!\n\n - Crieve";
end,
}, isCollectibleRWP,
"WithFaction", {
collectible = function(t)
return t.collectibleAsCost or app.Settings.Collectibles.Reputations;
end,
collected = function(t)
if t.collectedAsCost == false then
return 0;
end
if t.repeatable then
return (app.CurrentCharacter.Factions[t.factionID] and 1)
or (ATTAccountWideData.Factions[t.factionID] and 2);
else
-- This is used for the Grand Commendations unlocking Bonus Reputation
if ATTAccountWideData.FactionBonus[t.factionID] then return 1; end
if select(15, GetFactionInfoByID(t.factionID)) then
ATTAccountWideData.FactionBonus[t.factionID] = 1;
return 1;
end
end
-- This is used by reputation tokens. (turn in items)
if app.CurrentCharacter.Factions[t.factionID] then return 1; end
if app.Settings.AccountWide.Reputations and ATTAccountWideData.Factions[t.factionID] then return 2; end
end,
}, (function(t) return t.factionID; end));
app.CreateHeirloom = function(id, t)
t = CreateHeirloom(id, t);
--t.b = 2; -- Heirlooms are always BoA
-- unlocking the heirloom is the only thing contained in the heirloom
t.g = { CreateHeirloomUnlock(id, { e = t.e, u = t.u }); }
tinsert(heirloomIDs, id);
return t;
end
else
-- No difference between an item and an heirloom in classic, yet.
app.CreateHeirloom = function(id, t)
return app.CreateItem(id, t);
end
end
local fields = CloneDictionary(itemFields);
fields.collectible = function(t)
return app.Settings.Collectibles.Toys;
end
fields.collected = function(t)
return app.SetCollected(t, "Toys", t.toyID, GetItemCount(t.toyID, true) > 0);
end
fields.itemID = function(t)
return t.toyID;
end
if C_ToyBox then
-- Toy API is in!
local C_ToyBox_GetToyInfo = C_ToyBox.GetToyInfo;
local function isBNETCollectible(toyID)
if C_ToyBox_GetToyInfo(toyID) then
return true;
end
end
fields.collected = function(t)
local toyID = t.toyID;
if isBNETCollectible(toyID) then
if ATTAccountWideData.Toys[toyID] then return 1; end
return app.SetAccountCollected(t, "Toys", toyID, PlayerHasToy(toyID));
else
return app.SetCollected(t, "Toys", toyID, GetItemCount(toyID, true) > 0);
end
end;
fields.description = function(t)
if not isBNETCollectible(t.toyID) then
return "This is not a Toy as classified by Blizzard, but it is something that SHOULD be a Toy! Keep this in your inventory somewhere on an alt until Blizzard fixes it.";
end
end;
fields.isBNETCollectible = function(t)
return isBNETCollectible(t.toyID);
end
app.events.TOYS_UPDATED = function(toyID, new)
if toyID then
app.SetAccountCollected(app.SearchForField("toyID", toyID)[1] or app.CreateToy(toyID), "Toys", toyID, PlayerHasToy(toyID));
app:RefreshDataQuietly("TOYS_UPDATED", true);
end
end
tinsert(app.EventHandlers.OnReady, function()
app:RegisterEvent("TOYS_UPDATED");
end);
end
app.CreateToy = app.CreateClass("Toy", "toyID", fields);
local HarvestedItemDatabase = {};
local itemHarvesterFields = CloneDictionary(itemFields);
itemHarvesterFields.collectible = function(t)
return true;
end
itemHarvesterFields.collected = app.ReturnFalse;
itemHarvesterFields.text = function(t)
local link = t.link;
if link then
local itemName, itemLink, itemQuality, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount,
itemEquipLoc, itemTexture, sellPrice, classID, subclassID, bindType, expacID, setID, isCraftingReagent
= GetItemInfo(link);
if itemName then
local spellName, spellID;
if classID == "Recipe" or classID == "Mount" then
spellName, spellID = GetItemSpell(t.itemID);
if spellName == "Learning" then spellID = nil; end -- RIP.
end
--setmetatable(t, t.conditions[2]);
local info = {
["name"] = itemName,
["itemID"] = t.itemID,
["equippable"] = itemEquipLoc and itemEquipLoc ~= "" and true or false,
["class"] = classID,
["subclass"] = subclassID,
["inventoryType"] = C_Item.GetItemInventoryTypeByID(t.itemID),
["b"] = bindType,
["q"] = itemQuality,
["iLvl"] = itemLevel,
["spellID"] = spellID,
};
if itemMinLevel and itemMinLevel > 1 then
info.lvl = itemMinLevel;
end
if info.inventoryType == 0 then
info.inventoryType = nil;
end
if info.b and info.b ~= 1 then
info.b = nil;
end
if info.q and info.q < 1 then
info.q = nil;
end
if info.iLvl and info.iLvl < 2 then
info.iLvl = nil;
end
t.itemType = itemType;
t.itemSubType = itemSubType;
t.info = info;
t.retries = nil;
HarvestedItemDatabase[t.itemID] = info;
AllTheThingsAD.HarvestedItemDatabase = HarvestedItemDatabase;
return link;
end
end
t.retries = (t.retries or 0) + 1;
if t.retries > 30 then
rawset(t, "collected", true);
end
end
local ItemHarvester = CreateFrame("GameTooltip", "ATTCItemHarvester", UIParent, "GameTooltipTemplate");
app.CreateItemHarvester = app.CreateClass("ItemHarvester", "itemID", itemHarvesterFields,
"TooltipHarvester", {
text = function(t)
local link = t.link;
if link then
ItemHarvester:SetOwner(UIParent,"ANCHOR_NONE")
ItemHarvester:SetHyperlink(link);
local lineCount = ItemHarvester:NumLines();
if ATTCItemHarvesterTextLeft1:GetText() and ATTCItemHarvesterTextLeft1:GetText() ~= RETRIEVING_DATA and lineCount > 0 then
local requirements = {};
for index=2,lineCount,1 do
local line = _G["ATTCItemHarvesterTextLeft" .. index] or _G["ATTCItemHarvesterText" .. index];
if line then
local text = line:GetText();
if text then
if string.find(text, "Classes: ") then
local classes = {};
local _,list = strsplit(":", text);
for i,s in ipairs({strsplit(",", list)}) do
tinsert(classes, app.ClassDB[strtrim(s)]);
end
if #classes > 0 then
t.info.classes = classes;
end
elseif string.find(text, "Races: ") then
local races = {};
local _,list = strsplit(":", text);
for i,s in ipairs({strsplit(",", list)}) do
tinsert(races, app.RaceDB[strtrim(s)]);
end
if #races > 0 then
t.info.races = races;
end
elseif string.find(text, "Requires") and not string.find(text, "Level") and not string.find(text, "Riding") then
local c = strsub(text, 1, 1);
if c ~= " " and c ~= "\t" and c ~= "\n" and c ~= "\r" then
text = strsub(strtrim(text), 9);
if string.find(text, "-") then
local faction,replevel = strsplit("-", text);
t.info.minReputation = { app.GetFactionIDByName(faction), app.GetFactionStandingThresholdFromString(replevel) };
else
if string.find(text, "%(") then
if t.info.requireSkill then
-- If non-specialization skill is already assigned, skip this part.
text = nil;
else
text = strsplit("(", text);
end
end
if text then
local spellName = strtrim(text);
if spellName == "Herbalism" then spellName = "Herb Gathering"; end
local spellID = app.SpellNameToSpellID[spellName];
if spellID then
local skillID = app.SpellIDToSkillID[spellID];
if skillID then
t.info.requireSkill = skillID;
else
print("Unknown Skill '" .. spellName .. "'");
tinsert(requirements, spellName);
end
else
print("Unknown Spell '" .. spellName .. "'");
tinsert(requirements, spellName);
end
end
end
end
end
end
end
end
if #requirements > 0 then
t.info.otherRequirements = requirements;
end
rawset(t, "text", link);
rawset(t, "collected", true);
end
ItemHarvester:Hide();
return link;
end
end
}, app.ReturnFalse);
end)();
-- Map Lib
(function()
local C_Map_GetMapArtID = C_Map.GetMapArtID;
local C_Map_GetMapLevels = C_Map.GetMapLevels;
local C_Map_GetBestMapForUnit = C_Map.GetBestMapForUnit;
local C_MapExplorationInfo_GetExploredMapTextures = C_MapExplorationInfo.GetExploredMapTextures;
local C_MapExplorationInfo_GetExploredAreaIDsAtPosition = C_MapExplorationInfo.GetExploredAreaIDsAtPosition;
local mapIDToMapName, mapIDToAreaID = {}, {
[427] = { 132, 800 }, -- Coldridge Valley, Coldridge Pass
[465] = { 154 }, -- Deathknell
[425] = { 9, 59, 24, 34 }, -- Northshire Valley, Northshire Vineyards, Northshire Abbey, Echo Ridge Mine
[460] = { 188 }, -- Shadowglen
[462] = { 221 }, -- Camp Narache
[467] = { 3431 }, -- Sunstrider Isle
[468] = { 3526, 3527, 3560, 3528, 3559, 3529, 3530, 3561 }, -- Ammen Vale, Crash Site, Ammen Fields, Silverline Lake, Nestlewood Hills, Nestlewood Thicket, Shadow Ridge, The Sacred Grove
[348] = { 4095 }, -- Magisters' Terrace
};
for mapID,area in pairs(mapIDToAreaID) do
local info = C_Map_GetAreaInfo(area[1]);
if info then
mapIDToMapName[mapID] = info;
L.ZONE_TEXT_TO_MAP_ID[info] = mapID;
if #area > 1 then
for i=2,#area,1 do
local info = C_Map_GetAreaInfo(area[i]);
if info then L.ALT_ZONE_TEXT_TO_MAP_ID[info] = mapID; end
end
end
end
end
app.GetCurrentMapID = function()
local originalMapID = C_Map_GetBestMapForUnit("player");
local substitutions = L.QUEST_ID_TO_MAP_ID[originalMapID];
if substitutions then
for questID,mapID in pairs(substitutions) do
if not C_QuestLog_IsQuestFlaggedCompleted(questID) then
return mapID;
end
end
end
local zoneTextSubstitution = L.MAP_ID_TO_ZONE_TEXT[originalMapID];
if zoneTextSubstitution then
local zone = GetRealZoneText();
if zone then
if zoneTextSubstitution == zone then return originalMapID; end
local mapID = L.ZONE_TEXT_TO_MAP_ID[zone] or L.ALT_ZONE_TEXT_TO_MAP_ID[zone];
if mapID then
return mapID;
end
end
zone = GetSubZoneText();
if zone and zone ~= "" then
if zoneTextSubstitution == zone then return originalMapID; end
local mapID = L.ZONE_TEXT_TO_MAP_ID[zone] or L.ALT_ZONE_TEXT_TO_MAP_ID[zone];
if mapID then return mapID; end
end
else
local zone = GetRealZoneText();
if zone then
local mapID = L.ZONE_TEXT_TO_MAP_ID[zone] or L.ALT_ZONE_TEXT_TO_MAP_ID[zone];
if mapID then
return mapID;
end
end
zone = GetSubZoneText();
if zone and zone ~= "" then
local mapID = L.ZONE_TEXT_TO_MAP_ID[zone] or L.ALT_ZONE_TEXT_TO_MAP_ID[zone];
if mapID then return mapID; end
end
end
return originalMapID;
end
app.GetMapName = function(mapID)
if mapID then
local zoneTextSubstitution = L.MAP_ID_TO_ZONE_TEXT[mapID];
if zoneTextSubstitution then return zoneTextSubstitution; end
if mapIDToMapName[mapID] then return mapIDToMapName[mapID]; end
local info = C_Map_GetMapInfo(mapID);
return (info and info.name) or ("Map ID #" .. mapID);
else
return "Map ID #???";
end
end
app.CreateExploration = app.CreateClass("Exploration", "explorationID", {
["text"] = function(t)
return C_Map_GetAreaInfo(t.explorationID) or RETRIEVING_DATA;
end,
["title"] = function(t)
return t.maphash;
end,
["description"] = function(t)
if not TomTom then
return "You can use Alt+Right Click to plot the coordinates with TomTom installed. If this refuses to be marked collected for you in ATT, try reloading your UI or relogging.";
else
return "You can use Alt+Right Click to plot the coordinates. If this refuses to be marked collected for you in ATT, try reloading your UI or relogging.";
end
end,
["preview"] = function(t)
local exploredMapTextureInfo = t.exploredMapTextureInfo;
if exploredMapTextureInfo then
local texture = exploredMapTextureInfo.fileDataIDs[1];
if texture then
rawset(t, "preview", texture);
return texture;
end
end
end,
["artID"] = function(t)
return t.parent and (t.parent.artID or (t.parent.parent and t.parent.parent.artID));
end,
["icon"] = function(t)
return app.asset("Category_Exploration");
end,
["mapID"] = function(t)
return t.parent and (t.parent.mapID or (t.parent.parent and t.parent.parent.mapID));
end,
["collectible"] = function(t)
return app.Settings.Collectibles.Exploration;
end,
["collected"] = function(t)
if app.CurrentCharacter.Exploration[t.explorationID] then return 1; end
if app.Settings.AccountWide.Exploration and ATTAccountWideData.Exploration[t.explorationID] then return 2; end
local maphash = t.maphash;
if maphash then
local exploredMapTextures = C_MapExplorationInfo_GetExploredMapTextures(t.mapID)
if exploredMapTextures then
for i,info in ipairs(exploredMapTextures) do
local hash = info.textureWidth..":"..info.textureHeight..":"..info.offsetX..":"..info.offsetY;
if hash == maphash then return app.SetCollected(nil, "Exploration", t.explorationID, true); end
end
--[[
if not app.MAPHASHTHING then app.MAPHASHTHING = {}; end
if not app.MAPHASHTHING[t.explorationID] then
app.MAPHASHTHING[t.explorationID] = true;
print("Failed to detect maphash '" .. maphash .. "' on map " .. t.mapID .. ".");
end
]]--
end
end
--[[
if not app.MAPTHING then app.MAPTHING = {}; end
local exploredMapTextures = C_MapExplorationInfo_GetExploredMapTextures(t.mapID);
if not app.MAPTHING[t.mapID] and exploredMapTextures then
app.MAPTHING[t.mapID] = true;
local hashes = {};
for i,o in ipairs(t.parent.g) do
if o.maphash then hashes[o.maphash] = o; end
end
for i,info in ipairs(exploredMapTextures) do
local hash = info.textureWidth..":"..info.textureHeight..":"..info.offsetX..":"..info.offsetY;
if hash and not hashes[hash] then
print("Failed to find areaID for maphash '" .. hash .. "' on map " .. t.mapID .. ".");
end
end
end
local coords = t.coords;
if coords and #coords > 0 then
local c = coords[1];
local explored = C_MapExplorationInfo_GetExploredAreaIDsAtPosition(c[2], CreateVector2D(c[1] / 100, c[2] / 100, c[2]));
if explored then
for _,areaID in ipairs(explored) do
if areaID == t.explorationID then
app.SetCollected(nil, "Exploration", areaID, true);
return 1;
end
end
end
end
]]--
end,
["coords"] = function(t)
local coords = app.ExplorationAreaPositionDB[t.explorationID];
if not coords then
local maphash = t.maphash;
if maphash then
local coords = {};
local width, height, offsetX, offsetY = strsplit(":", maphash);
tinsert(coords, {((offsetX + (width * 0.5)) * 100) / WorldMapFrame:GetWidth(), ((offsetY + (height * 0.5)) * 100) / WorldMapFrame:GetHeight(), t.mapID});
return coords;
end
end
return coords;
end,
["exploredMapTextureInfo"] = function(t)
local maphash = t.maphash;
if maphash then
local exploredMapTextures = C_MapExplorationInfo_GetExploredMapTextures(t.mapID)
if exploredMapTextures then
for i,info in ipairs(exploredMapTextures) do
local hash = info.textureWidth..":"..info.textureHeight..":"..info.offsetX..":"..info.offsetY;
if hash == maphash then
rawset(t, "exploredMapTextureInfo", info);
return info;
end
end
end
end
end
});
local ExplorationGrid = {};
local levelOfDetail = -1;--200;
for i=0,levelOfDetail,1 do
for j=0,levelOfDetail,1 do
tinsert(ExplorationGrid, CreateVector2D(i / levelOfDetail, j / levelOfDetail));
end
end
local DiscoveredNewArea = {};
local simplifyExplorationData = function()
local i = 100;
while InCombatLockdown() do coroutine.yield(); end
while i > 0 do i = i - 1; coroutine.yield(); end
app.print("Simplifying Exploration Data...");
local allMapData = {};
local explorationDB = {};
local explorationAreaPositionDB = {};
for areaID,coords in pairs(app.ExplorationAreaPositionDB) do
for i,coord in ipairs(coords) do
local mapID = coord[3];
if mapID then
local x, y = math.floor(coord[1] * 100), math.floor(coord[2] * 100);
local hash = x .. ":" .. y;
local mapData = allMapData[mapID];
if not mapData then
mapData = {};
mapData.areas = {};
mapData.areaList = {};
mapData.hits = {};
allMapData[mapID] = mapData;
explorationDB[mapID] = mapData.areaList;
end
if not mapData.areas[areaID] then
mapData.areas[areaID] = 1;
tinsert(mapData.areaList, areaID);
end
if not mapData.hits[hash] then
mapData.hits[hash] = { areaID };
else
tinsert(mapData.hits[hash], areaID);
end
end
end
end
app.print("Determining best coordinates for areas...");
coroutine.yield();
local sortMethod = function(a, b)
return a[1] > b[1];
end;
for mapID,mapData in pairs(allMapData) do
app.print("Determining best coordinates for map ".. mapID);
local hitsByAreaID, hitsByCount = {}, {};
for i,areaID in ipairs(mapData.areaList) do
hitsByAreaID[areaID] = {};
end
for hash,hits in pairs(mapData.hits) do
tinsert(hitsByCount, { #hits, hash, hits});
end
app.Sort(hitsByCount, sortMethod);
coroutine.yield();
-- Now randomly grab hashes until every area has a few hashes
while #hitsByCount > 0 do
local index = math.random(#hitsByCount);
local hit = hitsByCount[index];
tremove(hitsByCount, index);
for i,areaID in ipairs(hit[3]) do
local hits = hitsByAreaID[areaID];
if #hits < 10 then tinsert(hits, hit[2]); end
end
end
-- Now that each has some hashes (probably), let's simplfy that data table.
for areaID,hits in pairs(hitsByAreaID) do
local positions = explorationAreaPositionDB[areaID];
if not positions then
positions = {};
explorationAreaPositionDB[areaID] = positions;
end
for i,hash in ipairs(hits) do
local x, y = hash:match("(%d+):(%d+)");
tinsert(positions, { tonumber(x) * 0.01, tonumber(y) * 0.01, mapID });
end
end
end
AllTheThingsAD.ExplorationDB = explorationDB;
AllTheThingsAD.ExplorationAreaPositionDB = explorationAreaPositionDB;
app.ExplorationDB = explorationDB;
app.ExplorationAreaPositionDB = explorationAreaPositionDB;
app.print("Done Simplifying Exploration Data.");
end
local onMapUpdate = function(t)
local explorationByAreaID = {};
local explorationHeader = nil;
local coordinates = {};
if t.g then
for i,o in ipairs(t.g) do
if o.key == "headerID" and o.headerID == app.HeaderConstants.EXPLORATION then
explorationHeader = o;
if o.g then
for j,e in ipairs(o.g) do
explorationByAreaID[e.explorationID] = e;
if e.coords and #e.coords > 0 then
tinsert(coordinates, e.coords[1]);
else
--print("Missing Coordinates for areaID", e.explorationID);
end
end
end
break;
end
end
end
local id = t.mapID;
local newExplorationObjects = {};
local areaIDs = app.ExplorationDB[id];
for _,pos in ipairs(coordinates) do
local explored = C_MapExplorationInfo_GetExploredAreaIDsAtPosition(pos[3] or id, CreateVector2D(pos[1] / 100, pos[2] / 100));
if explored then
for _,areaID in ipairs(explored) do
app.SetCollected(nil, "Exploration", areaID, true);
local o = explorationByAreaID[areaID];
if not o and not DiscoveredNewArea[areaID] and #SearchForField("explorationID", areaID) < 1 then
DiscoveredNewArea[areaID] = true;
o = app.CreateExploration(areaID);
explorationByAreaID[areaID] = o;
tinsert(newExplorationObjects, o);
print("Found New AreaID:", id, t.text, areaID, o.text);
tinsert(areaIDs, areaID);
end
end
end
end
for _,pos in ipairs(ExplorationGrid) do
local explored = C_MapExplorationInfo_GetExploredAreaIDsAtPosition(id, pos);
if explored then
for _,areaID in ipairs(explored) do
app.SetCollected(nil, "Exploration", areaID, true);
local o = explorationByAreaID[areaID];
if not o and not DiscoveredNewArea[areaID] then
DiscoveredNewArea[areaID] = true;
o = app.CreateExploration(areaID);
explorationByAreaID[areaID] = o;
tinsert(newExplorationObjects, o);
print("Found New AreaID:", id, t.text, areaID, o.text);
tinsert(areaIDs, areaID);
end
local coords = app.ExplorationAreaPositionDB[areaID];
if not coords then
coords = {};
app.ExplorationAreaPositionDB[areaID] = coords;
local missing = AllTheThingsAD.ExplorationAreaPositionDB;
if not missing then
missing = {};
--AllTheThingsAD.ExplorationAreaPositionDB = missing;
end
missing[areaID] = coords;
end
tinsert(coords, {pos.x * 100, pos.y * 100, id});
end
end
end
if #newExplorationObjects > 0 then
if explorationHeader then
if not explorationHeader.g then
explorationHeader.g = {};
end
for i,o in ipairs(newExplorationObjects) do
tinsert(explorationHeader.g, o);
o.parent = explorationHeader;
end
else
explorationHeader = app.CreateNPC(app.HeaderConstants.EXPLORATION, newExplorationObjects);
explorationHeader.u = t.u;
for i,o in ipairs(newExplorationObjects) do
o.parent = explorationHeader;
if not o.u then o.u = t.u; end
end
explorationHeader.parent = t;
tinsert(t.g, 1, explorationHeader);
end
end
if explorationHeader and explorationHeader.g then
app.Sort(explorationHeader.g, app.SortDefaults.Text);
end
rawset(t, "OnUpdate", nil);
--app:StartATTCoroutine("Simplifying Exploration Data", simplifyExplorationData);
end;
app.SortExplorationDB = function()
local e,t=ATTC.ExplorationDB,{};
for mapID,areas in pairs(e) do
local s = {};
t[mapID] = s;
for i,areaID in ipairs(areas) do
tinsert(s, { areaID, C_Map_GetAreaInfo(areaID) });
end
app.Sort(s, function(a, b)
return a[2] < b[2];
end);
end
AllTheThingsAD.NamedExplorationDB = t;
end
local createMap, mapClass = app.CreateClass("Map", "mapID", {
["text"] = function(t)
return t.isRaid and ("|c" .. app.Colors.Raid .. t.name .. "|r") or t.name;
end,
["name"] = function(t)
return t.headerID and app.NPCNameFromID[t.headerID] or app.GetMapName(t.mapID);
end,
["icon"] = function(t)
return t.headerID and L["HEADER_ICONS"][t.headerID] or app.asset("Category_Zones");
end,
["lore"] = function(t)
return t.headerID and L["HEADER_LORE"][t.headerID];
end,
["description"] = function(t)
return t.headerID and L["HEADER_DESCRIPTIONS"][t.headerID];
end,
["back"] = function(t)
if app.CurrentMapID == t.mapID or (t.maps and contains(t.maps, app.CurrentMapID)) then
return 1;
end
end,
["artID"] = function(t)
return C_Map_GetMapArtID(t.mapID);
end,
["lvl"] = function(t)
return C_Map_GetMapLevels(t.mapID);
end,
});
app.BaseMap = mapClass;
app.CreateMap = function(id, t)
local t = createMap(id, t);
local artID = t.artID;
if artID and t.g then
local explorationByAreaID = {};
local explorationHeader = nil;
for i,o in ipairs(t.g) do
if o.headerID == app.HeaderConstants.EXPLORATION then
explorationHeader = o;
if o.g then
for j,e in ipairs(o.g) do
local explorationID = e.explorationID;
if explorationID then
explorationByAreaID[explorationID] = e;
end
end
end
break;
end
end
local newExplorationObjects = {};
local areaIDs = app.ExplorationDB[id];
if not areaIDs then
areaIDs = {};
app.ExplorationDB[id] = areaIDs;
--AllTheThingsAD.ExplorationDB = ATTC.ExplorationDB;
else
for _,areaID in ipairs(areaIDs) do
if not explorationByAreaID[areaID] then
o = app.CreateExploration(areaID);
explorationByAreaID[areaID] = o;
tinsert(newExplorationObjects, o);
end
end
end
if #newExplorationObjects > 0 then
if explorationHeader then
if not explorationHeader.g then
explorationHeader.g = {};
end
for i,o in ipairs(newExplorationObjects) do
tinsert(explorationHeader.g, o);
o.parent = explorationHeader;
end
else
explorationHeader = app.CreateNPC(app.HeaderConstants.EXPLORATION, newExplorationObjects);
explorationHeader.u = t.u;
for i,o in ipairs(newExplorationObjects) do
o.parent = explorationHeader;
if not o.u then o.u = t.u; end
end
explorationHeader.parent = t;
tinsert(t.g, 1, explorationHeader);
end
end
if explorationHeader and explorationHeader.g then
app.Sort(explorationHeader.g, app.SortDefaults.Text);
end
if not rawget(t, "OnUpdate") then
t.OnUpdate = onMapUpdate;
end
end
if t.creatureID and t.creatureID < 0 then
t.headerID = t.creatureID;
t.creatureID = nil;
end
return t;
end
app.CreateInstance = app.CreateClass("Instance", "instanceID", {
["text"] = function(t)
return t.isRaid and ("|c" .. app.Colors.Raid .. t.name .. "|r") or t.name;
end,
["name"] = function(t)
return t.headerID and app.NPCNameFromID[t.headerID] or app.GetMapName(t.mapID);
end,
["icon"] = function(t)
return t.headerID and L["HEADER_ICONS"][t.headerID] or app.asset("Category_Zones");
end,
["back"] = function(t)
if app.CurrentMapID == t.mapID or (t.maps and contains(t.maps, app.CurrentMapID)) then
return 1;
end
end,
["mapID"] = function(t)
return t.maps and t.maps[1];
end,
["lvl"] = function(t)
return C_Map_GetMapLevels(t.mapID);
end,
["locks"] = function(t)
local lockouts = app.CurrentCharacter.Lockouts;
local locks = lockouts[t.name];
if locks then
t.locks = locks;
return locks;
end
local sins = t.sins;
if sins then
for i=1,#sins,1 do
locks = lockouts[sins[i]];
if locks then
t.locks = locks;
return locks;
end
end
end
local areaID = t["zone-text-areaID"];
if areaID then
local name = C_Map_GetAreaInfo(areaID);
if name then
locks = lockouts[name];
if locks then
t.locks = locks;
return locks;
end
end
end
end,
["saved"] = function(t)
return t.locks;
end,
},
"WithCreature", {}, (function(t)
if t.creatureID and t.creatureID < 0 then
t.headerID = t.creatureID;
t.creatureID = nil;
return true;
end
end));
app:RegisterEvent("MAP_EXPLORATION_UPDATED");
app:RegisterEvent("UI_INFO_MESSAGE");
app.events.MAP_EXPLORATION_UPDATED = function(...)
app.CurrentMapID = app.GetCurrentMapID();
app:StartATTCoroutine("RefreshExploration", function()
coroutine.yield();
local mapID = app.CurrentMapID;
while not mapID do
coroutine.yield();
mapID = app.CurrentMapID;
end
local pos = C_Map_GetPlayerMapPosition(mapID, "player");
if pos then
local px, py = pos:GetXY();
px, py = px * 100, py * 100;
local explored = C_MapExplorationInfo_GetExploredAreaIDsAtPosition(app.CurrentMapID, pos);
if explored then
local newArea = false;
for _,areaID in ipairs(explored) do
if not app.CurrentCharacter.Exploration[areaID] then
app.SetCollected(nil, "Exploration", areaID, true);
newArea = true;
if not app.ExplorationAreaPositionDB[areaID] then
local coord = {px, py, app.CurrentMapID};
print("New Coordinate: ", C_Map_GetAreaInfo(areaID), coord);
app.ExplorationAreaPositionDB[areaID] = { coord };
end
end
end
if newArea then app:RefreshDataQuietly("RefreshExploration", true); end
end
end
end);
end
app.events.UI_INFO_MESSAGE = function(messageID)
if messageID == 372 then
app.events.MAP_EXPLORATION_UPDATED();
end
end
local function UpdateMap()
app.CurrentMapID = app.GetCurrentMapID();
end
app:RegisterEvent("ZONE_CHANGED");
app:RegisterEvent("ZONE_CHANGED_INDOORS");
app:RegisterEvent("ZONE_CHANGED_NEW_AREA");
app.events.ZONE_CHANGED = UpdateMap;
app.events.ZONE_CHANGED_INDOORS = UpdateMap;
app.events.ZONE_CHANGED_NEW_AREA = UpdateMap;
UpdateMap();
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:RedrawWindows("NPCDisplayIDFromID");
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 C_TooltipInfo_GetHyperlink = C_TooltipInfo and C_TooltipInfo.GetHyperlink;
if C_TooltipInfo_GetHyperlink then
setmetatable(NPCNameFromID, { __index = function(t, id)
if id > 0 then
local tooltipData = C_TooltipInfo_GetHyperlink(format("unit:Creature-0-0-0-0-%d-0000000000",id));
if tooltipData then
local title = tooltipData.lines[1].leftText;
if title and #tooltipData.lines > 2 then
NPCTitlesFromID[id] = tooltipData.lines[2].leftText;
end
if title and title ~= RETRIEVING_DATA 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
ATTCNPCHarvester:SetOwner(UIParent,"ANCHOR_NONE")
ATTCNPCHarvester:SetHyperlink(format("unit:Creature-0-0-0-0-%d-0000000000",id))
local title = ATTCNPCHarvesterTextLeft1:GetText();
if title and ATTCNPCHarvester:NumLines() > 2 then
NPCTitlesFromID[id] = ATTCNPCHarvesterTextLeft2:GetText();
end
ATTCNPCHarvester:Hide();
if title and title ~= RETRIEVING_DATA 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 == app.HeaderConstants.VENDORS and "Interface\\Icons\\INV_Misc_Coin_01")
or app.DifficultyIcons[GetRelativeValue(t, "difficultyID") or 1];
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,
},
"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", BuildSourceTextForDynamicPath(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",
["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
t.result = cache[1];
return cache[1];
else
cache = CreateObject({[typ] = t.autoID});
t.result = cache;
return cache;
end
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,
},
"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)();
-- Object Lib (as in "World Object")
(function()
app.CreateObject = app.CreateClass("Object", "objectID", {
["text"] = function(t)
return t.isRaid and ("|c" .. app.Colors.Raid .. t.name .. "|r") or t.name;
end,
["name"] = function(t)
if t.providers then
for k,v in ipairs(t.providers) do
if v[2] > 0 then
if v[1] == "o" then
return app.ObjectNames[v[2]] or RETRIEVING_DATA;
elseif v[1] == "i" then
return GetItemInfo(v[2]) or RETRIEVING_DATA;
end
end
end
end
return app.ObjectNames[t.objectID] or ("Object ID #" .. t.objectID);
end,
["icon"] = function(t)
if t.providers then
for k,v in ipairs(t.providers) do
if v[2] > 0 then
if v[1] == "o" then
local icon = app.ObjectIcons[v[2]];
if icon then return icon; end
elseif v[1] == "i" then
return select(5, GetItemInfoInstant(v[2])) or "Interface\\Icons\\INV_Misc_Bag_10";
end
end
end
end
return app.ObjectIcons[t.objectID] or "Interface\\Icons\\INV_Misc_Bag_10";
end,
["model"] = function(t)
return app.ObjectModels[t.objectID];
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] = 158762, -- Arch
[261] = 5149, -- Beast Training
[164] = 2018, -- Blacksmithing
[185] = 2550, -- Cooking
[333] = 7411, -- Enchanting
[202] = 4036, -- Engineering
[356] = 7620, -- Fishing
[129] = 3273, -- First Aid
[182] = 2366, -- Herb Gathering
[773] = 45357, -- Inscription
[755] = 25229, -- Jewelcrafting
[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.OnUpdateForOmarionsHandbook = function(t)
t.visible = true;
t.collectible = nil;
if app.Settings:Get("DebugMode") or app.Settings:Get("AccountMode") or CompletedQuests[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 GetSpellInfo(t.spellID);
end,
["icon"] = function(t)
return select(3, GetSpellInfo(t.spellID));
end,
["spellID"] = function(t)
return app.SkillIDToSpellID[t.professionID];
end,
["requireSkill"] = function(t)
return t.professionID;
end,
["sym"] = function(t)
return {{"selectprofession", t.professionID}};
end
});
end)();
-- Quest Lib
(function()
local C_QuestLog_GetQuestInfo = C_QuestLog.GetTitleForQuestID or C_QuestLog.GetQuestInfo;
local C_QuestLog_GetQuestObjectives = C_QuestLog.GetQuestObjectives;
local GetQuestLogIndexByID = C_QuestLog.GetLogIndexForQuestID or GetQuestLogIndexByID;
local questRetries = {};
local QuestTitleFromID = setmetatable({}, { __index = function(t, id)
local title = C_QuestLog_GetQuestInfo(id);
if title and title ~= RETRIEVING_DATA then
rawset(questRetries, id, nil);
rawset(t, id, title);
return title;
else
local retries = rawget(questRetries, id);
if retries and retries > 120 then
title = "Quest #" .. id .. "*";
rawset(questRetries, id, nil);
rawset(t, id, title);
return title;
else
rawset(questRetries, id, (retries or 0) + 1);
end
end
end })
local IgnoreErrorQuests = {
[1476]=1, -- Hearts of the Pure (Horde Pre-req for the Undercity Succubus Binding quest)
[1474]=1, -- The Binding (Succubus) [Undercity]
[1508]=1, -- Blind Cazul (Horde Pre-req for the Orgrimmar Succubus Binding quest)
[1509]=1, -- News of Dogran (1/2) (Horde Pre-req for the Orgrimmar Succubus Binding quest)
[1510]=1, -- News of Dogran (2/2) (Horde Pre-req for the Orgrimmar Succubus Binding quest)
[1511]=1, -- Ken'zigla's Draught (Horde Pre-req for the Orgrimmar Succubus Binding quest)
[1515]=1, -- Dogran's Captivity (Horde Pre-req for the Orgrimmar Succubus Binding quest)
[1512]=1, -- Love's Gift (Horde Pre-req for the Orgrimmar Succubus Binding quest)
[1513]=1, -- The Binding (Succubus) [Orgrimmar]
[1738]=1, -- Heartswood (Alliance Pre-req for the Stormwind City Succubus Binding quest)
[1739]=1, -- The Binding (Succubus) [Stormwind City]
[1516]=1, -- Call of Earth (1/3 Durotar)
[1519]=1, -- Call of Earth (1/3 Mulgore)
[9449]=1, -- Call of Earth (1/3 Ammen Vale)
[555]=1, -- Soothing Turtle Bisque (A)
[7321]=1, -- Soothing Turtle Bisque (H)
[3630]=1, -- Gnome Engineering [A]
[3632]=1, -- Gnome Engineering [A]
[3634]=1, -- Gnome Engineering [H]
[3635]=1, -- Gnome Engineering [H]
[3637]=1, -- Gnome Engineering [H]
[3526]=1, -- Goblin Engineering [H]
[3629]=1, -- Goblin Engineering [A]
[3633]=1, -- Goblin Engineering [H]
[4181]=1, -- Goblin Engineering [A]
[5517]=1, -- Chromatic Mantle of the Dawn
[5521]=1, -- Chromatic Mantle of the Dawn
[5524]=1, -- Chromatic Mantle of the Dawn
[5504]=1, -- Mantles of the Dawn
[5507]=1, -- Mantles of the Dawn
[5513]=1, -- Mantles of the Dawn
[7170]=1, -- Earned Reverence (Alliance)
[7165]=1, -- Earned Reverence (Horde)
[7171]=1, -- Legendary Heroes (Alliance)
[7166]=1, -- Legendary Heroes (Horde)
[7168]=1, -- Rise and Be Recognized (Alliance)
[7163]=1, -- Rise and Be Recognized (Horde)
[7172]=1, -- The Eye of Command (Alliance)
[7167]=1, -- The Eye of Command (Horde)
[7164]=1, -- Honored Amongst the Clan
[7169]=1, -- Honored Amongst the Guard
[8870]=1, -- The Lunar Festival
[8871]=1, -- The Lunar Festival
[8872]=1, -- The Lunar Festival
[8873]=1, -- The Lunar Festival
[8874]=1, -- The Lunar Festival
[8875]=1, -- The Lunar Festival
[8700]=1, -- Band of Unending Life
[8692]=1, -- Cloak of Unending Life
[8708]=1, -- Mace of Unending Life
[8704]=1, -- Signet of the Unseen Path
[8696]=1, -- Cloak of the Unseen Path
[8712]=1, -- Scythe of the Unseen Path
[8699]=1, -- Band of Vaulted Secrets
[8691]=1, -- Drape of Vaulted Secrets
[8707]=1, -- Blade of Vaulted Secrets
[8703]=1, -- Ring of Eternal Justice
[8695]=1, -- Cape of Eternal Justice
[8711]=1, -- Blade of Eternal Justice
[8697]=1, -- Ring of Infinite Wisdom
[8689]=1, -- Shroud of Infinite Wisdom
[8705]=1, -- Gavel of Infinite Wisdom
[8701]=1, -- Band of Veiled Shadows
[8693]=1, -- Cloak of Veiled Shadows
[8709]=1, -- Dagger of Veiled Shadows
[8698]=1, -- Ring of the Gathering Storm
[8690]=1, -- Cloak of the Gathering Storm
[8706]=1, -- Hammer of the Gathering Storm
[8702]=1, -- Ring of Unspoken Names
[8694]=1, -- Shroud of Unspoken Names
[8710]=1, -- Kris of Unspoken Names
[8556]=1, -- Signet of Unyielding Strength
[8557]=1, -- Drape of Unyielding Strength
[8558]=1, -- Sickle of Unyielding Strength
[9520]=1, -- Diabolical Plans [Alliance]
[9535]=1, -- Diabolical Plans [Horde]
[9522]=1, -- Never Again! [Alliance]
[9536]=1, -- Never Again! [Horde]
[10371]=1, -- Yorus Barleybrew (Draenei)
[10621]=1, -- Illidari Bane-Shard (A)
[10623]=1, -- Illidari Bane-Shard (H)
[10759]=1, -- Find the Deserter (A)
[10761]=1, -- Find the Deserter (H)
[11185]=1, -- The Apothecary's Letter
[11186]=1, -- Signs of Treachery?
[11201]=1, -- The Grimtotem Plot
[11123]=1, -- Inspecting the Ruins [Alliance]
[11124]=1, -- Inspecting the Ruins [Horde]
[11150]=1, -- Raze Direhorn Post! [Alliance]
[11205]=1, -- Raze Direhorn Post! [Horde]
[11215]=1, -- Help Mudsprocket
};
setmetatable(CompletedQuests, {__newindex = function (t, key, value)
if value then
rawset(t, key, value);
rawset(DirtyQuests, key, true);
app.SetCollected(nil, "Quests", key, true);
if app.Settings:GetTooltipSetting("Report:CompletedQuests") then
local searchResults = SearchForField("questID", key);
if #searchResults > 0 then
local questID, nmr, nmc, text = key, false, false, "";
for i,searchResult in ipairs(searchResults) do
if searchResult.key == "questID" and not IgnoreErrorQuests[questID] and not GetRelativeField(searchResult, "headerID", app.HeaderConstants.TIER_ZERO_POINT_FIVE_SETS) then
if searchResult.nmr and not nmr then
nmr = true;
text = searchResult.text;
end
if searchResult.nmc and not nmc then
nmc = true;
text = searchResult.text;
end
end
end
if app.Settings:GetTooltipSetting("Report:UnsortedQuests") then
return true;
end
if nmc then key = key .. " [C]"; end
if nmr then key = key .. " [R]"; end
key = key .. " (" .. (text or RETRIEVING_DATA) .. ")";
else
local text = C_QuestLog_GetQuestInfo(key) or RETRIEVING_DATA;
key = key .. " [M] (" .. text .. ")";
end
print("Completed Quest #" .. key);
end
end
end});
app.GetQuestName = function(questID)
return QuestTitleFromID[questID];
end
local criteriaFuncs = {
["achID"] = function(achievementID)
return app.CurrentCharacter.Achievements[achievementID];
end,
["lvl"] = function(v)
return app.Level >= v;
end,
["questID"] = function(questID)
return IsQuestFlaggedCompleted(questID);
end,
["spellID"] = function(spellID)
return app.CurrentCharacter.Spells[spellID] or app.CurrentCharacter.ActiveSkills[spellID];
end,
["factionID"] = function(v)
-- v = factionID.standingRequiredToLock
local factionID = math.floor(v + 0.00001);
local lockStanding = math.floor((v - factionID) * 10 + 0.00001);
local standing = select(3, GetFactionInfoByID(factionID)) or 4;
--app.print("Check Faction", factionID, "Standing (", standing, ") is locked @ (", lockStanding, ")");
return standing >= lockStanding;
end,
};
local OnUpdateForLockCriteria = function(t)
local lockCriteria = t.lc;
if lockCriteria then
local criteriaRequired = lockCriteria[1];
local critKey, critFunc, nonQuestLock;
for i=2,#lockCriteria,2 do
critKey = lockCriteria[i];
critFunc = criteriaFuncs[critKey];
if critFunc then
if critFunc(lockCriteria[i + 1]) then
if not nonQuestLock and critKey ~= "questID" then
nonQuestLock = true;
end
criteriaRequired = criteriaRequired - 1;
if criteriaRequired <= 0 then
t.locked = true;
-- if this was locked due to something other than a Quest specifically, indicate it cannot be done in Party Sync
if nonQuestLock then
-- app.PrintDebug("Automatic DisablePartySync", app:Linkify(questID, app.Colors.ChatLink, "search:questID:" .. questID))
t.DisablePartySync = true;
end
break;
end
end
else
app.print("Unknown 'lockCriteria' key:", critKey, lockCriteria[i + 1]);
end
end
end
end
local createQuest = app.CreateClass("Quest", "questID", {
["text"] = function(t)
if t.repeatable then return "|cff0070DD" .. t.name .. "|r"; end
return t.name;
end,
["name"] = function(t)
return QuestTitleFromID[t.questID] or (t.npcID and app.NPCNameFromID[t.npcID]) or RETRIEVING_DATA;
end,
["icon"] = function(t)
if t.providers then
for k,v in ipairs(t.providers) do
if v[2] > 0 then
if v[1] == "o" then
local icon = app.ObjectIcons[v[2]];
if icon then return icon; end
elseif v[1] == "i" then
return select(5, GetItemInfoInstant(v[2])) or "Interface\\Icons\\INV_Misc_Book_09";
end
end
end
end
if t.isWorldQuest then
return app.asset("Interface_WorldQuest");
elseif t.repeatable then
return app.asset("Interface_Questd");
else
return app.asset("Interface_Quest");
end
end,
["model"] = function(t)
if t.providers then
for k,v in ipairs(t.providers) do
if v[2] > 0 then
if v[1] == "o" then
return app.ObjectModels[v[2]];
end
end
end
end
end,
["link"] = function(t)
if t.questID then return "[" .. t.name .. " (".. t.questID .. ")]"; end
end,
["collectible"] = function(t)
if app.Settings.Collectibles.Quests then
if C_QuestLog_IsOnQuest(t.questID) then
return true;
end
if t.locked then return app.Settings.AccountWide.Quests; end
return not t.repeatable and not t.isBreadcrumb;
end
end,
["collected"] = function(t)
if C_QuestLog_IsOnQuest(t.questID) then
return false;
end
return IsQuestFlaggedCompletedForObject(t);
end,
["trackable"] = app.ReturnTrue,
["saved"] = function(t)
return IsQuestFlaggedCompletedForObject(t) == 1;
end,
},
"SetupLockCriteria", nil, (function(t)
if t.lc then
if t.OnUpdate then
print("BRUH ON UPDATE WITH LOCK CRITERIA QUEST ID #", t.questID);
else
t.OnUpdate = OnUpdateForLockCriteria;
end
end
end),
"WithReputation", {
collectible = function(t)
if app.Settings.Collectibles.Quests then
if C_QuestLog_IsOnQuest(t.questID) then
return true;
end
if t.locked then return app.Settings.AccountWide.Quests; end
if t.maxReputation and app.Settings.Collectibles.Reputations then
return true;
end
return not t.repeatable and not t.isBreadcrumb;
end
end,
collected = function(t)
if C_QuestLog_IsOnQuest(t.questID) then
return false;
end
local flag = IsQuestFlaggedCompletedForObject(t);
if flag then
return flag;
end
if t.maxReputation then
if (select(6, GetFactionInfoByID(t.maxReputation[1])) or 0) >= t.maxReputation[2] then
return t.repeatable and 1 or 2;
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 ~= "questID" and searchResult.collected then
return 2;
end
end
end
end
end
end
}, (function(t) return t.maxReputation; end),
"AsBreadcrumb", {
collectible = function(t)
if app.Settings.Collectibles.Quests then
if C_QuestLog_IsOnQuest(t.questID) or IsQuestFlaggedCompletedForObject(t) then
return true;
end
local results = SearchForField("sourceQuestID", t.questID);
if #results > 0 then
for i,o in ipairs(results) do
if o.collectible and not o.collected then
return true;
end
end
end
end
return false;
end,
collected = function(t)
return IsQuestFlaggedCompletedForObject(t);
end,
text = function(t)
return "|cffcbc3e3" .. t.name .. "|r";
end
}, (function(t) return t.isBreadcrumb; end));
app.CreateQuest = createQuest;
app.CreateQuestWithFactionData = function(t)
local aqd, hqd = t.aqd, t.hqd;
local questData, otherQuestData;
if app.FactionID == HORDE_FACTION_ID then
questData = hqd;
otherQuestData = aqd;
else
questData = aqd;
otherQuestData = hqd;
end
-- Move over the quest data's groups.
if questData.g then
if not t.g then
t.g = questData.g;
else
for _,o in ipairs(questData.g) do
tinsert(t.g, 1, o);
end
end
questData.g = nil;
end
if otherQuestData.g then
for _,o in ipairs(otherQuestData.g) do
o.parent = otherQuestData;
end
end
-- Apply this quest's current data into the other faction's quest. (this is for tooltip caching and source quest resolution)
for key,value in pairs(t) do
if key ~= "g" then
otherQuestData[key] = value;
end
end
-- Apply the faction specific quest data to this object.
for key,value in pairs(questData) do t[key] = value; end
aqd.r = ALLIANCE_FACTION_ID;
hqd.r = HORDE_FACTION_ID;
t.otherQuestData = otherQuestData;
otherQuestData.nmr = 1;
return createQuest(t.questID, t);
end
app.CreateQuestObjective = app.CreateClass("Objective", "objectiveID", {
["text"] = function(t)
return t.name;
end,
["name"] = function(t)
local questID = t.questID;
if questID then
local objectives = C_QuestLog_GetQuestObjectives(questID);
if objectives then
local objective = objectives[t.objectiveID];
if objective then
return objective.text;
end
end
if t.providers then
for k,v in ipairs(t.providers) do
if v[2] > 0 then
if v[1] == "o" then
return app.ObjectNames[v[2]] or RETRIEVING_DATA;
elseif v[1] == "i" then
return GetItemInfo(v[2]) or RETRIEVING_DATA;
end
end
end
end
if t.spellID then return GetSpellInfo(t.spellID); end
return RETRIEVING_DATA;
end
return "INVALID: Must be relative to a Quest Object.";
end,
["icon"] = function(t)
if t.providers then
for k,v in ipairs(t.providers) do
if v[2] > 0 then
if v[1] == "o" then
local icon = app.ObjectIcons[v[2]];
if icon then return icon; end
elseif v[1] == "i" then
return select(5, GetItemInfoInstant(v[2])) or "Interface\\Worldmap\\Gear_64Grey";
end
end
end
end
if t.spellID then return select(3, GetSpellInfo(t.spellID)); end
return t.parent.icon or "Interface\\Worldmap\\Gear_64Grey";
end,
["model"] = function(t)
if t.providers then
for k,v in ipairs(t.providers) do
if v[2] > 0 then
if v[1] == "o" then
return app.ObjectModels[v[2]];
end
end
end
end
end,
["hash"] = function(t)
return "ob:" .. t.objectiveID .. ":" .. (t.questID or 0);
end,
["objectiveID"] = function(t)
return 1;
end,
["questID"] = function(t)
return t.parent.questID;
end,
["isDaily"] = function(t)
return t.parent.isDaily;
end,
["isWeekly"] = function(t)
return t.parent.isWeekly;
end,
["isMonthly"] = function(t)
return t.parent.isMonthly;
end,
["isYearly"] = function(t)
return t.parent.isYearly;
end,
["isWorldQuest"] = function(t)
return t.parent.isWorldQuest;
end,
["repeatable"] = function(t)
return t.parent.repeatable;
end,
["collectible"] = function(t)
if not t.questID then
return false;
end
return app.Settings.Collectibles.Quests and C_QuestLog_IsOnQuest(t.questID);
end,
["trackable"] = function(t)
if not t.questID then
return false;
end
return C_QuestLog_IsOnQuest(t.questID);
end,
["collected"] = function(t)
-- If the parent is collected, return immediately.
local collected = t.parent.collected;
if collected then return collected; end
-- Check to see if the objective was completed.
local questID = t.questID;
if questID then
-- If the player isn't on that quest, return.
local index = GetQuestLogIndexByID(questID);
if index == 0 then return 0; end
-- If the player completed the quest, return.
if select(6, GetQuestLogTitle(index)) then return 1; end
local objectives = C_QuestLog_GetQuestObjectives(questID);
if objectives then
local objective = objectives[t.objectiveID];
if objective then
return objective.finished and 1;
end
end
end
end,
["saved"] = function(t)
-- If the parent is saved, return immediately.
local saved = t.parent.saved;
if saved then return saved; end
-- Check to see if the objective was completed.
local questID = t.questID;
if questID then
local objectives = C_QuestLog_GetQuestObjectives(questID);
if objectives then
local objective = objectives[t.objectiveID];
if objective then
return objective.finished and 1;
end
end
end
end,
["objectiveCost"] = function(t)
-- This is only used to calculate how many things are required for an objective when its using a provider.
local questID = t.questID;
if questID then
-- If the player isn't on that quest, return.
local index = GetQuestLogIndexByID(questID);
if index == 0 then return 0; end
-- If the player completed the quest, return.
if select(6, GetQuestLogTitle(index)) then return 0; end
local objectives = C_QuestLog_GetQuestObjectives(questID);
if objectives then
local objective = objectives[t.objectiveID];
if objective then
if objective.finished then
return 0;
end
if questID == 14107 then
print(t.text, objective.numRequired);
end
return objective.numRequired or 1;
end
end
end
return 0;
end
});
app.CompareQuestieDB = function()
if QuestieLoader then
local QuestieDB,missingQuestIDs = QuestieLoader:ImportModule("QuestieDB"), {};
for id,_ in pairs(QuestieDB.QuestPointers) do
local s = SearchForField("questID", id);
if #s == 0 then
tinsert(missingQuestIDs, id);
end
end
app.Sort(missingQuestIDs, app.SortDefaults.Number);
for _,id in ipairs(missingQuestIDs) do
print("Missing Quest ", id);
end
else
print("Error: Questie not available. Please enable it!");
end
end
app.AddQuestObjectivesToTooltip = function(tooltip, reference)
local objectified = false;
local questLogIndex = GetQuestLogIndexByID(reference.questID);
if questLogIndex then
local lore, objective = GetQuestLogQuestText(questLogIndex);
if lore and app.Settings:GetTooltipSetting("Lore") then
tooltip:AddLine(Colorize(lore, app.Colors.TooltipLore), 1, 1, 1, 1);
end
if objective and app.Settings:GetTooltipSetting("Objectives") then
tooltip:AddLine(QUEST_OBJECTIVES, 1, 1, 1, 1);
tooltip:AddLine(objective, 0.4, 0.8, 1, 1);
objectified = true;
end
end
if not reference.saved and app.Settings:GetTooltipSetting("Objectives") then
local objectives = C_QuestLog_GetQuestObjectives(reference.questID);
if objectives and #objectives > 0 then
if not objectified then
tooltip:AddLine(QUEST_OBJECTIVES, 1, 1, 1, 1);
end
for i,objective in ipairs(objectives) do
local _ = objective.text;
if not _ or _:sub(1, 1) == " " then
_ = RETRIEVING_DATA;
end
tooltip:AddDoubleLine(" " .. _, GetCompletionIcon(objective.finished), 1, 1, 1, 1);
end
end
end
end
-- Game Events that trigger visual updates, but no computation updates.
local softRefresh = function()
wipe(searchCache);
end;
app.events.BAG_NEW_ITEMS_UPDATED = softRefresh;
app.events.CRITERIA_UPDATE = softRefresh;
app.events.QUEST_REMOVED = softRefresh;
app.events.QUEST_WATCH_UPDATE = softRefresh;
app.events.QUEST_ACCEPTED = function(questLogIndex, questID)
if not questID then questID = questLogIndex; end
if questID then rawset(QuestTitleFromID, questID, nil); end
softRefresh();
end
app.events.QUEST_LOG_UPDATE = function()
app:UnregisterEvent("QUEST_LOG_UPDATE");
if C_QuestLog_GetAllCompletedQuestIDs then
local completedQuests = C_QuestLog_GetAllCompletedQuestIDs();
if completedQuests and #completedQuests > 0 then
for i,questID in ipairs(completedQuests) do
CompletedQuests[questID] = true;
end
end
else
GetQuestsCompleted(CompletedQuests);
end
for questID,completed in pairs(DirtyQuests) do
app.QuestCompletionHelper(tonumber(questID));
end
wipe(DirtyQuests);
wipe(searchCache);
end
app.events.QUEST_TURNED_IN = function(questID)
local quest = SearchForField("questID", questID);
if #quest > 0 and (not quest[1].repeatable or (quest[1].isDaily or quest[1].isMonthly or quest[1].isYearly)) then
CompletedQuests[questID] = true;
for questID,completed in pairs(DirtyQuests) do
app.QuestCompletionHelper(tonumber(questID));
end
wipe(DirtyQuests);
end
app:RefreshDataQuietly("QUEST_TURNED_IN", true);
end
app:RegisterEvent("BAG_NEW_ITEMS_UPDATED");
app:RegisterEvent("CRITERIA_UPDATE");
app:RegisterEvent("QUEST_ACCEPTED");
app:RegisterEvent("QUEST_LOG_UPDATE");
app:RegisterEvent("QUEST_REMOVED");
app:RegisterEvent("QUEST_TURNED_IN");
app:RegisterEvent("QUEST_WATCH_UPDATE");
end)();
-- Recipe & Spell Lib
(function()
local craftColors = {
RGBToHex(64,192,64),
RGBToHex(255,255,0),
RGBToHex(255,128,64),
[0]=RGBToHex(128, 128, 128),
};
local CraftTypeIDToColor = function(craftTypeID)
return craftColors[craftTypeID];
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 = GetSpellInfo(spellID, rank);
else
spellName = GetSpellInfo(spellID);
end
if spellName and spellName ~= "" and spellName ~= RETRIEVING_DATA 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 = GetSpellInfo(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, "", 1, 1;
for spellTabIndex=1,numSpellTabs do
local numSpells = select(4, GetSpellTabInfo(spellTabIndex));
for spellIndex=1,numSpells do
local spellName, _, _, _, _, _, spellID = GetSpellInfo(offset, BOOKTYPE_SPELL);
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
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 select(3, GetSpellInfo(t.spellID)) or (t.requireSkill and select(3, GetSpellInfo(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.craftTypeID and Colorize(t.name, CraftTypeIDToColor(t.craftTypeID)) or t.link;
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,
["craftTypeID"] = function(t)
return app.CurrentCharacter.SpellRanks[t.spellID];
end,
["trackable"] = app.ReturnTrue,
["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 = 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;
local createRecipe = app.CreateClass("Recipe", "spellID", recipeFields,
"WithItem", {
baseIcon = function(t)
return select(5, GetItemInfoInstant(t.itemID)) or baseIconFromSpellID(t);
end,
link = function(t)
return select(2, GetItemInfo(t.itemID)) or RETRIEVING_DATA;
end,
name = function(t)
return GetItemInfo(t.itemID) or nameFromSpellID(t);
end,
tsm = function(t)
return sformat("i:%d", 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)();
-- Tier Lib
(function()
local math_floor = math.floor;
local baseTier = {
__index = function(t, key)
if key == "key" then
return "tierID";
else
local info = rawget(L.TIER_DATA, t.tierID);
return info and rawget(info, key);
end
end
};
app.CreateTier = function(id, t)
-- patch can be included in the id
local tierID = math_floor(id);
t = constructor(tierID, t, "tierID");
if id > tierID then
local patch_decimal = 100 * (id - tierID);
local patch = math_floor(patch_decimal + 0.0001);
local rev = math_floor(10 * (patch_decimal - patch) + 0.0001);
-- print("tier cache",id,tierID,patch_decimal,patch,rev)
t.text = tostring(tierID).."."..tostring(patch).."."..tostring(rev);
end
return setmetatable(t, baseTier);
end
end)();
-- Title Lib
(function()
local function StylizePlayerTitle(title, style, me)
if title then
if style == 0 then
-- Prefix
return title .. me;
elseif style == 1 then
-- Player Name First
return me .. title;
elseif style == 2 then
-- Player Name First (with space)
return me .. " " .. title;
elseif style == 3 then
-- Comma Separated
return me .. ", " .. title;
end
else
return title or "??";
end
end
local OnUpdateForSpecificGender = function(t)
if not (app.Settings:Get("AccountMode") or app.Settings:Get("DebugMode") or t.playerGender == UnitSex("player")) then
t.visible = false;
return true;
elseif t.parent.titleIDs then
UpdateGroup(t.parent, t);
t.visible = false;
return true;
end
end
local OnUpdateForGenderedTitle = function(t)
if not (app.Settings:Get("AccountMode") or app.Settings:Get("DebugMode")) then
t.progress = nil;
t.total = nil;
t.g = nil;
else
if not t.titleObjects then
local titleObjects = {};
for index,titleID in ipairs(t.titleIDs) do
local titleObject = app.CreateTitle(titleID, { ["playerGender"] = index == 1 and 2 or 3, ["OnUpdate"] = OnUpdateForSpecificGender });
titleObject.parent = t;
tinsert(titleObjects, titleObject);
end
t.titleObjects = titleObjects;
end
local g = {};
for index,titleObject in ipairs(t.titleObjects) do
if titleObject.titleID ~= t.titleID then tinsert(g, titleObject); end
end
if #g > 0 then t.g = g; end
end
end
app.CreateTitle = app.CreateClass("Title", "titleID", {
["icon"] = function(t)
return "Interface\\Icons\\INV_Misc_Horn_01";
end,
["description"] = function(t)
return L["TITLES_DESC"];
end,
["text"] = function(t)
return "|c" .. app.Colors.Account .. (t.name or RETRIEVING_DATA) .. "|r";
end,
["name"] = function(t)
return StylizePlayerTitle(t.titleName, t.style, UnitName("player"));
end,
["playerGender"] = function(t)
return UnitSex("player");
end,
["titleName"] = function(t)
return GetTitleName(t.titleID);
end,
["title"] = function(t)
return StylizePlayerTitle(t.titleName, t.style, "("..CALENDAR_PLAYER_NAME..")");
end,
["style"] = function(t)
local name = t.titleName;
if name then
local first = string.sub(name, 1, 1);
if first == " " then
-- Suffix
first = string.sub(name, 2, 2);
if first == string.upper(first) then
-- Comma Separated
return 3;
end
-- Player Name First
return 1;
else
local last = string.sub(name, -1);
if last == " " then
-- Prefix
return 0;
end
-- Suffix
if first == string.lower(first) then
-- Player Name First with a space
return 2;
end
-- Comma Separated
return 3;
end
end
return 1; -- Player Name First
end,
["collectible"] = function(t)
return app.Settings.Collectibles.Titles;
end,
["trackable"] = app.ReturnTrue,
["collected"] = function(t)
local titleID = t.titleID;
return app.SetCollected(t, "Titles", titleID, IsTitleKnown(titleID));
end,
["saved"] = function(t)
return IsTitleKnown(t.titleID);
end,
["OnUpdateForSpecificGender"] = function(t)
return OnUpdateForSpecificGender;
end,
},
"WithGender", {
collected = function(t)
local ids, acctTitles, charTitles = t.titleIDs, ATTAccountWideData.Titles, app.CurrentCharacter.Titles;
local m, f = ids[1], ids[2];
local alreadyHaveOne = charTitles[m] or charTitles[f];
local collectedM = app.SetCollected(nil, "Titles", m, IsTitleKnown(m));
local collectedF = app.SetCollected(nil, "Titles", f, IsTitleKnown(f));
if collectedM == 1 or collectedF == 1 then
if not alreadyHaveOne then AddToCollection(t); end
return 1;
elseif collectedM == 2 or collectedF == 2 then
return 2;
end
end,
description = function(t)
return "This title changes its state whenever your character changes its gender identity. This is particularly common in Brunnhildar Village in Storm Peaks or by means of using an Engineering teleport. In account mode you will need to have multiple characters with representation of both gender types.";
end,
saved = function(t)
local ids = t.titleIDs;
return IsTitleKnown(ids[1]) or IsTitleKnown(ids[2]);
end,
title = function(t)
local ids, acctTitles = t.titleIDs, ATTAccountWideData.Titles;
local m, f = ids[1], ids[2];
local player = "("..CALENDAR_PLAYER_NAME..")";
return " " .. StylizePlayerTitle(GetTitleName(m), t.style, player).. "\n " .. StylizePlayerTitle(GetTitleName(f), t.style, player)
.. DESCRIPTION_SEPARATOR .. GetCollectionIcon(acctTitles[m]) .. "\n" .. GetCollectionIcon(acctTitles[f]);
end,
titleID = function(t)
return t.playerGender == 2 and t.titleIDs[1] or t.titleIDs[2];
end,
OnUpdate = function(t)
return OnUpdateForGenderedTitle;
end
}, (function(t) return t.titleIDs; end));
end)();
-- Unsupported Libs
(function()
-- Neither of these are supported at this time.
app.CreateArtifact = function(id, t)
return { text = "Artifact #" .. id, description = "This data type is not supported at this time." };
end
app.CreateAzeriteEssence = function(id, t)
return { text = "AzeriteEssence #" .. id, description = "This data type is not supported at this time." };
end
app.CreateConduit = function(id, t)
return { text = "Conduit #" .. id, description = "This data type is not supported at this time." };
end
app.CreateDrakewatcherManuscript = function(id, t)
return { text = "DrakewatcherManuscript #" .. id, description = "This data type is not supported at this time." };
end
app.CreateFollower = function(id, t)
return { text = "Follower #" .. id, description = "This data type is not supported at this time." };
end
app.CreateGearSet = function(id, t)
return { text = "GearSet #" .. id, description = "This data type is not supported at this time." };
end
app.CreateGearSetHeader = function(id, t)
return { text = "GearSetHeader #" .. id, description = "This data type is not supported at this time." };
end
app.CreateGearSetSubHeader = function(id, t)
return { text = "GearSetSubHeader #" .. id, description = "This data type is not supported at this time." };
end
app.CreateMusicRoll = function(questID, t)
return { text = "MusicRoll #" .. questID, description = "This data type is not supported at this time." };
end
app.CreatePetAbility = function(id, t)
return { text = "PetAbility #" .. id, description = "This data type is not supported at this time." };
end
app.CreateRace = function(id, t)
return { text = "Race #" .. id, description = "This data type is not supported at this time." };
end
app.CreateRuneforgeLegendary = function(id, t)
return { text = "RuneforgeLegendary #" .. id, description = "This data type is not supported at this time." };
end
app.CreateSelfieFilter = function(id, t)
return { text = "SelfieFilter #" .. id, description = "This data type is not supported at this time." };
end
app.CreateVignette = function(id, t)
return { text = "Vignette #" .. id, description = "This data type is not supported at this time." };
end
app.CreateItemSource = function(sourceID, itemID, t)
t = app.CreateItem(itemID, t);
t.s = sourceID;
return t;
end
end)();
-- Filtering
function app.Filter()
-- Meaning "Don't display."
return false;
end
function app.NoFilter()
-- Meaning "Display as expected."
return true;
end
function app.FilterGroupsByLevel(group)
-- after 9.0, transition to a req lvl range, either min, or min + max
if group.lvl then
local minlvl, maxlvl;
if type(group.lvl) == "table" then
minlvl = group.lvl[1];
maxlvl = group.lvl[2];
else
minlvl = group.lvl;
end
if maxlvl then
-- min and max provided
return app.Level >= minlvl and app.Level <= maxlvl;
elseif minlvl then
-- only min provided
return app.Level >= minlvl;
end
end
-- no level requirement on the group, have to include it
return true;
end
function app.FilterGroupsByCompletion(group)
return group.progress < group.total;
end
function app.FilterItemBind(item)
return item.b == 2 or item.b == 3; -- BoE
end
function app.FilterItemClass(item)
if app.UnobtainableItemFilter(item) and app.RequireEventFilter(item) and app.PvPFilter(item) then
if app.ItemBindFilter(item) then return true; end
return app.ItemTypeFilter(item)
and app.RequireBindingFilter(item)
and app.RequiredSkillFilter(item)
and app.ClassRequirementFilter(item)
and app.RaceRequirementFilter(item)
and app.RequireFactionFilter(item);
end
end
function app.FilterItemPvP(item)
if item.pvp then
return false;
else
return true;
end
end
function app.FilterItemClass_RequireClasses(item)
return not item.nmc;
end
function app.FilterItemClass_RequireItemFilter(item)
if item.f then
if app.Settings:GetFilter(item.f) then
return true;
else
return false;
end
else
return true;
end
end
function app.FilterItemClass_RequireRaces(item)
return not item.nmr;
end
if app.GameBuildVersion > 11403 then
function app.FilterItemClass_RequireRacesCurrentFaction(item)
if item.nmr then
if item.r then
if item.r == app.FactionID then
return true;
else
return false;
end
end
if item.races then
if app.FactionID == HORDE_FACTION_ID then
return containsAny(item.races, HORDE_ONLY);
else
return containsAny(item.races, ALLIANCE_ONLY);
end
else
return false;
end
else
return true;
end
end
else
function app.FilterItemClass_RequireRacesCurrentFaction(item)
if item.nmr then
if item.r then
if item.r == app.FactionID then
return true;
else
return false;
end
end
if item.races then
if app.FactionID == HORDE_FACTION_ID then
return containsAny(item.races, HORDE_ONLY);
else
return containsAny(item.races, ALLIANCE_ONLY);
end
else
return false;
end
else
if item.nmc then
if item.c and #item.c == 1 then
if app.FactionID == HORDE_FACTION_ID then
return item.c[1] ~= 2; -- Check for Paladin
else
return item.c[1] ~= 7; -- Check for Shaman
end
else
return true;
end
else
return true;
end
end
end
end
function app.FilterItemClass_RequireBinding(item)
if item.b and (item.b == 2 or item.b == 3) then
return false;
else
return true;
end
end
function app.FilterItemClass_RequiredSkill(item)
local requireSkill = item.requireSkill;
if requireSkill and (not item.professionID or not GetRelativeValue(item, "DontEnforceSkillRequirements") or item.b == 1) then
requireSkill = app.SkillIDToSpellID[requireSkill];
return requireSkill and app.CurrentCharacter.ActiveSkills[requireSkill];
else
return true;
end
end
function app.FilterItemClass_RequireFaction(item)
if item.minReputation and app.IsFactionExclusive(item.minReputation[1]) then
if item.minReputation[2] > (select(6, GetFactionInfoByID(item.minReputation[1])) or 0) then
return false;
else
return true;
end
else
return true;
end
end
function app.FilterItemClass_UnobtainableItem(item)
if item.u and not app.Settings:GetUnobtainableFilter(item.u) then
return false;
else
return true;
end
end
function app.FilterItemTrackable(group)
return group.trackable;
end
function app.ObjectVisibilityFilter(group)
return group.visible;
end
-- Default Filter Settings (changed in ADDON_LOADED and in the Options Menu)
app.VisibilityFilter = app.ObjectVisibilityFilter;
app.GroupFilter = app.FilterItemClass;
app.GroupRequirementsFilter = app.NoFilter;
app.GroupVisibilityFilter = app.NoFilter;
app.ItemBindFilter = app.FilterItemBind;
app.ItemTypeFilter = app.NoFilter;
app.PvPFilter = app.NoFilter;
app.CollectedItemVisibilityFilter = app.NoFilter;
app.ClassRequirementFilter = app.NoFilter;
app.RaceRequirementFilter = app.NoFilter;
app.RequireBindingFilter = app.NoFilter;
app.RequireEventFilter = app.Modules.Events.FilterIsEventActive;
app.RequiredSkillFilter = app.NoFilter;
app.RequireFactionFilter = app.FilterItemClass_RequireFaction;
app.UnobtainableItemFilter = app.FilterItemClass_UnobtainableItem;
app.ShowIncompleteThings = app.Filter;
-- Recursive Checks
app.RecursiveGroupRequirementsFilter = function(group)
if app.GroupRequirementsFilter(group) and app.GroupFilter(group) then
if group.parent then return app.RecursiveGroupRequirementsFilter(group.parent); end
return true;
end
return false;
end
app.RecursiveClassAndRaceFilter = function(group)
if app.ClassRequirementFilter(group) and app.RaceRequirementFilter(group) then
if group.parent then return app.RecursiveClassAndRaceFilter(group.parent); end
return true;
end
return false;
end
app.RecursiveDefaultClassAndRaceFilter = function(group)
if app.FilterItemClass_RequireClasses(group) and app.FilterItemClass_RequireRaces(group) then
if group.parent then return app.RecursiveDefaultClassAndRaceFilter(group.parent); end
return true;
end
return false;
end
app.RecursiveUnobtainableFilter = function(group)
if app.UnobtainableItemFilter(group) and app.RequireEventFilter(group) then
if group.parent then return app.RecursiveUnobtainableFilter(group.parent); end
return true;
end
return false;
end
app.RecursiveIsDescendantOfParentWithValue = function(group, field, value)
if group then
if group[field] and group[field] == value then
return true
else
if group.parent then
return app.RecursiveIsDescendantOfParentWithValue(group.parent, field, value)
end
end
end
return false;
end
-- Processing Functions (Coroutines)
UpdateGroup = function(parent, group)
local visible = false;
-- Determine if this user can enter the instance or acquire the item.
if app.GroupRequirementsFilter(group) then
-- Check if this is a group
if group.g then
-- If this item is collectible, then mark it as such.
if group.collectible then
-- An item is a special case where it may have both an appearance and a set of items
group.progress = group.collected and 1 or 0;
group.total = 1;
else
-- Default to 0 for both
group.progress = 0;
group.total = 0;
end
-- Update the subgroups recursively...
visible = UpdateGroups(group, group.g);
-- If the 'can equip' filter says true
if app.GroupFilter(group) and app.ClassRequirementFilter(group) and app.RaceRequirementFilter(group) then
-- Increment the parent group's totals.
parent.total = (parent.total or 0) + group.total;
parent.progress = (parent.progress or 0) + group.progress;
-- If this group is trackable, then we should show it.
if group.total > 0 and app.GroupVisibilityFilter(group) then
visible = true;
elseif app.ShowIncompleteThings(group) and not group.saved then
visible = true;
elseif ((group.itemID and group.f) or group.sym) and app.Settings.Collectibles.Loot then
visible = true;
end
else
visible = false;
end
else
-- If the 'can equip' filter says true
if app.GroupFilter(group) then
if group.collectible then
-- Increment the parent group's totals.
parent.total = (parent.total or 0) + 1;
-- If we've collected the item, use the "Show Collected Items" filter.
if group.collected then
parent.progress = (parent.progress or 0) + 1;
if app.CollectedItemVisibilityFilter(group) then
visible = true;
end
else
visible = true;
end
elseif group.trackable then
-- If this group is trackable, then we should show it.
if app.ShowIncompleteThings(group) and not group.saved then
visible = true;
end
elseif ((group.itemID and group.f) or group.sym) and app.Settings.Collectibles.Loot then
visible = true;
elseif app.Settings:Get("DebugMode") then
visible = true;
end
elseif app.Settings:Get("DebugMode") then
visible = true;
else
visible = false;
end
end
end
-- Set the visibility
group.visible = visible;
return visible;
end
UpdateGroups = function(parent, g)
if g then
local visible = false;
for i=1,#g,1 do
local group = g[i];
if group.OnUpdate then
--local lastUpdate = GetTimePreciseSec();
local result = group:OnUpdate(group);
--local duration = (GetTimePreciseSec() - lastUpdate) * 10000;
--if duration > 10 then print(group.text, "OnUpdate: ", duration); end
if not result then
if UpdateGroup(parent, group) then
visible = true;
end
elseif group.visible then
visible = true;
end
elseif UpdateGroup(parent, group) then
visible = true;
end
end
return visible;
end
end
local function UpdateParentProgress(group)
if group.collectible then
group.progress = group.progress + 1;
end
-- Continue on to this object's parent.
if group.parent then
if group.visible then
-- If we were initially visible, then update the parent.
UpdateParentProgress(group.parent);
-- If this group is trackable, then we should show it.
if app.GroupVisibilityFilter(group) then
group.visible = true;
elseif app.ShowIncompleteThings(group) then
group.visible = not group.saved;
else
group.visible = false;
end
end
end
end
app.UpdateGroups = UpdateGroups;
app.UpdateParentProgress = UpdateParentProgress;
-- Helper Methods
function app.GetNumberOfItemsUntilNextPercentage(progress, total)
if total <= progress then
return "|c" .. GetProgressColor(1) .. "YOU DID IT!|r";
else
local originalPercent = progress / total;
local nextPercent = math.ceil(originalPercent * 100);
local roundedPercent = nextPercent * 0.01;
local diff = math.ceil(total * (roundedPercent - originalPercent));
if diff < 1 or nextPercent == 100 then
return "|c" .. GetProgressColor(1) .. (total - progress) .. " THINGS UNTIL 100%|r";
elseif diff == 1 then
return "|c" .. GetProgressColor(roundedPercent) .. diff .. " THING UNTIL " .. nextPercent .. "%|r";
else
return "|c" .. GetProgressColor(roundedPercent) .. diff .. " THINGS UNTIL " .. nextPercent .. "%|r";
end
end
end
function app.QuestCompletionHelper(questID)
-- Search ATT for the related quests.
local searchResults = SearchForField("questID", questID);
if #searchResults > 0 then
-- Only increase progress for Quests as Collectible users.
if app.Settings.Collectibles.Quests then
-- Attempt to cleanly refresh the data.
for i,result in ipairs(searchResults) do
if result.visible and result.parent and result.parent.total then
result.marked = true;
end
end
for i,result in ipairs(searchResults) do
if result.marked then
result.marked = nil;
if result.total then
-- This is an item that has a relative set of groups
UpdateParentProgress(result);
-- If this is NOT a group...
if not result.g and result.collectible then
-- If we've collected the item, use the "Show Collected Items" filter.
result.visible = app.CollectedItemVisibilityFilter(result);
end
else
UpdateParentProgress(result.parent);
if result.collectible then
-- If we've collected the item, use the "Show Collected Items" filter.
result.visible = app.CollectedItemVisibilityFilter(result);
end
end
end
end
end
end
end
-- Refresh certain kinds of data.
local function RefreshSkills()
-- Store Skill Data
local activeSkills = app.CurrentCharacter.ActiveSkills;
wipe(activeSkills);
rawset(app.SpellNameToSpellID, 0, nil);
app.GetSpellName(0);
local GetSkillLineInfo = _G["GetSkillLineInfo"];
if GetSkillLineInfo then
for index=GetNumSkillLines(),2,-1 do
local skillName, header, isExpanded, skillRank, numTempPoints, skillModifier,
skillMaxRank, isAbandonable, stepCost, rankCost, minLevel, skillCostType,
skillDescription = GetSkillLineInfo(index);
if not header then
local spellID = app.SpellNameToSpellID[skillName];
if spellID then
local spellName = GetSpellInfo(spellID);
for _,s in pairs(app.SkillIDToSpellID) do
if GetSpellInfo(s) == spellName then
spellID = s;
break;
end
end
activeSkills[spellID] = { skillRank, skillMaxRank };
else
for _,s in pairs(app.SkillIDToSpellID) do
if GetSpellInfo(s) == skillName then
spellID = s;
break;
end
end
if spellID then
activeSkills[spellID] = { skillRank, skillMaxRank };
else
--print(skillName, header, isExpanded, skillRank, numTempPoints, skillModifier, skillMaxRank, isAbandonable, stepCost, rankCost, minLevel, skillCostType, skillDescription);
end
end
end
end
end
-- Hunter Only
if app.ClassIndex == 3 then
activeSkills[5149] = { 1, 1 };
end
-- Clone the data for the specializations.
for specID,spellID in pairs(app.SpecializationSpellIDs) do
local baseSpell = activeSkills[spellID];
if baseSpell and (app.CurrentCharacter.Spells[specID] or IsSpellKnown(specID)) then
activeSkills[specID] = baseSpell;
end
end
end
local function RefreshCollections()
app:StartATTCoroutine("RefreshingCollections", function()
while InCombatLockdown() do coroutine.yield(); end
app.print("Refreshing collection...");
app.events.QUEST_LOG_UPDATE();
coroutine.yield();
-- Harvest Illusion Collections
if C_TransmogCollection and C_TransmogCollection.GetIllusions then
local collectedIllusions = ATTAccountWideData.Illusions;
for _,illusion in ipairs(C_TransmogCollection.GetIllusions()) do
if illusion.isCollected then collectedIllusions[illusion.sourceID] = 1; end
end
coroutine.yield();
end
-- Refresh Toys
local collected;
for id,t in pairs(app.SearchForFieldContainer("toyID")) do
if #t > 0 then
collected = t[1].collected; -- Run the collected field's code.
end
end
coroutine.yield();
RefreshSkills();
app:RefreshDataCompletely("RefreshCollections");
app.print("Done refreshing collection.");
end);
end
app.RefreshCollections = RefreshCollections;
-- Automatically Refresh Saved Instances
local function RefreshSaves()
local waitTimer = 30;
while waitTimer > 0 do
coroutine.yield();
waitTimer = waitTimer - 1;
end
-- While the player is in combat, wait for combat to end.
while InCombatLockdown() do coroutine.yield(); end
-- While the player is still logging in, wait.
while not app.GUID do coroutine.yield(); end
-- Cache the lockouts across your account.
local serverTime = GetServerTime();
-- Check to make sure that the old instance data has expired
for guid,character in pairs(ATTCharacterData) do
local locks = character.Lockouts;
if locks then
for name,instance in pairs(locks) do
local count = 0;
for difficulty,lock in pairs(instance) do
if type(lock) ~= "table" or type(lock.reset) ~= "number" or serverTime >= lock.reset then
-- Clean this up.
instance[difficulty] = nil;
else
count = count + 1;
end
end
if count == 0 then
-- Clean this up.
locks[name] = nil;
end
end
end
end
-- While the player is still waiting for information, wait.
-- NOTE: Usually, this is only 1 wait.
local counter = 0;
while GetNumSavedInstances() < 1 do
coroutine.yield();
counter = counter + 1;
if counter > 10 then
app.refreshingSaves = false;
return false;
end
end
-- Update Saved Instances
local myLockouts = app.CurrentCharacter.Lockouts;
for instanceIter=1,GetNumSavedInstances() do
local name, id, reset, difficulty, locked, _, _, isRaid, _, _, numEncounters = GetSavedInstanceInfo(instanceIter);
if locked then
-- Update the name of the instance and cache the lock for this instance
difficulty = difficulty or 7;
reset = serverTime + reset;
local locks = myLockouts[name];
if not locks then
locks = {};
myLockouts[name] = locks;
end
-- Create the lock for this difficulty
local lock = locks[difficulty];
if not lock then
lock = { ["id"] = id, ["reset"] = reset, ["encounters"] = {}};
locks[difficulty] = lock;
else
lock.id = id;
lock.reset = reset;
end
-- If this is LFR, then don't share.
if difficulty == 7 or difficulty == 17 then
if #lock.encounters == 0 then
-- Check Encounter locks
for encounterIter=1,numEncounters do
local name, _, isKilled = GetSavedInstanceEncounterInfo(instanceIter, encounterIter);
tinsert(lock.encounters, { ["name"] = name, ["isKilled"] = isKilled });
end
else
-- Check Encounter locks
for encounterIter=1,numEncounters do
local name, _, isKilled = GetSavedInstanceEncounterInfo(instanceIter, encounterIter);
if not lock.encounters[encounterIter] then
tinsert(lock.encounters, { ["name"] = name, ["isKilled"] = isKilled });
elseif isKilled then
lock.encounters[encounterIter].isKilled = true;
end
end
end
else
-- Create the pseudo "shared" lock
local shared = locks["shared"];
if not shared then
shared = {};
shared.id = id;
shared.reset = reset;
shared.encounters = {};
locks["shared"] = shared;
-- Check Encounter locks
for encounterIter=1,numEncounters do
local name, _, isKilled = GetSavedInstanceEncounterInfo(instanceIter, encounterIter);
tinsert(lock.encounters, { ["name"] = name, ["isKilled"] = isKilled });
-- Shared Encounter is always assigned if this is the first lock seen for this instance
tinsert(shared.encounters, { ["name"] = name, ["isKilled"] = isKilled });
end
else
-- Check Encounter locks
for encounterIter=1,numEncounters do
local name, _, isKilled = GetSavedInstanceEncounterInfo(instanceIter, encounterIter);
if not lock.encounters[encounterIter] then
tinsert(lock.encounters, { ["name"] = name, ["isKilled"] = isKilled });
elseif isKilled then
lock.encounters[encounterIter].isKilled = true;
end
if not shared.encounters[encounterIter] then
tinsert(shared.encounters, { ["name"] = name, ["isKilled"] = isKilled });
elseif isKilled then
shared.encounters[encounterIter].isKilled = true;
end
end
end
end
end
end
-- Mark that we're done now.
app:RedrawWindows("RefreshSaves");
end
app:RegisterEvent("BOSS_KILL");
app.events.BOSS_KILL = function(id, name, ...)
-- This is so that when you kill a boss, you can trigger
-- an automatic update of your saved instance cache.
-- (It does lag a little, but you can disable this if you want.)
-- Waiting until the LOOT_CLOSED occurs will prevent the failed Auto Loot bug.
-- print("BOSS_KILL", id, name, ...);
app:UnregisterEvent("LOOT_CLOSED");
app:RegisterEvent("LOOT_CLOSED");
end
app.events.LOOT_CLOSED = function()
-- Once the loot window closes after killing a boss, THEN trigger the update.
app:UnregisterEvent("LOOT_CLOSED");
app:UnregisterEvent("UPDATE_INSTANCE_INFO");
app:RegisterEvent("UPDATE_INSTANCE_INFO");
RequestRaidInfo();
end
app.events.UPDATE_INSTANCE_INFO = function()
app:UnregisterEvent("UPDATE_INSTANCE_INFO");
app:StartATTCoroutine("RefreshSaves", RefreshSaves);
end
-- TomTom Support
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, __TomTomWaypointFirst = setmetatable({}, { __index = function(t, mapID)
local o = setmetatable({}, __TomTomWaypointCacheIndexX);
rawset(t, mapID, o);
return o;
end });
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] or defaultMapID,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 opt = { from = "ATT", persistent = false };
opt.title = first.text or RETRIEVING_DATA;
local displayID = GetDisplayID(first);
if displayID then
opt.minimap_displayID = displayID;
opt.worldmap_displayID = displayID;
end
if first.icon then
opt.minimap_icon = first.icon;
opt.worldmap_icon = first.icon;
end
if TomTom.DefaultCallbacks then
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(group.achievementID));
else
if key == "npcID" then key = "creatureID"; end
AttachTooltipSearchResults(tooltip, 1, key .. ":" .. o[o.key], SearchForField, key, o[o.key], line);
end
end
tooltip:Show();
end
callbacks.world.tooltip_update = nil;
callbacks.world.tooltip_show = callbacks.minimap.tooltip_show;
opt.callbacks = callbacks;
end
TomTom:AddWaypoint(mapID, xnormal, y / 1000, opt);
end
end
end
end
TomTom:SetClosestWaypoint();
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
-- Minimap Button
function AllTheThings_MinimapButtonOnClick(self, button)
if button == "RightButton" then
-- Right Button opens the Options menu.
app.Settings:Open();
else
-- Left Button
if IsShiftKeyDown() then
app.RefreshCollections();
elseif IsAltKeyDown() or IsControlKeyDown() then
app.ToggleMiniListForCurrentZone();
else
app.ToggleMainList();
end
end
end
function AllTheThings_MinimapButtonOnEnter(self, button)
GameTooltip:SetOwner(type(self) ~= "string" and self or button, "ANCHOR_LEFT");
GameTooltip:ClearLines();
local reference = app:GetDataCache();
if reference then
GameTooltipIcon:SetSize(72,72);
GameTooltipIcon:ClearAllPoints();
GameTooltipIcon:SetPoint("TOPRIGHT", GameTooltip, "TOPLEFT", 0, 0);
GameTooltipIcon.icon:SetTexture(reference.preview or reference.icon);
local texcoord = reference.texcoord;
if texcoord then
GameTooltipIcon.icon:SetTexCoord(texcoord[1], texcoord[2], texcoord[3], texcoord[4]);
else
GameTooltipIcon.icon:SetTexCoord(0, 1, 0, 1);
end
GameTooltipIcon:Show();
local left, right = strsplit(DESCRIPTION_SEPARATOR, reference.title);
GameTooltip:AddDoubleLine(reference.text, reference.progressText, 1, 1, 1);
GameTooltip:AddDoubleLine(left, right, 1, 1, 1);
local prime = app:GetWindow("Prime");
if prime and prime.forceFullDataRefresh then
GameTooltip:AddDoubleLine("Updates Paused", L["MAIN_LIST_REQUIRES_REFRESH"], 1, 0.4, 0.4);
else
GameTooltip:AddLine(reference.description, 0.4, 0.8, 1, 1);
end
else
GameTooltip:AddDoubleLine(L["TITLE"], "Click to load addon.", 1, 1, 1);
GameTooltipIcon:Hide();
end
GameTooltip:AddLine(L["MINIMAP_MOUSEOVER_TEXT"], 1, 1, 1);
GameTooltip:Show();
end
function AllTheThings_MinimapButtonOnLeave()
GameTooltip:Hide();
GameTooltipIcon.icon.Background:Hide();
GameTooltipIcon.icon.Border:Hide();
GameTooltipIcon:Hide();
GameTooltipModel:Hide();
end
local function CreateMinimapButton()
-- Create the Button for the Minimap frame. Create a local and non-local copy.
local size = app.Settings:GetTooltipSetting("MinimapSize");
local button = CreateFrame("BUTTON", appName .. "-Minimap", Minimap);
button:SetPoint("CENTER", 0, 0);
button:SetFrameStrata("HIGH");
button:SetMovable(true);
button:EnableMouse(true);
button:RegisterForDrag("LeftButton", "RightButton");
button:RegisterForClicks("LeftButtonUp", "RightButtonUp");
button:SetSize(size, size);
-- Create the Button Texture
local texture = button:CreateTexture(nil, "BACKGROUND");
texture:SetTexture(app.asset("Discord_2_64"));
texture:SetAllPoints();
button.texture = texture;
-- Create the Button Texture
local oldtexture = button:CreateTexture(nil, "BACKGROUND");
oldtexture:SetPoint("CENTER", 0, 0);
oldtexture:SetTexture(app.asset("logo_tiny"));
oldtexture:SetSize(21, 21);
button.oldtexture = oldtexture;
-- Create the Button Tracking Border
local border = button:CreateTexture(nil, "BORDER");
border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder");
border:SetPoint("CENTER", 12, -12);
border:SetSize(56, 56);
button.border = border;
button.UpdateStyle = function(self)
if app.Settings:GetTooltipSetting("MinimapStyle") then
self:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight", "ADD");
self.texture:Hide();
self.oldtexture:Show();
self.border:Show();
else
self:SetHighlightTexture(app.asset("MinimapHighlight_64x64"));
self.texture:Show();
self.oldtexture:Hide();
self.border:Hide();
end
end
button:UpdateStyle();
-- Button Configuration
local rounding = 10;
local MinimapShapes = {
-- quadrant booleans (same order as SetTexCoord)
-- {bottom-right, bottom-left, top-right, top-left}
-- true = rounded, false = squared
["ROUND"] = {true, true, true, true },
["SQUARE"] = {false, false, false, false},
["CORNER-TOPLEFT"] = {false, false, false, true },
["CORNER-TOPRIGHT"] = {false, false, true, false},
["CORNER-BOTTOMLEFT"] = {false, true, false, false},
["CORNER-BOTTOMRIGHT"] = {true, false, false, false},
["SIDE-LEFT"] = {false, true, false, true },
["SIDE-RIGHT"] = {true, false, true, false},
["SIDE-TOP"] = {false, false, true, true },
["SIDE-BOTTOM"] = {true, true, false, false},
["TRICORNER-TOPLEFT"] = {false, true, true, true },
["TRICORNER-TOPRIGHT"] = {true, false, true, true },
["TRICORNER-BOTTOMLEFT"] = {true, true, false, true },
["TRICORNER-BOTTOMRIGHT"] = {true, true, true, false},
};
button.update = function(self)
local position = GetDataMember("Position", -10.31);
local angle = math.rad(position) -- determine position on your own
local x, y
local cos = math.cos(angle)
local sin = math.sin(angle)
local q = 1;
if cos < 0 then
q = q + 1; -- lower
end
if sin > 0 then
q = q + 2; -- right
end
local width = (Minimap:GetWidth() * 0.5) + 8;
local height = (Minimap:GetHeight() * 0.5) + 8;
if MinimapShapes[GetMinimapShape and GetMinimapShape() or "ROUND"][q] then
x = cos*width;
y = sin*height;
else
x = math.max(-width, math.min(cos*(math.sqrt(2*(width)^2)-rounding), width))
y = math.max(-height, math.min(sin*(math.sqrt(2*(height)^2)-rounding), height))
end
self:SetPoint("CENTER", "Minimap", "CENTER", -math.floor(x), math.floor(y));
end
local update = function(self)
local w, x = GetCursorPosition();
local y, z = Minimap:GetLeft(), Minimap:GetBottom();
local s = UIParent:GetScale();
w = y - w / s + 70; x = x / s - z - 70;
SetDataMember("Position", math.deg(math.atan2(x, w)));
self:Raise();
self:update();
end
-- Register for Frame Events
button:SetScript("OnDragStart", function(self)
self:SetScript("OnUpdate", update);
end);
button:SetScript("OnDragStop", function(self)
self:SetScript("OnUpdate", nil);
end);
button:SetScript("OnEnter", AllTheThings_MinimapButtonOnEnter);
button:SetScript("OnLeave", AllTheThings_MinimapButtonOnLeave);
button:SetScript("OnClick", AllTheThings_MinimapButtonOnClick);
button:update();
button:Show();
return button;
end
app.CreateMinimapButton = CreateMinimapButton;
-- Row Helper Functions
local SetPortraitTexture = _G["SetPortraitTexture"];
local SetPortraitTextureFromDisplayID = _G["SetPortraitTextureFromCreatureDisplayID"];
local function SetPortraitIcon(self, data, x)
local displayID = GetDisplayID(data);
if displayID then
SetPortraitTextureFromDisplayID(self, displayID);
self:SetWidth(self:GetHeight());
self:SetTexCoord(0, 1, 0, 1);
return true;
elseif data.unit and not data.icon then
SetPortraitTexture(self, data.unit);
self:SetWidth(self:GetHeight());
self:SetTexCoord(0, 1, 0, 1);
return true;
end
-- Fallback to a traditional icon.
if data.atlas then
self:SetAtlas(data.atlas);
self:SetWidth(self:GetHeight());
self:SetTexCoord(0, 1, 0, 1);
if data["atlas-background"] then
self.Background:SetAtlas(data["atlas-background"]);
self.Background:SetWidth(self:GetHeight());
self.Background:Show();
end
if data["atlas-border"] then
self.Border:SetAtlas(data["atlas-border"]);
self.Border:SetWidth(self:GetHeight());
self.Border:Show();
if data["atlas-color"] then
local swatches = data["atlas-color"];
self.Border:SetVertexColor(swatches[1], swatches[2], swatches[3], swatches[4] or 1.0);
else
self.Border:SetVertexColor(1, 1, 1, 1.0);
end
end
return true;
elseif data.icon then
self:SetWidth(self:GetHeight());
self:SetTexture(data.icon);
local texcoord = data.texcoord;
if texcoord then
self:SetTexCoord(texcoord[1], texcoord[2], texcoord[3], texcoord[4]);
else
self:SetTexCoord(0, 1, 0, 1);
end
return true;
end
end
local function CalculateRowBack(data)
if data.back then return data.back; end
if data.parent then
return CalculateRowBack(data.parent) * 0.5;
else
return 0;
end
end
local function CalculateRowIndent(data)
if data.indent then return data.indent; end
if data.parent then
return CalculateRowIndent(data.parent) + 1;
else
return 0;
end
end
local CreateRow;
local function SetRowData(self, row, data)
if row.ref ~= data then
-- New data, update everything
row.ref = data;
row.summaryText = nil;
if not data then
row.Background:SetAlpha(0);
row.Background:Hide();
row.Texture:Hide();
row.Texture.Background:Hide();
row.Texture.Border:Hide();
row.Indicator:Hide();
row.Summary:Hide();
row.Label:Hide();
row:Hide();
return;
end
local font = data.font or "GameFontNormal";
if font ~= row.lastFont then
row.Label:SetFontObject(font);
row.Summary:SetFontObject(font);
row.lastFont = font;
end
-- Every valid row has a summary and label
row.Label:SetPoint("RIGHT", row.Summary, "LEFT", 0, 0);
row.Summary:Show();
row.Label:Show();
row:Show();
-- Calculate the indent
local indent = ((CalculateRowIndent(data) * 8) or 0) + 8;
row.Texture.Background:SetPoint("LEFT", row, "LEFT", indent, 0);
row.Texture.Border:SetPoint("LEFT", row, "LEFT", indent, 0);
row.Texture:SetPoint("LEFT", row, "LEFT", indent, 0);
row.indent = indent;
-- Calculate the back color
local back = CalculateRowBack(data);
if back then
row.back = back;
if back > 0 then
row.Background:SetAlpha(back);
row.Background:Show();
else
row.Background:Hide();
end
end
elseif not data then
return; -- Already cleared
end
-- Update the Summary Text (this will be the thing that updates the most)
local summary = data.summary or GetProgressTextForRow(data);
local oldSummary = row.summaryText;
if oldSummary then
if summary then
if oldSummary ~= summary then
row.Summary:SetText(summary);
row.summaryText = summary;
self.smudged = true; -- Mark this as smudged, so that it knowns to Update the rows completely. (this means that something changed its state)
end
else
row.Summary:SetText((data.g and not data.expanded and #data.g > 0 and "+++") or "---");
row.summaryText = nil;
self.smudged = true; -- Mark this as smudged, so that it knowns to Update the rows completely. (this means that something changed its state)
end
else
if summary then
row.Summary:SetText(summary);
row.summaryText = summary;
else
row.Summary:SetText((data.g and not data.expanded and #data.g > 0 and "+++") or "---");
end
end
-- Determine the Indicator Texture
-- TODO: Move this to a field?
local indicatorTexture = data.e and L["UNOBTAINABLE_ITEM_TEXTURES"][4];
if data.u then
local reason = L["UNOBTAINABLE_ITEM_REASONS"][data.u];
if reason and (not reason[5] or app.GameBuildVersion < reason[5]) then
indicatorTexture = L["UNOBTAINABLE_ITEM_TEXTURES"][reason[1]];
end
end
-- If data is quest and is currently accepted or saved...
if data.questID and C_QuestLog_IsOnQuest(data.questID) then
indicatorTexture = app.asset("known_circle");
elseif data.saved then
if data.parent and data.parent.locks or data.isDaily then
indicatorTexture = app.asset("known");
else
indicatorTexture = app.asset("known_green");
end
end
-- Check to see what the text is currently
local text = data.text;
if text ~= row.text then
if not text then
text = RETRIEVING_DATA;
self.processingLinks = true;
elseif string.match(text, RETRIEVING_DATA) or text:find("^%[%]") or text:find("%[]") then
-- This means the link is still rendering
self.processingLinks = true;
else
row.text = text;
end
row.Label:SetText(text);
row:SetHeight(select(2, row.Label:GetFont()) + 4);
end
-- If the data has a texture, assign it.
if SetPortraitIcon(row.Texture, data) and row.Texture:GetTextureFilePath() then
row.Texture:Show();
row.Label:SetPoint("LEFT", row.Texture, "RIGHT", 2, 0);
-- If we have a texture, let's assign it.
if indicatorTexture then
row.Indicator:SetTexture(indicatorTexture);
row.Indicator:SetPoint("RIGHT", row.Texture, "LEFT", -2, 0);
row.Indicator:Show();
else
row.Indicator:Hide();
end
else
row.Texture:Hide();
row.Label:SetPoint("LEFT", row, "LEFT", row.indent, 0);
-- If we have a texture, let's assign it.
if indicatorTexture then
row.Indicator:SetTexture(indicatorTexture);
row.Indicator:SetPoint("RIGHT", row, "LEFT", row.indent, 0);
row.Indicator:Show();
else
row.Indicator:Hide();
end
end
end
local function RedrawVisibleRowData(self)
-- If there is no raw data, then return immediately.
if not self.rowData then return; end
-- Make it so that if you scroll all the way down, you have the ability to see all of the text every time.
local totalRowCount = #self.rowData;
if totalRowCount > 0 then
-- Ensure that the first row doesn't move out of position.
local container = self.Container;
local row = container.rows[1];
if not row then return; end
SetRowData(self, row, row.ref);
-- Fill the remaining rows up to the (visible) row count.
local containerHeight, totalHeight = container:GetHeight(), row:GetHeight();
for i=2,totalRowCount do
row = container.rows[i];
if row then
SetRowData(self, row, row.ref);
totalHeight = totalHeight + row:GetHeight();
if totalHeight > containerHeight then
break;
end
else
break;
end
end
end
end
local function UpdateRowProgress(group)
if group.collectible then
group.progress = group.collected and 1 or 0;
group.total = 1;
else
group.progress = 0;
group.total = 0;
end
if group.g then
for i,subgroup in ipairs(group.g) do
UpdateRowProgress(subgroup);
if subgroup.total then
group.progress = group.progress + subgroup.progress;
group.total = group.total + subgroup.total;
end
end
end
end
local function UpdateVisibleRowData(self)
-- If there is no raw data, then return immediately.
if not self.rowData then return; end
if self:GetHeight() > 48 then self.ScrollBar:Show(); else self.ScrollBar:Hide(); end
-- Make it so that if you scroll all the way down, you have the ability to see all of the text every time.
local totalRowCount = #self.rowData;
if totalRowCount > 0 then
-- Ensure that the first row doesn't move out of position.
local container = self.Container;
local row = container.rows[1] or CreateRow(container);
SetRowData(self, row, self.rowData[1]);
-- Fill the remaining rows up to the (visible) row count.
local current, rowCount, containerHeight, totalHeight =
math.max(1, math.min(self.CurrentIndex, totalRowCount)) + 1, 1, container:GetHeight(), row:GetHeight();
for i=2,totalRowCount do
row = container.rows[i] or CreateRow(container);
SetRowData(self, row, self.rowData[current]);
totalHeight = totalHeight + row:GetHeight();
if totalHeight > containerHeight then
break;
else
current = current + 1;
rowCount = rowCount + 1;
end
end
-- Hide the extra rows if any exist
for i=math.max(2, rowCount + 1),#container.rows do
local row = container.rows[i];
if row.ref then
SetRowData(self, row, nil);
else
break;
end
end
self:SetMinMaxValues(rowCount, totalRowCount + 1);
-- The data is smudged, meaning it needs to be Updated.
if self.smudged then
self.smudged = nil;
self:Update(true);
end
-- If the rows need to be processed again, do so next update.
if self.processingLinks then
self:StartATTCoroutine("Process Links", function()
while self.processingLinks do
self.processingLinks = nil;
coroutine.yield();
self:Redraw();
end
if self.UpdateDone then
self:StartATTCoroutine("UpdateDone", function()
coroutine.yield();
self:StartATTCoroutine("UpdateDoneP2", function()
coroutine.yield();
self:UpdateDone();
end);
end);
end
end);
elseif self.UpdateDone and rowCount > 5 then
self:StartATTCoroutine("UpdateDone", function()
coroutine.yield();
self:StartATTCoroutine("UpdateDoneP2", function()
coroutine.yield();
self:UpdateDone();
end);
end);
end
else
self:Hide();
end
end
local function IsSelfOrChild(self, focus)
-- This function helps validate that the focus is within the local hierarchy.
return focus and (self == focus or IsSelfOrChild(self, focus:GetParent()));
end
local function StopMovingOrSizing(self)
if self.isMoving then
self:StopMovingOrSizing();
self.isMoving = false;
self:RecordSettings();
end
end
local function StartMovingOrSizing(self)
if not (self:IsMovable() or self:IsResizable()) then
return
end
if self.isMoving then
StopMovingOrSizing(self);
else
self.isMoving = true;
if not self:IsMovable() or ((select(2, GetCursorPosition()) / self:GetEffectiveScale()) < math.max(self:GetTop() - 40, self:GetBottom() + 10)) then
self:StartSizing();
self:StartATTCoroutine("StartMovingOrSizing (Sizing)", function()
while self.isMoving do
self:Refresh();
coroutine.yield();
end
end);
else
self:StartMoving();
self:StartATTCoroutine("StartMovingOrSizing (Moving)", function()
while IsSelfOrChild(self, GetMouseFocus()) do
coroutine.yield();
end
StopMovingOrSizing(self);
end);
end
end
end
local function RowOnClick(self, button)
local reference = self.ref;
if reference then
-- If the row data itself has an OnClick handler... execute that first.
if reference.OnClick and reference.OnClick(self, button) then
return true;
end
if IsShiftKeyDown() then
if button == "RightButton" then
if app.Settings:GetTooltipSetting("Sort:Progress") then
app.print("Sorting selection by total progress...");
app:StartATTCoroutine("Sorting", function() app.SortGroup(reference, "progress", self, false) end);
else
app.print("Sorting selection alphabetically...");
app:StartATTCoroutine("Sorting", function() app.SortGroup(reference, "name", self, false) end);
end
return true;
end
-- If we're at the Auction House
if (AuctionFrame and AuctionFrame:IsShown()) or (AuctionHouseFrame and AuctionHouseFrame:IsShown()) then
local search = SearchForMissingItemNames(reference);
local count = #search;
if count < 1 then
app.print("No cached items found in search. Expand the group and view the items to cache the names and try again. Only Bind on Equip items will be found using this search.");
return true;
end
-- Auctionator Support
if TSM_API and TSM_API.IsUIVisible("AUCTION") then
app.print("TSM is not currently supported as the API for Classic is really limited.");
return true;
elseif Atr_SearchAH then
Atr_SelectPane(3);
if count > 1 then
Atr_SearchAH(L["TITLE"], search, LE_ITEM_CLASS_ARMOR);
return true;
else
Atr_SetSearchText (search[1]);
Atr_Search_Onclick ();
return true;
end
elseif Auctionator and Auctionator.API and AuctionatorTabs_Shopping then
Auctionator.API.v1.MultiSearchExact(L["TITLE"], search);
return;
end
-- Attempt to search manually with the link.
local link = reference.link or reference.silentLink;
if link and HandleModifiedItemClick(link) then
AuctionFrameBrowse_Search();
end
return true;
else
-- Not at the Auction House
-- If this reference has a link, then attempt to preview the appearance or write to the chat window.
local link = reference.link or reference.silentLink;
if link then
if HandleModifiedItemClick(link) or ChatEdit_InsertLink(link or BuildSourceTextForChat(reference, 0)) then return true; end
local _, dialog = StaticPopup_Visible("ALL_THE_THINGS_EDITBOX");
if dialog then dialog.editBox:SetText(link); return true; end
end
if button == "LeftButton" then RefreshCollections(); end
return true;
end
end
-- Control Click Expands the Groups
if IsControlKeyDown() then
-- If this reference has a link, then attempt to preview the appearance.
local link = reference.link or reference.silentLink;
if link and HandleModifiedItemClick(link) then
return true;
end
-- If this reference is anything else, expand the groups.
if reference.g then
if self.index < 1 and #reference.g > 0 then
ExpandGroupsRecursively(reference, not reference.g[1].expanded, true);
else
ExpandGroupsRecursively(reference, not reference.expanded, true);
end
self:GetParent():GetParent():Update();
return true;
end
end
-- All non-Shift Right Clicks open a mini list or the settings.
if button == "RightButton" then
if IsAltKeyDown() then
AddTomTomWaypoint(reference, false);
elseif self.index > 0 then
app:CreateMiniListForGroup(self.ref);
else
app.Settings:Open();
end
elseif self.index > 0 then
reference.expanded = not reference.expanded;
self:GetParent():GetParent():Update();
elseif not reference.expanded then
reference.expanded = true;
self:GetParent():GetParent():Update();
else
-- Allow the First Frame to move the parent.
local owner = self:GetParent():GetParent();
if owner:IsMovable() then
self:SetScript("OnMouseUp", function(self)
self:SetScript("OnMouseUp", nil);
StopMovingOrSizing(owner);
end);
StartMovingOrSizing(owner);
end
end
end
end
local function RowOnEnter(self)
local reference = self.ref; -- NOTE: This is the good ref value, not the parasitic one.
if reference and GameTooltip then
GameTooltipIcon.icon.Background:Hide();
GameTooltipIcon.icon.Border:Hide();
GameTooltipIcon:Hide();
GameTooltipModel:Hide();
GameTooltip:ClearLines();
GameTooltipIcon:ClearAllPoints();
GameTooltipModel:ClearAllPoints();
if self:GetCenter() > (UIParent:GetWidth() / 2) then
GameTooltip:SetOwner(self, "ANCHOR_LEFT");
GameTooltipIcon:SetPoint("TOPRIGHT", GameTooltip, "TOPLEFT", 0, 0);
GameTooltipModel:SetPoint("TOPRIGHT", GameTooltip, "TOPLEFT", 0, 0);
else
GameTooltip:SetOwner(self, "ANCHOR_RIGHT");
GameTooltipIcon:SetPoint("TOPLEFT", GameTooltip, "TOPRIGHT", 0, 0);
GameTooltipModel:SetPoint("TOPLEFT", GameTooltip, "TOPRIGHT", 0, 0);
end
-- NOTE: Order matters, we "fall-through" certain values in order to pass this information to the item ID section.
if not reference.creatureID then
if reference.itemID then
local link = reference.link;
if link and link ~= "" then
pcall(GameTooltip.SetHyperlink, GameTooltip, link);
else
GameTooltip:AddLine("Item #" .. reference.itemID);
if reference and reference.u then
local reason = L["UNOBTAINABLE_ITEM_REASONS"][reference.u];
if reason and (not reason[5] or app.GameBuildVersion < reason[5]) then GameTooltip:AddLine(reason[2], 1, 1, 1, true); end
end
if reference.e then
local reason = app.Modules.Events.GetEventTooltipNoteForGroup(reference);
if reason then
local left, right = strsplit(DESCRIPTION_SEPARATOR, reason);
if right then
GameTooltip:AddDoubleLine(left, right, 0.4, 0.8, 1, 0.4, 0.8, 1, 1);
else
GameTooltip:AddLine(left, 0.4, 0.8, 1, 1);
end
end
end
AttachTooltipSearchResults(GameTooltip, 1, "itemID:" .. reference.itemID, SearchForField, "itemID", reference.itemID);
end
elseif reference.currencyID then
GameTooltip:SetCurrencyByID(reference.currencyID, 1);
elseif reference.key ~= "questID" then
local link = reference.link;
if link then
pcall(GameTooltip.SetHyperlink, GameTooltip, link);
if reference.spellID then
local requireSkill = GetRelativeValue(reference, "requireSkill");
if requireSkill == 333 then
AttachTooltipSearchResults(GameTooltip, 1, "spellID:" .. reference.spellID, SearchForField, "spellID", reference.spellID);
elseif requireSkill == 960 then
GameTooltip:AddLine(GameTooltipTextLeft1:GetText(), 1, 1, 1, true);
GameTooltipTextLeft1:SetText(reference.name);
GameTooltip:Show();
end
end
end
end
end
-- Miscellaneous fields
if GameTooltip:NumLines() < 1 then
GameTooltip:AddLine(reference.text);
end
if app.Settings:GetTooltipSetting("Progress") then
if reference.trackable and reference.total and reference.total >= 2 then
GameTooltip:AddDoubleLine("Tracking Progress", GetCompletionText(reference.saved));
end
end
-- Relative ATT location
if reference.parent and not reference.itemID then
if reference.parent.parent then
GameTooltip:AddDoubleLine(reference.parent.parent.text or RETRIEVING_DATA, reference.parent.text or RETRIEVING_DATA);
else
--GameTooltip:AddLine(reference.parent.text or RETRIEVING_DATA, 1, 1, 1);
end
end
local linesByText = {};
for i=1,GameTooltip:NumLines() do
linesByText[_G["GameTooltipTextLeft"..i]:GetText()] = true;
end
local title = reference.title;
if title then
local left, right = strsplit(DESCRIPTION_SEPARATOR, title);
if right then
GameTooltip:AddDoubleLine(left, right, 1, 1, 1);
else
GameTooltip:AddLine(title, 1, 1, 1);
end
elseif reference.retries then
GameTooltip:AddLine("Failed to acquire information. This quest may have been removed from the game. " .. tostring(reference.retries), 1, 1, 1);
end
if reference.lvl then
local minlvl, maxlvl;
if type(reference.lvl) == "table" then
minlvl = reference.lvl[1] or 0;
maxlvl = reference.lvl[2] or 0;
else
minlvl = reference.lvl;
end
-- i suppose a maxlvl of 1 might exist?
if maxlvl and maxlvl > 0 then
GameTooltip:AddDoubleLine(L["REQUIRES_LEVEL"], tostring(minlvl) .. " to " .. tostring(maxlvl));
-- no point to show 'requires lvl 1'
elseif minlvl and minlvl > 1 then
GameTooltip:AddDoubleLine(L["REQUIRES_LEVEL"], tostring(minlvl));
end
end
if reference.b and app.Settings:GetTooltipSetting("binding") then GameTooltip:AddDoubleLine("Binding", tostring(reference.b)); end
if reference.requireSkill then GameTooltip:AddDoubleLine(L["REQUIRES"], GetSpellInfo(app.SkillIDToSpellID[reference.requireSkill] or 0) or RETRIEVING_DATA); end
if reference.f and reference.f > 0 and app.Settings:GetTooltipSetting("filterID") then
if reference.filterForRWP then
GameTooltip:AddDoubleLine(L["FILTER_ID"], tostring(L["FILTER_ID_TYPES"][reference.f]) .. " -> " .. tostring(L["FILTER_ID_TYPES"][reference.filterForRWP]));
else
GameTooltip:AddDoubleLine(L["FILTER_ID"], tostring(L["FILTER_ID_TYPES"][reference.f]));
end
end
if reference.achievementID and app.Settings:GetTooltipSetting("achievementID") then
GameTooltip:AddDoubleLine(L["ACHIEVEMENT_ID"], tostring(reference.achievementID));
if reference.sourceQuests and not (GetCategoryInfo and GetCategoryInfo(92) ~= "") then
GameTooltip:AddLine("This achievement has associated quests that can be completed before the introduction of the Achievement system coming with the Wrath Prepatch. Not all achievements can be tracked this way, but for those that can, they will be displayed. All other non-trackable achievements will be activated with the prepatch.", 0.4, 0.8, 1, true);
end
end
if app.Settings:GetTooltipSetting("creatureID") then
if reference.creatureID then
GameTooltip:AddDoubleLine(L["CREATURE_ID"], tostring(reference.creatureID));
elseif reference.npcID then
GameTooltip:AddDoubleLine(L["NPC_ID"], tostring(reference.npcID));
end
end
if reference.factionID and app.Settings:GetTooltipSetting("factionID") then GameTooltip:AddDoubleLine(L["FACTION_ID"], tostring(reference.factionID)); end
if reference.minReputation and not reference.maxReputation then
local standingId, offset = app.GetFactionStanding(reference.minReputation[2])
local msg = "Requires a minimum standing of"
if offset ~= 0 then msg = msg .. " " .. offset end
msg = msg .. " " .. app.GetFactionStandingText(standingId) .. " with " .. (GetFactionInfoByID(reference.minReputation[1]) or "the opposite faction") .. "."
GameTooltip:AddLine(msg);
end
if reference.maxReputation and not reference.minReputation then
local standingId, offset = app.GetFactionStanding(reference.maxReputation[2])
local msg = "Requires a standing lower than"
if offset ~= 0 then msg = msg .. " " .. offset end
msg = msg .. " " .. app.GetFactionStandingText(standingId) .. " with " .. (GetFactionInfoByID(reference.maxReputation[1]) or "the opposite faction") .. "."
GameTooltip:AddLine(msg);
end
if reference.minReputation and reference.maxReputation then
local minStandingId, minOffset = app.GetFactionStanding(reference.minReputation[2])
local maxStandingId, maxOffset = app.GetFactionStanding(reference.maxReputation[2])
if reference.maxReputation[1] == reference.minReputation[1] then
local msg = "Requires a standing between"
if minOffset ~= 0 then msg = msg .. " " .. minOffset end
msg = msg .. " " .. app.GetFactionStandingText(minStandingId) .. " and"
if maxOffset ~= 0 then msg = msg .. " " .. maxOffset end
msg = msg .. " " .. app.GetFactionStandingText(maxStandingId) .. " with " .. (GetFactionInfoByID(reference.minReputation[1]) or "the opposite faction") .. ".";
GameTooltip:AddLine(msg);
else
local msg = "Requires a minimum standing of"
if minOffset ~= 0 then msg = msg .. " " .. minOffset end
msg = msg .. " " .. app.GetFactionStandingText(minStandingId) .. " with " .. (GetFactionInfoByID(reference.minReputation[1]) or "the opposite faction") .. "."
GameTooltip:AddLine(msg);
msg = "Requires a standing lower than"
if maxOffset ~= 0 then msg = msg .. " " .. maxOffset end
msg = msg .. " " .. app.GetFactionStandingText(maxStandingId) .. " with " .. (GetFactionInfoByID(reference.maxReputation[1]) or "the opposite faction") .. "."
GameTooltip:AddLine(msg);
end
end
if reference.illusionID and app.Settings:GetTooltipSetting("illusionID") then GameTooltip:AddDoubleLine(L["ILLUSION_ID"], tostring(reference.illusionID)); end
if reference.objectID and app.Settings:GetTooltipSetting("objectID") then GameTooltip:AddDoubleLine(L["OBJECT_ID"], tostring(reference.objectID)); end
if reference.speciesID and app.Settings:GetTooltipSetting("speciesID") then GameTooltip:AddDoubleLine(L["SPECIES_ID"], tostring(reference.speciesID)); end
if reference.spellID then
if app.Settings:GetTooltipSetting("spellID") then GameTooltip:AddDoubleLine(L["SPELL_ID"], tostring(reference.spellID) .. " (" .. (app.GetSpellName(reference.spellID, reference.rank) or "??") .. ")"); end
-- If the item is a recipe, then show which characters know this recipe.
if not reference.collectible and app.Settings:GetTooltipSetting("KnownBy") then
local knownBy = {};
for _,character in pairs(ATTCharacterData) do
if character.ActiveSkills and not character.ignored then
local skills = character.ActiveSkills[reference.spellID];
if skills then tinsert(knownBy, { character, skills[1], skills[2] }); end
end
end
if #knownBy > 0 then
app.Sort(knownBy, function(a, b)
return a[2] > b[2];
end);
GameTooltip:AddLine("|c" .. app.Colors.TooltipDescription .. "Known by:|r");
for i,data in ipairs(knownBy) do
local character = data[1];
GameTooltip:AddDoubleLine(" " .. string.gsub(character and character.text or "???", "-" .. GetRealmName(), ""), data[2] .. " / " .. data[3]);
end
end
end
end
if reference.flightPathID and app.Settings:GetTooltipSetting("flightPathID") then GameTooltip:AddDoubleLine(L["FLIGHT_PATH_ID"], tostring(reference.flightPathID)); end
if reference.mapID and app.Settings:GetTooltipSetting("mapID") then GameTooltip:AddDoubleLine(L["MAP_ID"], tostring(reference.mapID)); end
if reference.explorationID and app.Settings:GetTooltipSetting("explorationID") then GameTooltip:AddDoubleLine(L["EXPLORATION_ID"], tostring(reference.explorationID)); end
if reference.artID and app.Settings:GetTooltipSetting("artID") then GameTooltip:AddDoubleLine(L["ART_ID"], tostring(reference.artID)); end
--if reference.hash then GameTooltip:AddDoubleLine("Hash", tostring(reference.hash)); end
if reference.coords and app.Settings:GetTooltipSetting("Coordinates") then
local currentMapID, j, str = app.CurrentMapID, 0;
for i,coord in ipairs(reference.coords) do
local x, y = coord[1], coord[2];
local mapID = coord[3] or currentMapID;
if mapID ~= currentMapID then
str = app.GetMapName(mapID) or "??";
if app.Settings:GetTooltipSetting("mapID") then
str = str .. " (" .. mapID .. ")";
end
str = str .. ": ";
else
str = "";
end
GameTooltip:AddDoubleLine(j == 0 and "Coordinates" or " ",
str.. GetNumberWithZeros(math.floor(x * 10) * 0.1, 1) .. ", " .. GetNumberWithZeros(math.floor(y * 10) * 0.1, 1), 1, 1, 1, 1, 1, 1);
j = j + 1;
if j > 8 then
break;
end
end
end
if reference.coord and app.Settings:GetTooltipSetting("Coordinates") then
GameTooltip:AddDoubleLine("Coordinate",
GetNumberWithZeros(math.floor(reference.coord[1] * 10) * 0.1, 1) .. ", " ..
GetNumberWithZeros(math.floor(reference.coord[2] * 10) * 0.1, 1), 1, 1, 1, 1, 1, 1);
end
if reference.providers then
local counter = 0;
for i,provider in pairs(reference.providers) do
local providerType = provider[1];
local providerID = provider[2] or 0;
local providerString = UNKNOWN;
if providerType == "o" then
providerString = app.ObjectNames[providerID] or reference.text or ("Object: " .. RETRIEVING_DATA)
if app.Settings:GetTooltipSetting("objectID") then
providerString = providerString .. ' (' .. providerID .. ')';
end
elseif providerType == "n" then
providerString = (providerID > 0 and app.NPCNameFromID[providerID]) or ("Creature: " .. RETRIEVING_DATA)
if app.Settings:GetTooltipSetting("creatureID") then
providerString = providerString .. ' (' .. providerID .. ')';
end
elseif providerType == "i" then
local _,name,_,_,_,_,_,_,_,icon = GetItemInfo(providerID);
providerString = (icon and ("|T" .. icon .. ":0|t") or "") .. (name or ("Item: " .. RETRIEVING_DATA));
if app.Settings:GetTooltipSetting("itemID") then
providerString = providerString .. ' (' .. providerID .. ')';
end
end
GameTooltip:AddDoubleLine(counter == 0 and "Provider(s)" or " ", providerString);
counter = counter + 1;
end
end
if not reference.itemID then
if reference.lore and app.Settings:GetTooltipSetting("Lore") then
local lore = reference.lore;
if not linesByText[lore] then
local r,g,b = HexToRGB(app.Colors.TooltipLore);
GameTooltip:AddLine(lore, r, g, b, 1);
end
end
local description = reference.description;
if description and app.Settings:GetTooltipSetting("Descriptions") then
if not linesByText[description] then
local r,g,b = HexToRGB(app.Colors.TooltipDescription);
GameTooltip:AddLine(description, r, g, b, 1);
end
if reference.maps then
local description,maps,umaps,name = "Maps: ",{},{};
for i=1,#reference.maps,1 do
name = app.GetMapName(reference.maps[i]);
if name and not umaps[name] then
umaps[name] = true;
tinsert(maps, name);
end
end
for i,name in ipairs(maps) do
if i > 1 then description = description .. ", "; end
description = description .. name;
end
GameTooltip:AddLine(" ", 1, 1, 1, 1);
local r,g,b = HexToRGB(app.Colors.TooltipDescription);
GameTooltip:AddLine(description, r, g, b, 1);
end
elseif reference.maps then
local description,maps,umaps,name = "Maps: ",{},{};
for i=1,#reference.maps,1 do
name = app.GetMapName(reference.maps[i]);
if name and not umaps[name] then
umaps[name] = true;
tinsert(maps, name);
end
end
for i,name in ipairs(maps) do
if i > 1 then description = description .. ", "; end
description = description .. name;
end
local r,g,b = HexToRGB(app.Colors.TooltipDescription);
GameTooltip:AddLine(description, r, g, b, 1);
end
if reference.nextEvent then
local timeStrings = app.Modules.Events.GetEventTimeStrings(reference.nextEvent);
if timeStrings then
local r,g,b = HexToRGB(app.Colors.TooltipDescription);
for i,timeString in ipairs(timeStrings) do
local left, right = strsplit(DESCRIPTION_SEPARATOR, timeString);
if right then
GameTooltip:AddDoubleLine(left, right, r, g, b, r, g, b, 1);
else
GameTooltip:AddLine(left, r, g, b, 1);
end
end
end
end
local awp, rwp = GetRelativeValue(reference, "awp"), reference.rwp;
if rwp then
local rwpString = GetRemovedWithPatchString(rwp);
if not linesByText[rwpString] then
local r,g,b = HexToRGB(app.Colors.RemovedWithPatch);
GameTooltip:AddLine(rwpString, r, g, b, 1);
end
end
if awp and ((rwp or (reference.u and reference.u < 3)) or awp >= app.GameBuildVersion) then
local awpString = GetAddedWithPatchString(awp, awp and rwp and awp > rwp);
if awpString and not linesByText[awpString] then
local r,g,b = HexToRGB(app.Colors.AddedWithPatch);
GameTooltip:AddLine(awpString, r, g, b, 1);
end
end
if reference.questID and not reference.objectiveID then
app.AddQuestObjectivesToTooltip(GameTooltip, reference);
end
if reference.u then
local reason = L["UNOBTAINABLE_ITEM_REASONS"][reference.u];
if reason and (not reason[5] or app.GameBuildVersion < reason[5]) then GameTooltip:AddLine(reason[2], 1, 1, 1, true); end
end
if reference.e then
local reason = app.Modules.Events.GetEventTooltipNoteForGroup(reference);
if reason then
local left, right = strsplit(DESCRIPTION_SEPARATOR, reason);
if right then
GameTooltip:AddDoubleLine(left, right, 0.4, 0.8, 1, 0.4, 0.8, 1, 1);
else
GameTooltip:AddLine(left, 0.4, 0.8, 1, 1);
end
end
end
if reference.sym then GameTooltip:AddLine("Right click to view more information.", 0.8, 0.8, 1, true); end
end
if reference.titleID then
if app.Settings:GetTooltipSetting("titleID") then GameTooltip:AddDoubleLine(L["TITLE_ID"], tostring(reference.titleID)); end
AttachTooltipSearchResults(GameTooltip, 1, "titleID:" .. reference.titleID, SearchForField, "titleID", reference.titleID);
end
if reference.questID and app.Settings:GetTooltipSetting("questID") then GameTooltip:AddDoubleLine(L["QUEST_ID"], tostring(reference.questID)); end
if reference.qgs and app.Settings:GetTooltipSetting("QuestGivers") then
if app.Settings:GetTooltipSetting("creatureID") then
for i,qg in ipairs(reference.qgs) do
GameTooltip:AddDoubleLine(i == 1 and L["QUEST_GIVER"] or " ", tostring(qg > 0 and app.NPCNameFromID[qg] or "") .. " (" .. qg .. ")");
end
else
for i,qg in ipairs(reference.qgs) do
GameTooltip:AddDoubleLine(i == 1 and L["QUEST_GIVER"] or " ", tostring(qg > 0 and app.NPCNameFromID[qg] or qg));
end
end
end
if reference.crs then
if app.Settings:GetTooltipSetting("creatureID") then
for i,cr in ipairs(reference.crs) do
GameTooltip:AddDoubleLine(i == 1 and CREATURE or " ", tostring(cr > 0 and app.NPCNameFromID[cr] or "") .. " (" .. cr .. ")");
end
else
for i,cr in ipairs(reference.crs) do
GameTooltip:AddDoubleLine(i == 1 and CREATURE or " ", tostring(cr > 0 and app.NPCNameFromID[cr] or cr));
end
end
end
if reference.c and app.Settings:GetTooltipSetting("ClassRequirements") then
local str = "";
for i,cl in ipairs(reference.c) do
local info = C_CreatureInfo.GetClassInfo(cl);
if info then
if i > 1 then str = str .. ", "; end
str = str .. info.className;
end
end
GameTooltip:AddDoubleLine("Classes", str);
end
if app.Settings:GetTooltipSetting("RaceRequirements") then
if reference.races then
local str = "";
for i,race in ipairs(reference.races) do
local info = C_CreatureInfo.GetRaceInfo(race);
if info then
if i > 1 then str = str .. ", "; end
str = str .. info.raceName;
end
end
GameTooltip:AddDoubleLine("Races", str);
elseif reference.r and reference.r > 0 then
GameTooltip:AddDoubleLine("Races", (reference.r == 2 and ITEM_REQ_ALLIANCE) or (reference.r == 1 and ITEM_REQ_HORDE) or "Unknown");
end
end
if reference.isDaily then GameTooltip:AddLine("This can be completed daily.");
elseif reference.isWeekly then GameTooltip:AddLine("This can be completed weekly.");
elseif reference.isMontly then GameTooltip:AddLine("This can be completed monthly.");
elseif reference.isYearly then GameTooltip:AddLine("This can be completed yearly.");
elseif reference.repeatable then GameTooltip:AddLine("This can be repeated multiple times."); end
if reference.pvp and not reference.itemID then GameTooltip:AddLine(L["REQUIRES_PVP"], 1, 1, 1, 1, true); end
if not GameTooltipModel:TrySetModel(reference) then
local texture = reference.preview or reference.icon;
if texture then
if reference.explorationID and reference.maphash and reference.preview then
local width, height, offsetX, offsetY = strsplit(":", reference.maphash);
GameTooltipIcon:SetSize(tonumber(width) or 72,tonumber(height) or 72);
else
GameTooltipIcon:SetSize(72,72);
end
GameTooltipIcon.icon:SetTexture(texture);
local texcoord = reference.texcoord;
if texcoord then
GameTooltipIcon.icon:SetTexCoord(texcoord[1], texcoord[2], texcoord[3], texcoord[4]);
else
GameTooltipIcon.icon:SetTexCoord(0, 1, 0, 1);
end
GameTooltipIcon:Show();
end
end
if reference.displayID and app.Settings:GetTooltipSetting("displayID") then
GameTooltip:AddDoubleLine("Display ID", reference.displayID);
end
if reference.modelID and app.Settings:GetTooltipSetting("modelID") then
GameTooltip:AddDoubleLine("Model ID", reference.modelID);
end
if reference.cost then
if type(reference.cost) == "table" then
local _, name, icon, amount;
for k,v in pairs(reference.cost) do
_ = v[1];
if _ == "g" then
GameTooltip:AddDoubleLine(k == 1 and "Cost" or " ", GetCoinTextureString(v[2]));
else
if _ == "i" then
local item = app.CreateItem(v[2]);
name = item.name;
icon = item.icon;
elseif _ == "c" then
local currency = app.CreateCurrencyClass(v[2]);
name = currency.text;
icon = currency.icon;
end
name = (icon and ("|T" .. icon .. ":0|t") or "") .. (name or RETRIEVING_DATA);
_ = (v[3] or 1);
if _ > 1 then
name = _ .. "x " .. name;
end
GameTooltip:AddDoubleLine(k == 1 and "Cost" or " ", name);
end
end
else
GameTooltip:AddDoubleLine("Cost", GetCoinTextureString(reference.cost));
end
end
if app.Settings:GetTooltipSetting("Progress") then
local right = (app.Settings:GetTooltipSetting("ShowIconOnly") and GetProgressTextForRow or GetProgressTextForTooltip)(reference);
if right and right ~= "" and right ~= "---" then
GameTooltipTextRight1:SetText(right);
GameTooltipTextRight1:Show();
end
end
-- Show Breadcrumb information
if reference.isBreadcrumb then
GameTooltip:AddLine("This is a breadcrumb quest.");
end
GameTooltip:AddDoubleLine("Type", reference.__type);
-- Show lockout information about an Instance (Raid or Dungeon)
local locks = reference.locks;
if locks then
if locks.encounters then
GameTooltip:AddDoubleLine("Resets", date("%c", locks.reset));
for encounterIter,encounter in pairs(locks.encounters) do
GameTooltip:AddDoubleLine(" " .. encounter.name, GetCompletionIcon(encounter.isKilled));
end
else
if reference.isLockoutShared and locks.shared then
GameTooltip:AddDoubleLine("Shared", date("%c", locks.shared.reset));
for encounterIter,encounter in pairs(locks.shared.encounters) do
GameTooltip:AddDoubleLine(" " .. encounter.name, GetCompletionIcon(encounter.isKilled));
end
else
for key,value in pairs(locks) do
if key == "shared" then
-- Skip
else
GameTooltip:AddDoubleLine(Colorize(GetDifficultyInfo(key), app.DifficultyColors[key] or app.Colors.DefaultDifficulty), date("%c", value.reset));
for encounterIter,encounter in pairs(value.encounters) do
GameTooltip:AddDoubleLine(" " .. encounter.name, GetCompletionIcon(encounter.isKilled));
end
end
end
end
end
end
if reference.OnTooltip then reference:OnTooltip(); end
if reference.questID and app.Settings:GetTooltipSetting("SummarizeThings") then
if not reference.repeatable and app.Settings:GetTooltipSetting("Show:OtherCharacterQuests") then
local incompletes, realmName = {}, GetRealmName();
for guid,character in pairs(ATTCharacterData) do
if not character.ignored and character.realm == realmName
and (not reference.r or (character.factionID and reference.r == character.factionID))
and (not reference.races or (character.raceID and contains(reference.races, character.raceID)))
and (not reference.c or (character.classID and contains(reference.c, character.classID)))
and (character.Quests and not character.Quests[reference.questID]) then
incompletes[guid] = character;
end
end
incompletes[app.GUID] = nil;
local desc, j = "", 0;
for guid,character in pairs(incompletes) do
if j > 0 then desc = desc .. ", "; end
desc = desc .. (character.text or guid);
j = j + 1;
end
if j > 0 then
GameTooltip:AddLine("Incomplete on " .. string.gsub(desc, "-" .. realmName, ""), 1, 1, 1, true);
end
end
end
-- Show Quest Prereqs
local isDebugMode = app.Settings:Get("DebugMode");
if reference.sourceQuests and (isDebugMode or not reference.saved) then
local currentMapID, prereqs, bc = app.CurrentMapID, {}, {};
for i,sourceQuestID in ipairs(reference.sourceQuests) do
if sourceQuestID > 0 and (isDebugMode or not IsQuestFlaggedCompleted(sourceQuestID)) then
local sqs = SearchForField("questID", sourceQuestID);
if #sqs > 0 then
local bestMatch = nil;
for j,sq in ipairs(sqs) do
if sq.questID == sourceQuestID and not sq.objectiveID then
if isDebugMode or (app.RecursiveClassAndRaceFilter(sq) and not IsQuestFlaggedCompleted(sourceQuestID)) then
if sq.sourceQuests then
-- Always prefer the source quest with additional source quest data.
bestMatch = sq;
elseif not sq.itemID and (not bestMatch or not bestMatch.sourceQuests) then
-- Otherwise try to find the version of the quest that isn't an item.
bestMatch = sq;
end
end
end
end
if bestMatch then
if bestMatch.isBreadcrumb then
tinsert(bc, bestMatch);
else
tinsert(prereqs, bestMatch);
end
end
else
tinsert(prereqs, app.CreateQuest(sourceQuestID));
end
end
end
if prereqs and #prereqs > 0 then
GameTooltip:AddLine("This quest has an incomplete prerequisite quest that you need to complete first.");
for i,prereq in ipairs(prereqs) do
local text = " " .. prereq.questID .. ": " .. (prereq.text or RETRIEVING_DATA);
local mapID = GetBestMapForGroup(prereq, currentMapID);
if mapID and mapID ~= currentMapID then text = text .. " (" .. app.GetMapName(mapID) .. ")"; end
GameTooltip:AddDoubleLine(text, GetCompletionIcon(IsQuestFlaggedCompleted(prereq.questID)));
end
end
if bc and #bc > 0 then
GameTooltip:AddLine("This quest has a breadcrumb quest that you may be unable to complete after completing this one.");
for i,prereq in ipairs(bc) do
local text = " " .. prereq.questID .. ": " .. (prereq.text or RETRIEVING_DATA);
local mapID = GetBestMapForGroup(prereq, currentMapID);
if mapID and mapID ~= currentMapID then text = text .. " (" .. app.GetMapName(mapID) .. ")"; end
GameTooltip:AddDoubleLine(text, GetCompletionIcon(IsQuestFlaggedCompleted(prereq.questID)));
end
end
end
if reference.g then
-- If we're at the Auction House
if (AuctionFrame and AuctionFrame:IsShown()) or (AuctionHouseFrame and AuctionHouseFrame:IsShown()) then
GameTooltip:AddLine(L[(self.index > 0 and "OTHER_ROW_INSTRUCTIONS_AH") or "TOP_ROW_INSTRUCTIONS_AH"], 1, 1, 1);
else
GameTooltip:AddLine(L[(self.index > 0 and "OTHER_ROW_INSTRUCTIONS") or "TOP_ROW_INSTRUCTIONS"], 1, 1, 1);
end
end
GameTooltip:Show();
end
end
local function RowOnLeave(self)
if GameTooltip then
GameTooltip:ClearLines();
GameTooltip:Hide();
GameTooltipIcon.icon.Background:Hide();
GameTooltipIcon.icon.Border:Hide();
GameTooltipIcon:Hide();
GameTooltipModel:Hide();
end
end
CreateRow = function(self)
local row = CreateFrame("Button", nil, self);
row.index = #self.rows;
if row.index == 0 then
-- This means relative to the parent.
row:SetPoint("TOPLEFT");
row:SetPoint("TOPRIGHT");
else
-- This means relative to the row above this one.
row:SetPoint("TOPLEFT", self.rows[row.index], "BOTTOMLEFT");
row:SetPoint("TOPRIGHT", self.rows[row.index], "BOTTOMRIGHT");
end
tinsert(self.rows, row);
-- Setup highlighting and event handling
row:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD");
row:RegisterForClicks("LeftButtonDown","RightButtonDown");
row:SetScript("OnClick", RowOnClick);
row:SetScript("OnEnter", RowOnEnter);
row:SetScript("OnLeave", RowOnLeave);
row:EnableMouse(true);
-- Label is the text information you read.
row.Label = row:CreateFontString(nil, "ARTWORK", "GameFontNormal");
row.Label:SetJustifyH("LEFT");
row.Label:SetPoint("BOTTOM");
row.Label:SetPoint("TOP");
row:SetHeight(select(2, row.Label:GetFont()) + 4);
-- Summary is the completion summary information. (percentage text)
row.Summary = row:CreateFontString(nil, "ARTWORK", "GameFontNormal");
row.Summary:SetJustifyH("CENTER");
row.Summary:SetPoint("BOTTOM");
row.Summary:SetPoint("RIGHT");
row.Summary:SetPoint("TOP");
-- Background is used by the Map Highlight functionality.
row.Background = row:CreateTexture(nil, "BACKGROUND");
row.Background:SetPoint("LEFT", 4, 0);
row.Background:SetPoint("BOTTOM");
row.Background:SetPoint("RIGHT");
row.Background:SetPoint("TOP");
row.Background:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight");
-- Indicator is used by the Instance Saves functionality.
row.Indicator = row:CreateTexture(nil, "ARTWORK");
row.Indicator:SetPoint("BOTTOM");
row.Indicator:SetPoint("TOP");
row.Indicator:SetWidth(row:GetHeight());
-- Texture is the icon.
row.Texture = row:CreateTexture(nil, "ARTWORK");
row.Texture:SetPoint("BOTTOM");
row.Texture:SetPoint("TOP");
row.Texture:SetWidth(row:GetHeight());
row.Texture.Background = row:CreateTexture(nil, "BACKGROUND");
row.Texture.Background:SetPoint("BOTTOM");
row.Texture.Background:SetPoint("TOP");
row.Texture.Background:SetWidth(row:GetHeight());
row.Texture.Border = row:CreateTexture(nil, "BORDER");
row.Texture.Border:SetPoint("BOTTOM");
row.Texture.Border:SetPoint("TOP");
row.Texture.Border:SetWidth(row:GetHeight());
-- Clear the Row Data Initially
SetRowData(self, row, nil);
return row;
end
-- Window Creation
app.Windows = {};
local defaultNoEntriesRow = {
text = "No data was found.",
preview = app.asset("Discord_2_128"),
description = "If you believe this was in error, try activating 'Debug Mode'. One of your filters may be restricting the visibility of the group.",
};
local AllWindowSettings;
local function ApplySettingsForWindow(self, windowSettings)
if windowSettings.scale then self:SetScale(windowSettings.scale); end
if windowSettings.movable then
self:ClearAllPoints();
if windowSettings.x then
self:SetPoint(windowSettings.point or "CENTER", windowSettings.relativeTo or UIParent, windowSettings.relativePoint or "CENTER", windowSettings.x, windowSettings.y);
else
self:SetPoint("CENTER", UIParent, "CENTER");
end
end
if windowSettings.width then
self:SetSize(windowSettings.width, windowSettings.height);
end
self:SetMovable(windowSettings.movable);
self:SetResizable(windowSettings.resizable);
self:SetVisible(windowSettings.visible);
end
local function BuildSettingsForWindow(self, windowSettings)
local scale = self:GetScale();
if scale then
windowSettings.scale = scale;
local point, relativeTo, relativePoint, xOfs, yOfs = self:GetPoint()
if xOfs then
windowSettings.width = self:GetWidth();
windowSettings.height = self:GetHeight();
windowSettings.x = xOfs;
windowSettings.y = yOfs;
windowSettings.point = point;
windowSettings.relativePoint = relativePoint;
windowSettings.relativeTo = relativeTo and relativeTo:GetName();
end
else
windowSettings.scale = 1;
windowSettings.x = 0;
windowSettings.y = 0;
end
windowSettings.visible = not not self:IsVisible();
windowSettings.movable = not not self:IsMovable();
windowSettings.resizable = not not self:IsResizable();
end
local function ClearSettingsForWindow(self)
if not AllWindowSettings then return; end
AllWindowSettings[self.Suffix] = nil;
end
local function LoadSettingsForWindow(self)
if not AllWindowSettings then return; end
local name = self.Suffix;
local settings = AllWindowSettings[name];
if not settings then
settings = {};
AllWindowSettings[name] = settings;
end
self.Settings = settings;
self:Load();
end
local function LoadSettingsForWindows(windowSettings)
if AllWindowSettings then
return;
end
AllWindowSettings = windowSettings;
local dynamicWindows = {};
for name, settings in pairs(windowSettings) do
if settings.dynamic then
if settings.visible then
dynamicWindows[name] = settings;
else
windowSettings[name] = nil;
end
end
end
-- Load all of the windows other than Prime.
local primeWindow = app.Windows.Prime;
app.Windows.Prime = nil;
for name, window in pairs(app.Windows) do
LoadSettingsForWindow(window);
dynamicWindows[name] = nil;
end
-- Okay, now load Prime last.
app.Windows.Prime = primeWindow;
LoadSettingsForWindow(primeWindow);
dynamicWindows.Prime = nil;
for name,settings in pairs(dynamicWindows) do
settings.visible = false;
app:CreateMiniListFromSource(settings.key, settings.id, settings.sourcePath);
end
end
app:RegisterEvent("PLAYER_LOGOUT");
app.events.PLAYER_LOGOUT = function()
for _, window in pairs(app.Windows) do
window:Save();
end
end;
local function SetWindowData(self, data)
if self.data ~= data then
self.data = data;
self:DelayedRebuild();
end
end
local function ProcessGroup(data, object)
if app.VisibilityFilter(object) then
data[#data + 1] = object;
if object.g and object.expanded then
for i=1,#object.g,1 do
ProcessGroup(data, object.g[i]);
end
end
end
end
local function UpdateWindow(self, force, trigger)
-- If this window doesn't have data, do nothing.
local data = self.data;
if not data then
self:DelayedRebuild();
return;
end
if not self.rowData then
self.rowData = {};
else
wipe(self.rowData);
end
self.forceFullDataRefresh = self.forceFullDataRefresh or force or trigger;
if force or self:IsShown() then
data.expanded = true;
if self.forceFullDataRefresh then
local rows = self.Container.rows;
for i=1,#rows,1 do
SetRowData(self, rows[i], nil);
end
data.progress = 0;
data.total = 0;
--local lastUpdate = GetTimePreciseSec();
if not (data.OnUpdate and data:OnUpdate()) then
UpdateGroups(data, data.g);
-- Check to see if the data changed.
if self.data ~= data then
print(self.Suffix, app.Colorize("DATA CHANGED!", RGBToHex(255, 120, 120)));
return;
end
else
print(self.Suffix, "Returned true!!! Skipping the rest of this UpdateWindows call");
return;
end
self.forceFullDataRefresh = nil;
--print("UpdateGroups RESULT", (GetTimePreciseSec() - lastUpdate) * 10000);
end
ProcessGroup(self.rowData, data);
-- Does this user have everything?
if data.total and data.total > 0 then
if data.total <= data.progress then
if #self.rowData < 1 then
data.back = 1;
tinsert(self.rowData, data);
end
if self.missingData then
self.missingData = nil;
--print("UNSETTING MISSING DATA", trigger, self.AllowCompleteSound);
if trigger and self.AllowCompleteSound then
--print("PLAY COMPLETE SOUND", self.data.text);
app:PlayCompleteSound();
end
end
if not self.ignoreNoEntries then
local noentries = self.noEntriesRow or defaultNoEntriesRow;
noentries.parent = self.data;
tinsert(self.rowData, noentries);
end
else
self.missingData = true;
end
else
self.missingData = nil;
end
return true;
end
end
local function UpdateWindowColor(self, suffix)
self:SetBackdropBorderColor(1, 1, 1, 1);
self:SetBackdropColor(0, 0, 0, 1);
end
if app.Debugging then
function app:RedrawWindows(source)
app:StartATTCoroutine("RedrawWindows", function()
coroutine.yield();
coroutine.yield();
print("RedrawWindows: ", source);
local lastUpdate = GetTimePreciseSec();
for name, window in pairs(app.Windows) do
window:Redraw();
end
print("RedrawWindows: ", (GetTimePreciseSec() - lastUpdate) * 10000);
end);
end
function app:RefreshWindows(source)
print("RefreshWindows: ", source);
local lastUpdate = GetTimePreciseSec();
for name, window in pairs(app.Windows) do
window:Refresh();
end
print("RefreshWindows: ", (GetTimePreciseSec() - lastUpdate) * 10000);
end
function app:UpdateWindows(source, force, trigger)
print("UpdateWindows: ", source, force, trigger);
if trigger then trigger = source; end
local lastUpdate = GetTimePreciseSec();
for name, window in pairs(app.Windows) do
window:Update(force, trigger);
end
print("UpdateWindows: ", (GetTimePreciseSec() - lastUpdate) * 10000);
end
else
function app:RedrawWindows(source)
for name, window in pairs(app.Windows) do
window:Redraw();
end
end
function app:RefreshWindows(source)
for name, window in pairs(app.Windows) do
window:Refresh();
end
end
function app:UpdateWindows(source, force, trigger)
if trigger then trigger = source; end
for name, window in pairs(app.Windows) do
local window_oldUpdate = window.Update;
window.UpdatePending = true;
window.Update = function(self, ...)
local result = window_oldUpdate(self, ...);
self.Update = window_oldUpdate;
self.UpdatePending = nil;
return result;
end
end
for name, window in pairs(app.Windows) do
if window.UpdatePending then
window:Update(force, trigger);
end
end
end
end
function app:UpdateWindowColors()
for suffix, window in pairs(app.Windows) do
UpdateWindowColor(window, suffix);
end
end
local refreshDataCooldown = 5;
local refreshFromTrigger;
local currentlyRefreshingData = false;
local function RefreshData(source, trigger)
wipe(searchCache);
refreshDataCooldown = 5;
if trigger then
--print("REFRESH_DATA", source, trigger);
trigger = source;
end
refreshFromTrigger = refreshFromTrigger or trigger;
if currentlyRefreshingData then return; end
app:StartATTCoroutine("RefreshData", function()
currentlyRefreshingData = true;
-- While the player is in combat, wait for combat to end.
while InCombatLockdown() do coroutine.yield(); end
-- Wait 1/2 second. For multiple simultaneous requests, each one will reapply the delay.
while refreshDataCooldown > 0 do
refreshDataCooldown = refreshDataCooldown - 1;
coroutine.yield();
end
-- Send an Update to the Windows to Rebuild their Row Data
if app.forceFullDataRefresh then
app.forceFullDataRefresh = nil;
-- Execute the OnRecalculate handlers.
for i,handler in ipairs(app.EventHandlers.OnRecalculate) do
handler();
end
app:UpdateWindows(source, true, refreshFromTrigger);
else
app:UpdateWindows(source, nil, refreshFromTrigger);
end
refreshFromTrigger = nil;
-- Send a message to your party members.
local data = (app.CurrentCharacter and app.CurrentCharacter.PrimeData) or app:GetDataCache();
msg = "A\t" .. app.Version .. "\t" .. (data.progress or 0) .. "\t" .. (data.total or 0) .. "\t" .. data.modeString;
if app.lastProgressUpdateMessage ~= msg then
SendGroupMessage(msg);
SendGuildMessage(msg);
app.lastProgressUpdateMessage = msg;
end
wipe(searchCache);
currentlyRefreshingData = nil;
end);
end
function app:RefreshDataCompletely(source, trigger)
app.forceFullDataRefresh = true;
RefreshData("RefreshDataCompletely:" .. source, trigger);
end
function app:RefreshDataQuietly(source, trigger)
RefreshData("RefreshDataQuietly:" .. source, trigger);
end
local function SetWindowVisible(self, show)
if show then
self:Show();
else
self:Hide();
end
end
local function ToggleWindow(self)
self:SetVisible(not self:IsVisible());
end
local BuildCategory = function(self, headers, searchResults, inst)
local sources, header, headerType = {}, self;
for j,o in ipairs(searchResults) do
if not o.u or o.u ~= 1 then
MergeClone(sources, o);
if o.parent then
if not o.sourceQuests then
local questID = GetRelativeValue(o, "questID");
if questID then
if not inst.sourceQuests then
inst.sourceQuests = {};
end
if not contains(inst.sourceQuests, questID) then
tinsert(inst.sourceQuests, questID);
end
else
local sourceQuests = GetRelativeValue(o, "sourceQuests");
if sourceQuests then
if not inst.sourceQuests then
inst.sourceQuests = {};
for k,questID in ipairs(sourceQuests) do
tinsert(inst.sourceQuests, questID);
end
else
for k,questID in ipairs(sourceQuests) do
if not contains(inst.sourceQuests, questID) then
tinsert(inst.sourceQuests, questID);
end
end
end
end
end
end
if GetRelativeValue(o, "isHolidayCategory") then
headerType = "holiday";
elseif GetRelativeValue(o, "isPromotionCategory") then
headerType = "promo";
elseif GetRelativeValue(o, "isPVPCategory") then
headerType = "pvp";
elseif GetRelativeValue(o, "isEventCategory") then
headerType = "event";
elseif GetRelativeValue(o, "isWorldDropCategory") or o.parent.headerID == app.HeaderConstants.COMMON_BOSS_DROPS then
headerType = "drop";
elseif o.parent.npcID then
headerType = GetDeepestRelativeValue(o, "headerID") or o.parent.parent.headerID == app.HeaderConstants.VENDORS and app.HeaderConstants.VENDORS or "drop";
elseif GetRelativeValue(o, "isCraftedCategory") then
headerType = "crafted";
elseif o.parent.achievementID then
headerType = app.HeaderConstants.ACHIEVEMENTS;
else
headerType = GetDeepestRelativeValue(o, "headerID") or "drop";
if headerType == true then -- Seriously don't do this...
headerType = "drop";
end
end
local coords = GetRelativeValue(o, "coords");
if coords then
if not inst.coords then
inst.coords = { unpack(coords) };
else
for i,coord in ipairs(coords) do
tinsert(inst.coords, coord);
end
end
end
end
end
end
local count = #sources;
if count == 0 then return nil; end
if count == 1 then
for key,value in pairs(sources[1]) do
inst[key] = value;
end
elseif count > 1 then
-- Find the most accessible version of the thing.
app.Sort(sources, app.SortDefaults.Accessibility);
for key,value in pairs(sources[1]) do
inst[key] = value;
end
end
-- Determine the type of header to put the thing into.
if not headerType then headerType = "drop"; end
header = headers[headerType];
if not header then
if headerType == "holiday" then
header = app.CreateNPC(app.HeaderConstants.HOLIDAYS);
elseif headerType == "promo" then
header = {};
header.text = BATTLE_PET_SOURCE_8;
header.icon = app.asset("Category_Promo");
elseif headerType == "pvp" then
header = {};
header.text = PVP;
header.icon = app.asset("Category_PvP");
elseif headerType == "event" then
header = {};
header.text = BATTLE_PET_SOURCE_7;
header.icon = app.asset("Category_Event");
elseif headerType == "drop" then
header = {};
header.text = BATTLE_PET_SOURCE_1;
header.icon = app.asset("Category_WorldDrops");
elseif headerType == "crafted" then
header = {};
header.text = LOOT_JOURNAL_LEGENDARIES_SOURCE_CRAFTED_ITEM;
header.icon = app.asset("Category_Crafting");
elseif type(headerType) == "number" then
header = app.CreateNPC(headerType);
else
print("Unhandled Header Type", headerType);
end
if not headers[headerType] then
headers[headerType] = header;
tinsert(self.g, header);
header.parent = self;
header.g = {};
end
end
inst.parent = header;
inst.progress = nil;
inst.total = nil;
inst.g = nil;
MergeObject(header.g, inst);
return inst;
end
function app:GetWindow(suffix, settings)
local window = app.Windows[suffix];
if not window and settings then
-- Create the window instance.
window = CreateFrame("FRAME", nil, settings.parent or UIParent, BackdropTemplateMixin and "BackdropTemplate");
app.Windows[suffix] = window;
window.Suffix = suffix;
window.SetData = SetWindowData;
window.BuildCategory = BuildCategory;
window.AllowCompleteSound = settings.AllowCompleteSound;
window:SetScript("OnMouseDown", StartMovingOrSizing);
window:SetScript("OnMouseUp", StopMovingOrSizing);
window:SetScript("OnHide", StopMovingOrSizing);
window:SetBackdrop(backdrop);
window:SetClampedToScreen(true);
window:SetToplevel(true);
window:EnableMouse(true);
window:SetMovable(true);
window:SetResizable(true);
if window.SetResizeBounds then
window:SetResizeBounds(96, 32);
else
window:SetMinResize(96, 32);
end
window:SetSize(300, 300);
window:Hide();
if ATTClassicSettings then
if suffix == "Prime" then
window:SetScale(app.Settings:GetTooltipSetting("MainListScale"));
else
window:SetScale(app.Settings:GetTooltipSetting("MiniListScale"));
end
end
-- Whether or not to debug things
local debugging = settings.Debugging;--app.Debugging and window.Suffix == "Prime";
-- Load / Save, which allows windows to keep track of key pieces of information.
window.ClearSettings = ClearSettingsForWindow;
if not settings.IgnoreSettings then
local defaults = {};
BuildSettingsForWindow(window, defaults);
if settings.Defaults then
for key,value in pairs(settings.Defaults) do
defaults[key] = value;
end
end
window.Load = function(self)
local windowSettings = self.Settings;
if not windowSettings then
return;
end
setmetatable(windowSettings, { __index = defaults });
if settings.OnLoad then
settings.OnLoad(self, windowSettings);
end
ApplySettingsForWindow(self, windowSettings);
end
window.RecordSettings = function(self)
local windowSettings = self.Settings;
if windowSettings then
BuildSettingsForWindow(self, windowSettings);
end
return windowSettings;
end
window.Save = function(self)
local windowSettings = self:RecordSettings();
if windowSettings and settings.OnSave then
settings.OnSave(self, windowSettings);
end
end
else
local doNothing = function()
-- Do nothing
end;
window.Load = settings.OnLoad or doNothing;
window.RecordSettings = doNothing;
window.Save = settings.OnSave or doNothing;
end
-- Visible, which overrides the default functions and gives the addon the ability to recieve information about it.
local visible, oldShow, oldHide = false, window.Show, window.Hide;
window.Show = function(self)
if not visible then
visible = true;
oldShow(self);
if not self.data then
self:Rebuild();
else
self:Update();
end
if settings.OnShow then
settings.OnShow(self);
end
self:RecordSettings();
end
end
window.Hide = function(self)
if visible then
visible = false;
oldHide(self);
if settings.OnHide then
settings.OnHide(self);
end
self:RecordSettings();
end
end
window.SetVisible = SetWindowVisible;
window.Toggle = ToggleWindow;
-- Phase 1: Rebuild, which prepares the data for row data generation (first pass filters checking)
-- NOTE: You can return true from the rebuild function to call the default on your new group data.
window.DefaultRebuild = function(self)
BuildGroups(self.data);
end
local onRebuild = settings.OnRebuild;
if onRebuild then
if debugging then
window.ForceRebuild = function(self)
print("ForceRebuild: " .. suffix);
local lastUpdate = GetTimePreciseSec();
local response = onRebuild(self);
if self.data then
if response then self:DefaultRebuild(); end
print("ForceRebuild (DATA): " .. suffix, (GetTimePreciseSec() - lastUpdate) * 10000);
self:ForceUpdate(true);
else
print("ForceRebuild (NO DATA): " .. suffix, (GetTimePreciseSec() - lastUpdate) * 10000);
end
end
window.Rebuild = function(self)
print("Rebuild: " .. suffix);
local lastUpdate = GetTimePreciseSec();
local response = onRebuild(self);
if self.data then
if response then self:DefaultRebuild(); end
print("Rebuild (DATA): " .. suffix, (GetTimePreciseSec() - lastUpdate) * 10000);
self:Update(true);
else
print("Rebuild (NO DATA): " .. suffix, (GetTimePreciseSec() - lastUpdate) * 10000);
end
end
else
window.ForceRebuild = function(self)
local response = onRebuild(self);
if self.data then
if response then self:DefaultRebuild(); end
self:ForceUpdate(true);
end
end
window.Rebuild = function(self)
local response = onRebuild(self);
if self.data then
if response then self:DefaultRebuild(); end
self:Update(true);
end
end
end
else
if debugging then
window.ForceRebuild = function(self)
if self.data then
print("ForceRebuild: " .. suffix);
local lastUpdate = GetTimePreciseSec();
self:DefaultRebuild();
print("ForceRebuild: " .. suffix, (GetTimePreciseSec() - lastUpdate) * 10000);
self:ForceUpdate(true);
end
end
window.Rebuild = function(self)
if self.data then
print("Rebuild: " .. suffix);
local lastUpdate = GetTimePreciseSec();
self:DefaultRebuild();
print("Rebuild: " .. suffix, (GetTimePreciseSec() - lastUpdate) * 10000);
self:Update(true);
end
end
else
window.ForceRebuild = function(self)
if self.data then
self:DefaultRebuild();
self:ForceUpdate(true);
end
end
window.Rebuild = function(self)
if self.data then
self:DefaultRebuild();
self:Update(true);
end
end
end
end
-- Phase 2: Update, which takes the prepared data and revalidates it.
local OnUpdate = settings.OnUpdate or UpdateWindow;
window.DefaultUpdate = UpdateWindow;
if settings.Silent then
if debugging then
window.ForceUpdate = function(self, force, trigger)
print("ForceUpdate: " .. suffix, force, trigger);
local lastUpdate = GetTimePreciseSec();
local result = OnUpdate(self, force, trigger);
print("ForceUpdate: " .. suffix, (GetTimePreciseSec() - lastUpdate) * 10000);
self:Refresh();
return result;
end
window.Update = function(self, force, trigger)
if self:IsShown() then
print("UpdateWindow: " .. suffix, force, trigger);
local lastUpdate = GetTimePreciseSec();
local result = OnUpdate(self, force, trigger);
print("UpdateWindow: " .. suffix, (GetTimePreciseSec() - lastUpdate) * 10000);
self:Refresh();
return result;
else
self.forceFullDataRefresh = self.forceFullDataRefresh or force or trigger;
end
end
else
window.ForceUpdate = function(self, force, trigger)
local result = OnUpdate(self, force, trigger);
self:Refresh();
return result;
end
window.Update = function(self, force, trigger)
if self:IsShown() then
local result = OnUpdate(self, force, trigger);
self:Refresh();
return result;
else
self.forceFullDataRefresh = self.forceFullDataRefresh or force or trigger;
end
end
end
elseif debugging then
window.ForceUpdate = function(self, force, trigger)
print("ForceUpdate: " .. suffix, force, trigger);
local lastUpdate = GetTimePreciseSec();
local result = OnUpdate(self, force, trigger);
print("ForceUpdate: " .. suffix, (GetTimePreciseSec() - lastUpdate) * 10000);
self:Refresh();
return result;
end
window.Update = function(self, force, trigger)
print("UpdateWindow: " .. suffix, force, trigger);
local lastUpdate = GetTimePreciseSec();
local result = OnUpdate(self, force, trigger);
print("UpdateWindow: " .. suffix, (GetTimePreciseSec() - lastUpdate) * 10000);
self:Refresh();
return result;
end
else
window.ForceUpdate = function(self, force, trigger)
local result = OnUpdate(self, force, trigger);
self:Refresh();
return result;
end
window.Update = function(self, force, trigger)
local result = OnUpdate(self, force, trigger);
self:Refresh();
return result;
end
end
-- Phase 3: Refresh, which simply refreshes the rows as they are with the row data.
local defaultOnRefresh = UpdateVisibleRowData;
local onRefresh = settings.OnRefresh;
if onRefresh then
if debugging then
window.Refresh = function(self)
print("Refresh: " .. suffix);
local lastUpdate = GetTimePreciseSec();
if onRefresh(self) then defaultOnRefresh(self); end
print("Refresh: " .. suffix, (GetTimePreciseSec() - lastUpdate) * 10000);
end
else
window.Refresh = function(self)
if onRefresh(self) then defaultOnRefresh(self); end
end
end
else
if debugging then
window.Refresh = function(self)
print("Refresh: " .. suffix);
local lastUpdate = GetTimePreciseSec();
defaultOnRefresh(self);
print("Refresh: " .. suffix, (GetTimePreciseSec() - lastUpdate) * 10000);
end
else
window.Refresh = defaultOnRefresh;
end
end
-- Phase 4: Redraw, which only updates the rows that already have row data visually.
window.Redraw = function(self)
if self:IsShown() then
RedrawVisibleRowData(self);
end
end
local delays = {};
window.DelayedCall = function(self, method, delay, force)
delays[method] = delay or 60;
window:StartATTCoroutine("DelayedCall::" .. method, function()
while delays[method] > 0 do
coroutine.yield();
delays[method] = delays[method] - 1;
end
while InCombatLockdown() do
coroutine.yield();
end
window[method](window, force);
end);
end
window.DelayedRebuild = function(self)
self:DelayedCall("Rebuild", 1);
end
window.DelayedRefresh = function(self)
self:DelayedCall("Refresh", 5);
end
window.DelayedUpdate = function(self, force)
self:DelayedCall("Update", 60, force);
end
-- The Row Container. This contains all of the row frames.
local container = CreateFrame("FRAME", nil, window);
container:SetPoint("TOPLEFT", window, "TOPLEFT", 2, -6);
container:SetPoint("BOTTOM", window, "BOTTOM", 0, 6);
window.Container = container;
UpdateWindowColor(window, suffix);
container.rows = {};
container:Show();
local topright = window:CreateTexture(nil, "OVERLAY")
topright:SetTexture(251963) -- Interface\\PaperDollInfoFrame\\UI-GearManager-Border
topright:SetPoint("TOPRIGHT", window, "TOPRIGHT", -2, -2);
topright:SetTexCoord(0.7, 0.745, 0.04, 0.4)
topright:SetSize(20, 20);
window.TopRight = topright;
-- The Close Button. It's assigned as a local variable so you can change how it behaves.
local closeButton = CreateFrame("Button", nil, window, "UIPanelCloseButton");
closeButton:SetPoint("TOPRIGHT", window, "TOPRIGHT", 0, -1);
closeButton:SetScript("OnClick", function() window:Toggle(); end);
closeButton:SetSize(24, 24);
window.CloseButton = closeButton;
-- The Scroll Bar.
window.CurrentIndex = 0;
local scrollbar = CreateFrame("Slider", nil, window, "UIPanelScrollBarTemplate");
container:SetPoint("RIGHT", scrollbar, "LEFT", -2, 0);
scrollbar:SetPoint("TOP", window, "TOP", 0, -40);
scrollbar:SetPoint("BOTTOMRIGHT", window, "BOTTOMRIGHT", -4, 36);
scrollbar:SetScript("OnValueChanged", function(self, value)
if window.CurrentIndex ~= value then
window.CurrentIndex = value;
window:Refresh();
end
end);
scrollbar.back = scrollbar:CreateTexture(nil, "BACKGROUND");
scrollbar.back:SetColorTexture(0.1,0.1,0.1,1)
scrollbar.back:SetAllPoints(scrollbar);
scrollbar:SetMinMaxValues(1, 1);
scrollbar:SetValueStep(1);
scrollbar:SetValue(1);
scrollbar:SetObeyStepOnDrag(true);
scrollbar:SetWidth(16);
scrollbar:EnableMouseWheel(true);
window:SetScript("OnMouseWheel", function(self, delta)
scrollbar:SetValue(window.CurrentIndex - delta);
end);
window.SetMinMaxValues = function(self, displayedValue, totalValue)
scrollbar:SetMinMaxValues(1, math.max(1, totalValue - displayedValue));
end
-- The Corner Grip. (this isn't actually used, but it helps indicate to players that they can do something)
local grip = window:CreateTexture(nil, "ARTWORK");
grip:SetTexture(app.asset("grip"));
grip:SetSize(16, 16);
grip:SetTexCoord(0,1,0,1);
grip:SetPoint("BOTTOMRIGHT", -5, 5);
window:EnableMouseWheel(true);
window.ScrollBar = scrollbar;
-- Setup the Event Handlers
local handlers = {};
window:SetScript("OnEvent", function(self, e, ...)
if debugging then print(e, ...); end
local handler = handlers[e];
if handler then
handler(self, ...);
else
self:Update();
end
end);
if not settings.IgnoreQuestUpdates then
local delayedRefresh = function(self)
self:DelayedRefresh();
end;
handlers.BAG_UPDATE_DELAYED = delayedRefresh;
handlers.QUEST_WATCH_UPDATE = delayedRefresh;
handlers.QUEST_ITEM_UPDATE = delayedRefresh;
window:RegisterEvent("QUEST_WATCH_UPDATE");
window:RegisterEvent("QUEST_ITEM_UPDATE");
window:RegisterEvent("BAG_UPDATE_DELAYED");
local delayedUpdateWithTrigger = function(self)
self:Redraw();
self:DelayedUpdate(true);
end;
handlers.QUEST_ACCEPTED = delayedUpdateWithTrigger;
handlers.QUEST_REMOVED = delayedUpdateWithTrigger;
window:RegisterEvent("QUEST_ACCEPTED");
window:RegisterEvent("QUEST_REMOVED");
local delayedUpdate = function(self)
self:DelayedUpdate();
end;
handlers.QUEST_TURNED_IN = delayedUpdate;
handlers.QUEST_LOG_UPDATE = delayedUpdate;
window:RegisterEvent("QUEST_TURNED_IN");
window:RegisterEvent("QUEST_LOG_UPDATE");
end
if settings.OnInit then
settings.OnInit(window, handlers);
end
LoadSettingsForWindow(window);
end
return window;
end
function app:BuildFlatSearchFilteredResponse(groups, filter, t)
if groups then
for i,group in ipairs(groups) do
if filter(group) then
tinsert(t, CloneReference(group));
elseif group.g then
app:BuildFlatSearchFilteredResponse(group.g, filter, t);
end
end
end
end
function app:BuildFlatSearchResponse(groups, field, value, t)
if groups then
for i,group in ipairs(groups) do
local v = group[field];
if v and (v == value or (field == "requireSkill" and app.SpellIDToSkillID[app.SpecializationSpellIDs[v] or 0] == value)) then
tinsert(t, CloneReference(group));
elseif group.g then
app:BuildFlatSearchResponse(group.g, field, value, t);
end
end
end
end
function app:BuildFlatSearchResponseForField(groups, field, t)
if groups then
for i,group in ipairs(groups) do
if group[field] then
tinsert(t, CloneReference(group));
elseif group.g then
app:BuildFlatSearchResponseForField(group.g, field, t);
end
end
end
end
function app:BuildSearchFilteredResponse(groups, filter)
if groups then
local t;
for i,group in ipairs(groups) do
local response = app:BuildSearchFilteredResponse(group.g, filter);
if response then
if not t then t = {}; end
tinsert(t, setmetatable({g=response}, { __index = group }));
elseif filter(group) then
if not t then t = {}; end
tinsert(t, CloneReference(group));
end
end
return t;
end
end
function app:BuildSearchResponse(groups, field, value)
if groups then
local t;
for i,group in ipairs(groups) do
local response = app:BuildSearchResponse(group.g, field, value);
if response then
if not t then t = {}; end
tinsert(t, setmetatable({g=response}, { __index = group }));
else
local v = group[field];
if v and (v == value or (field == "requireSkill" and app.SpellIDToSkillID[app.SpecializationSpellIDs[v] or 0] == value)) then
if not t then t = {}; end
tinsert(t, CloneReference(group));
end
end
end
return t;
end
end
function app:BuildSearchResponseForField(groups, field)
if groups then
local t;
for i,group in ipairs(groups) do
local response = app:BuildSearchResponseForField(group.g, field);
if response then
if not t then t = {}; end
tinsert(t, setmetatable({g=response}, { __index = group }));
elseif group[field] then
if not t then t = {}; end
tinsert(t, CloneReference(group));
end
end
return t;
end
end
-- Dynamic Popouts for Quest Chains and other Groups
local function OnInitForPopout(self, group)
if group.questID or group.sourceQuests then
local mainQuest = CloneReference(group);
if group.parent then mainQuest.sourceParent = group.parent; end
if mainQuest.sym then
mainQuest.collectible = true;
mainQuest.visible = true;
mainQuest.progress = 0;
mainQuest.total = 0;
if not mainQuest.g then
local resolved = ResolveSymbolicLink(group);
if resolved then
for i=#resolved,1,-1 do
resolved[i] = CreateObject(resolved[i]);
end
mainQuest.g = resolved;
end
else
local resolved = ResolveSymbolicLink(group);
if resolved then
MergeObjects(mainQuest.g, resolved);
end
end
end
if questID then mainQuest.collectible = true; end
local g = { mainQuest };
-- Check to see if Source Quests are listed elsewhere.
if questID and not group.sourceQuests then
local searchResults = SearchForField("questID", questID);
if #searchResults > 1 then
for i=1,#searchResults,1 do
local searchResult = searchResults[i];
if searchResult.questID == questID and searchResult.sourceQuests then
searchResult = CloneReference(searchResult);
searchResult.collectible = true;
searchResult.g = g;
mainQuest = searchResult;
g = { mainQuest };
break;
end
end
end
end
-- Show Quest Prereqs
if mainQuest.sourceQuests then
local breakafter = 0;
local sourceQuests, sourceQuest, subSourceQuests, prereqs = mainQuest.sourceQuests;
while sourceQuests and #sourceQuests > 0 do
subSourceQuests = {}; prereqs = {};
for i,sourceQuestID in ipairs(sourceQuests) do
sourceQuest = sourceQuestID < 1 and SearchForField("creatureID", math.abs(sourceQuestID)) or SearchForField("questID", sourceQuestID);
if #sourceQuest > 0 then
local found = nil;
for i=1,#sourceQuest,1 do
-- Only care about the first search result.
local sq = sourceQuest[i];
if sq and sq.questID and not sq.objectiveID then
questID = sq.questID;
if sq.parent and sq.parent.questID == questID then
sq = sq.parent;
end
if app.GroupFilter(sq) then
if app.RecursiveClassAndRaceFilter(sq) and questID == sourceQuestID then
if not found or (not found.sourceQuests and sq.sourceQuests) then
found = sq;
end
end
end
end
end
if found then
sourceQuest = CloneReference(found);
sourceQuest.collectible = true;
sourceQuest.visible = true;
sourceQuest.hideText = true;
if found.sourceQuests and #found.sourceQuests > 0 and (not found.saved or app.CollectedItemVisibilityFilter(sourceQuest)) then
-- Mark the sub source quest IDs as marked (as the same sub quest might point to 1 source quest ID)
for j, subsourceQuests in ipairs(found.sourceQuests) do
subSourceQuests[subsourceQuests] = true;
end
end
else
sourceQuest = nil;
end
elseif sourceQuestID > 0 then
-- Create a Quest Object.
sourceQuest = app.CreateQuest(sourceQuestID, { ['visible'] = true, ['collectible'] = true, ['hideText'] = true });
else
-- Create a NPC Object.
sourceQuest = app.CreateNPC(math.abs(sourceQuestID), { ['visible'] = true, ['hideText'] = true });
end
-- If the quest was valid, attach it.
if sourceQuest then tinsert(prereqs, sourceQuest); end
end
-- Convert the subSourceQuests table into an array
sourceQuests = {};
if #prereqs > 0 then
for sourceQuestID,i in pairs(subSourceQuests) do
tinsert(sourceQuests, tonumber(sourceQuestID));
end
tinsert(prereqs, {
["text"] = "Upon Completion",
["description"] = "The above quests need to be completed before being able to complete the quest(s) listed below.",
["icon"] = "Interface\\Icons\\Spell_Holy_MagicalSentry.blp",
["visible"] = true,
["expanded"] = true,
["hideText"] = true,
["g"] = g,
});
g = prereqs;
breakafter = breakafter + 1;
if breakafter >= 100 then
app.print("Likely just broke out of an infinite source quest loop. Please report this to the ATT Discord!");
break;
end
end
end
-- Clean up the recursive hierarchy. (this removed duplicates)
sourceQuests = {};
prereqs = g;
local orig = g;
while prereqs and #prereqs > 0 do
for i=#prereqs,1,-1 do
local o = prereqs[i];
if o.key then
sourceQuest = o.key .. o[o.key];
if sourceQuests[sourceQuest] then
-- Already exists in the hierarchy. Uh oh.
tremove(prereqs, i);
else
sourceQuests[sourceQuest] = true;
end
end
end
if #prereqs > 1 then
prereqs = prereqs[#prereqs];
if prereqs then prereqs = prereqs.g; end
orig = prereqs;
else
prereqs = prereqs[#prereqs];
if prereqs then prereqs = prereqs.g; end
orig[#orig].g = prereqs;
end
end
-- Clean up standalone "Upon Completion" headers.
prereqs = g;
repeat
local n = #prereqs;
local lastprereq = prereqs[n];
if lastprereq.text == "Upon Completion" and n > 1 then
tremove(prereqs, n);
local g = prereqs[n-1].g;
if not g then
g = {};
prereqs[n-1].g = g;
end
if lastprereq.g then
for i,data in ipairs(lastprereq.g) do
tinsert(g, data);
end
end
prereqs = g;
else
prereqs = lastprereq.g;
end
until not prereqs or #prereqs < 1;
end
self.data = {
text = "Quest Chain Requirements",
icon = "Interface\\Icons\\Spell_Holy_MagicalSentry.blp",
description = "The following quests need to be completed before being able to complete the final quest.",
hideText = true,
g = g,
};
elseif group.sym then
self.data = CloneReference(group);
self.data.collectible = true;
self.data.visible = true;
self.data.progress = 0;
self.data.total = 0;
if not self.data.g then
local resolved = ResolveSymbolicLink(group);
if resolved then
for i=#resolved,1,-1 do
resolved[i] = CreateObject(resolved[i]);
end
self.data.g = resolved;
end
else
local resolved = ResolveSymbolicLink(group);
if resolved then
MergeObjects(self.data.g, resolved);
end
end
elseif group.g then
-- This is already a container with accurate numbers.
self.data = group;
else
-- This is a standalone item
group.visible = true;
if not group.g and (group.itemID or group.currencyID) then
local cmd = group.link or group.key .. ":" .. group[group.key];
group = GetCachedSearchResults(cmd, SearchForLink, cmd);
end
self.data = group;
end
-- Clone the data and then insert it into the Raw Data table.
self.data = CloneReference(self.data);
self.data.hideText = true;
self.data.visible = true;
self.data.indent = 0;
self.data.total = 0;
self.data.progress = 0;
-- If this is an achievement, build the criteria within it if possible.
local achievementID = group.achievementID;
if achievementID then
local searchResults = SearchForField("achievementID", achievementID);
if #searchResults > 0 then
for i=1,#searchResults,1 do
local searchResult = searchResults[i];
if searchResult.achievementID == achievementID and searchResult.criteriaID then
if not self.data.g then self.data.g = {}; end
MergeObject(self.data.g, CloneReference(searchResult));
end
end
end
end
--[[
local currencyID = group.currencyID;
if currencyID and not self.data.usedtobuy then
local searchResults = SearchForField("currencyIDAsCost", currencyID);
if #searchResults > 0 then
local usedtobuy = {};
usedtobuy.g = {};
usedtobuy.text = "Used to Buy";
usedtobuy.icon = "Interface\\Icons\\INV_Misc_Coin_01";
usedtobuy.description = "This tooltip dynamically calculates the total number you need based on what is still visible below this header.";
usedtobuy.OnTooltip = function(t)
local total = 0;
for _,o in ipairs(t.g) do
if o.visible then
if o.cost then
for k,v in ipairs(o.cost) do
if v[1] == "c" and v[2] == currencyID then
total = total + (v[3] or 1);
end
end
end
if o.providers then
for k,v in ipairs(o.providers) do
if v[1] == "c" and v[2] == currencyID then
total = total + (v[3] or 1);
end
end
end
end
end
GameTooltip:AddDoubleLine("Total Needed", total);
end
MergeObjects(usedtobuy.g, searchResults);
if not self.data.g then self.data.g = {}; end
tinsert(self.data.g, usedtobuy);
self.data.usedtobuy = usedtobuy;
end
end
local itemID = group.itemID;
if itemID and not self.data.tradedin then
local searchResults = SearchForField("itemIDAsCost", itemID);
if #searchResults > 0 then
local tradedin = {};
tradedin.g = {};
tradedin.text = "Used For";
tradedin.icon = "Interface\\Icons\\INV_Misc_Coin_01";
tradedin.description = "This tooltip dynamically calculates the total number you need based on what is still visible below this header.";
tradedin.OnTooltip = function(t)
local total = 0;
for _,o in ipairs(t.g) do
if o.visible then
if o.cost then
for k,v in ipairs(o.cost) do
if v[1] == "i" and v[2] == itemID then
total = total + (v[3] or 1);
end
end
end
if o.providers then
for k,v in ipairs(o.providers) do
if v[1] == "i" and v[2] == itemID then
total = total + (v[3] or 1);
end
end
end
end
end
GameTooltip:AddDoubleLine("Total Needed", total);
end
MergeObjects(tradedin.g, searchResults);
if not self.data.g then self.data.g = {}; end
tinsert(self.data.g, tradedin);
self.data.tradedin = tradedin;
end
end
]]--
if self.data.key then
if group.cost and type(group.cost) == "table" then
local costGroup = {
["text"] = "Cost",
["description"] = "The following contains all of the relevant items or currencies needed to acquire this.",
["icon"] = "Interface\\Icons\\INV_Misc_Coin_02",
["OnUpdate"] = app.AlwaysShowUpdate,
["g"] = {},
};
local costItem;
for i,c in ipairs(group.cost) do
costItem = nil;
if c[1] == "c" then
costItem = app.CreateCurrencyClass(c[2]);
elseif c[1] == "i" then
costItem = app.CreateItem(c[2]);
end
if costItem then
costItem = CloneReference(costItem);
costItem.visible = true;
costItem.OnUpdate = app.AlwaysShowUpdate;
MergeObject(costGroup.g, costItem);
end
end
if #costGroup.g > 0 then
if not self.data.g then self.data.g = {}; end
MergeObject(self.data.g, costGroup, 1);
end
end
if group.providers or group.qgs or group.crs then
local sourceGroup = {
["text"] = "Sources",
["description"] = "The following contains all of the relevant sources.",
["icon"] = "Interface\\Icons\\INV_Misc_Coin_02",
["OnUpdate"] = app.AlwaysShowUpdate,
["g"] = {},
};
local sourceItem;
if group.providers then
for _,p in ipairs(group.providers) do
sourceItem = nil;
if p[1] == "n" then
sourceItem = app.CreateNPC(p[2]);
elseif p[1] == "o" then
sourceItem = app.CreateObject(p[2]);
elseif p[1] == "i" then
sourceItem = app.CreateItem(p[2]);
end
if sourceItem then
sourceItem.visible = true;
sourceItem.OnUpdate = app.AlwaysShowUpdate;
MergeObject(sourceGroup.g, sourceItem);
end
end
end
if group.crs then
for _,cr in ipairs(group.crs) do
sourceItem = app.CreateNPC(cr);
sourceItem.visible = true;
sourceItem.OnUpdate = app.AlwaysShowUpdate;
MergeObject(sourceGroup.g, sourceItem);
end
end
if group.qgs then
for _,qg in ipairs(group.qgs) do
sourceItem = app.CreateNPC(qg);
sourceItem.visible = true;
sourceItem.OnUpdate = app.AlwaysShowUpdate;
MergeObject(sourceGroup.g, sourceItem);
end
end
if #sourceGroup.g > 0 then
if not self.data.g then self.data.g = {}; end
MergeObject(self.data.g, sourceGroup, 1);
end
end
end
BuildGroups(self.data);
UpdateGroups(self.data, self.data.g);
end
function app:CreateMiniListForGroup(group)
-- Is this an achievement criteria or lacking some achievement information?
local achievementID = group.achievementID;
if achievementID and (group.criteriaID or not group.g) then
local searchResults = SearchForField("achievementID", achievementID);
if #searchResults > 0 then
local bestResult;
for i=1,#searchResults,1 do
local searchResult = searchResults[i];
if searchResult.achievementID == achievementID and not searchResult.criteriaID then
if not bestResult or searchResult.g then
bestResult = searchResult;
end
end
end
if bestResult then group = bestResult; end
end
end
-- Is this a quest object or objective?
local questID, parent = group.questID, group.parent;
if questID and parent and parent.questID == questID then
group = parent;
end
-- Pop Out Functionality! :O
local popout = app:GetWindow(BuildSourceTextForDynamicPath(group), {
Silent = true,
AllowCompleteSound = true,
--Debugging = true,
OnInit = function(self)
OnInitForPopout(self, (group.OnPopout and group:OnPopout()) or group);
end,
OnLoad = function(self, settings)
settings.dynamic = true;
settings.sourcePath = self.Suffix;
-- This might be something we can rebuild
local key = group.key;
if key then
settings.key = key;
settings.id = group[key];
end
end,
OnSave = function(self, settings)
if not settings.visible then
self:ClearSettings();
end
end,
});
if IsAltKeyDown() then
AddTomTomWaypoint(popout.data, false);
else
if not popout.data.expanded then
ExpandGroupsRecursively(popout.data, true, true);
end
popout:SetVisible(true);
end
return popout;
end
local function SearchForSourcePath(g, hashes, level, count)
if g then
local hash = hashes[level];
if hash then
for i,o in ipairs(g) do
if (o.hash or o.name or o.text) == hash then
if level == count then return o; end
return SearchForSourcePath(o.g, hashes, level + 1, count);
end
end
end
end
end
function app:CreateMiniListFromSource(key, id, sourcePath)
-- If we provided the original source path, then we can find the exact element to popout.
if sourcePath then
local hashes = { strsplit(">", sourcePath) };
local ref = SearchForSourcePath(app:GetDataCache().g, hashes, 2, #hashes);
if ref then
app:CreateMiniListForGroup(ref);
return;
end
end
-- Without this it can't be recovered. :(
if key and id then
if sourcePath then
-- Try to find an exact match.
local searchResults = SearchForField(key, id);
if #searchResults > 0 then
for i,ref in ipairs(searchResults) do
if BuildSourceTextForDynamicPath(ref) == sourcePath then
app:CreateMiniListForGroup(ref);
return;
end
end
end
end
-- Search for the Link in the database
local cmd = key .. ":" .. id;
local ref = GetCachedSearchResults(cmd, SearchForLink, cmd);
if ref then
app:CreateMiniListForGroup(ref);
return;
end
-- Search for the field/value pair everywhere in the DB.
local t = {};
app:BuildFlatSearchResponse(app:GetDataCache().g, key, id, t);
if t and #t > 0 then
local ref = #t == 1 and t[1] or CreateObject({ hash = key .. id, key = key, [key] = id, g = t });
if ref then
app:CreateMiniListForGroup(ref);
return;
end
end
end
end
-- Create the Primary Collection Window (this allows you to save the size and location)
app:GetWindow("Prime", {
parent = UIParent,
Silent = true,
AllowCompleteSound = true,
Defaults = {
["y"] = 20,
["x"] = 0,
["scale"] = 1.2,
["width"] = 360,
["height"] = 600,
["visible"] = false,
["point"] = "CENTER",
["relativePoint"] = "CENTER",
},
OnInit = function(self)
app.ToggleMainList = function()
self:Toggle();
end
SLASH_ATTPRIME1 = "/allthethings";
SLASH_ATTPRIME2 = "/att";
SLASH_ATTPRIME3 = "/attc";
SLASH_ATTPRIME4 = "/things";
SLASH_ATTPRIME5 = "/attmain";
SlashCmdList["ATTPRIME"] = function(cmd)
if cmd and strlen(cmd) > 0 then
-- Search for the Link in the database
cmd = string.lower(cmd);
local group = GetCachedSearchResults(cmd, SearchForLink, cmd);
if group then app:CreateMiniListForGroup(group); end
else
-- Default command
self:Toggle();
end
end
end,
OnLoad = function(self, settings)
if not settings.visible then
self:ForceRebuild();
end
end,
OnRebuild = function(self)
-- Prime's data is built elsewhere.
self.data = app:GetDataCache();
return false;
end,
OnUpdate = function(self, ...)
self.DefaultUpdate(self, ...);
-- Write the current character's progress.
local rootData = self.data;
app.CurrentCharacter.PrimeData = {
progress = rootData.progress,
total = rootData.total,
modeString = rootData.modeString,
};
end
});
local function RefreshLocationCoroutine()
-- Wait a second, will ya? The position detection is BAD.
for i=1,30,1 do coroutine.yield(); end
-- Acquire the new map ID.
local mapID = app.GetCurrentMapID();
while not mapID do
coroutine.yield();
mapID = app.GetCurrentMapID();
end
app.CurrentMapID = mapID;
local window = app:GetWindow("CurrentInstance");
if window then window:SetMapID(mapID); end
end
local function SortForMiniList(a,b)
-- If either object doesn't exist
if a then
if not b then
return true;
end
elseif b then
return false;
else
-- neither a or b exists, equality returns false
return false;
end
if a.isRaid then
if not b.isRaid then
return true;
end
elseif b.isRaid then
return false;
elseif b.maps or b.mapID then
if not (a.maps or a.mapID) then
return true;
end
elseif a.maps then
return false;
end
-- Any two similar-type groups with text
return tostring(a.name or a.text) < tostring(b.name or b.text);
end
local function RefreshLocation()
app:StartATTCoroutine("RefreshLocation", RefreshLocationCoroutine);
end
local CachedMapData = setmetatable({}, {
__index = function(cachedMapData, mapID)
local results = SearchForField("mapID", mapID);
if #results > 0 then
-- Simplify the returned groups
local groups = {};
local headers = setmetatable({}, {
__index = function(t, headerID)
for i=1,#groups,1 do
local o = groups[i];
if o.headerID == headerID then
if not o.g then o.g = {}; end
t[headerID] = o;
return o;
end
end
local o = app.CreateNPC(headerID);
tinsert(groups, o);
t[headerID] = o;
o.g = {};
return o;
end
});
local function MergeIntoHeader(headerID, o)
MergeObject(headers[headerID].g, o);
end
local header = {};
header.mapID = mapID;
header.g = groups;
for i, group in ipairs(results) do
local clone = {};
for key,value in pairs(group) do
if key == "maps" then
local maps = {};
for i,mapID in ipairs(value) do
tinsert(maps, mapID);
end
clone[key] = maps;
elseif key == "g" then
local g = {};
for i,o in ipairs(value) do
o = CloneReference(o);
ExpandGroupsRecursively(o, false);
tinsert(g, o);
end
clone[key] = g;
else
clone[key] = value;
end
end
local c = GetRelativeValue(group, "c");
if c then clone.c = c; end
local r = GetRelativeValue(group, "r");
if r then clone.r = r; end
setmetatable(clone, getmetatable(group));
local key = group.key;
if (key == "mapID" or key == "instanceID") or ((key == "headerID" or key == "npcID") and (group.maps and (mapID < 0 and contains(group.maps, mapID)))) then
header.key = key;
header[key] = group[key];
MergeObject({header}, clone);
elseif key == "criteriaID" then
clone.achievementID = group.achievementID;
MergeIntoHeader(app.HeaderConstants.ACHIEVEMENTS, clone);
elseif key == "achievementID" then
MergeIntoHeader(app.HeaderConstants.ACHIEVEMENTS, clone);
elseif key == "questID" then
MergeIntoHeader(app.HeaderConstants.QUESTS, clone);
elseif key == "factionID" then
MergeIntoHeader(app.HeaderConstants.FACTIONS, clone);
elseif key == "explorationID" then
MergeIntoHeader(app.HeaderConstants.EXPLORATION, clone);
elseif key == "flightPathID" then
MergeIntoHeader(app.HeaderConstants.FLIGHT_PATHS, clone);
elseif key == "itemID" or key == "spellID" then
if GetRelativeField(group, "headerID", app.HeaderConstants.ZONE_DROPS) then
MergeIntoHeader(app.HeaderConstants.ZONE_DROPS, clone);
else
local requireSkill = GetRelativeValue(group, "requireSkill");
if requireSkill then
MergeObject(groups, app.CreateProfession(requireSkill, { g = { clone } }));
else
local headerID = GetRelativeValue(group, "headerID");
if headerID then
MergeIntoHeader(headerID, clone);
else
MergeObject(groups, clone);
end
end
end
elseif key == "headerID" then
MergeObject(groups, clone);
else
local headerID = GetRelativeValue(group, "headerID");
if headerID then
MergeIntoHeader(headerID, clone);
else
MergeObject(groups, clone);
end
end
end
-- Swap out the map data for the header.
results = ((results.classID and app.CreateCharacterClass) or (header.key == "instanceID" and app.CreateInstance) or app.CreateMap)(header[header.key], header);
ExpandGroupsRecursively(results, true);
results.visible = true;
results.expanded = true;
results.mapID = mapID;
results.back = 1;
results.indent = 0;
local difficultyID = (IsInInstance() and select(3, GetInstanceInfo())) or (EJ_GetDifficulty and EJ_GetDifficulty()) or 0;
if difficultyID ~= 0 then
for _,row in ipairs(header.g) do
if row.difficultyID or row.difficulties then
if (row.difficultyID or -1) == difficultyID or (row.difficulties and containsValue(row.difficulties, difficultyID)) then
if not row.expanded then ExpandGroupsRecursively(row, true, true); expanded = true; end
elseif row.expanded then
ExpandGroupsRecursively(row, false, true);
end
end
end
end
-- Sort the list, but not for instances.
if not results.instanceID then
app.Sort(groups, SortForMiniList);
end
-- Check to see completion...
BuildGroups(results);
cachedMapData[mapID] = results;
return results;
else
-- If we don't have any map data on this area, report it to the chat window.
print("No map found for this location ", app.GetMapName(mapID), " [", mapID, "]");
local mapInfo = C_Map_GetMapInfo(mapID);
if mapInfo then
local mapPath = mapInfo.name or ("Map ID #" .. mapID);
mapID = mapInfo.parentMapID;
while mapID do
mapInfo = C_Map_GetMapInfo(mapID);
if mapInfo then
mapPath = (mapInfo.name or ("Map ID #" .. mapID)) .. " > " .. mapPath;
mapID = mapInfo.parentMapID;
else
break;
end
end
print("Path: ", mapPath);
end
print("Please report this to the ATT Discord! Thanks! ", app.Version);
end
end
});
app:GetWindow("CurrentInstance", {
parent = UIParent,
Silent = true,
AllowCompleteSound = true,
Defaults = {
["y"] = 0,
["x"] = 0,
["scale"] = 0.7,
["width"] = 360,
["height"] = 176,
["visible"] = true,
["point"] = "BOTTOMRIGHT",
["relativePoint"] = "BOTTOMRIGHT",
},
OnInit = function(self, handlers)
SLASH_ATTMINILIST1 = "/attmini";
SLASH_ATTMINILIST2 = "/attminilist";
app.ToggleMiniListForCurrentZone = function() self:Toggle(); end;
SlashCmdList["ATTMINILIST"] = app.ToggleMiniListForCurrentZone;
local delayedUpdate = function()
self:DelayedUpdate(true);
end;
handlers.QUEST_TURNED_IN = delayedUpdate;
handlers.QUEST_LOG_UPDATE = delayedUpdate;
handlers.ZONE_CHANGED = RefreshLocation;
handlers.ZONE_CHANGED_INDOORS = RefreshLocation;
handlers.ZONE_CHANGED_NEW_AREA = RefreshLocation;
handlers.PLAYER_DIFFICULTY_CHANGED = function()
wipe(CachedMapData);
self:Rebuild();
end
self.SetMapID = function(self, mapID)
if mapID ~= self.mapID then
self.mapID = mapID;
self:Rebuild();
end
end
end,
OnLoad = function(self, settings)
self:RegisterEvent("ZONE_CHANGED");
self:RegisterEvent("ZONE_CHANGED_INDOORS");
self:RegisterEvent("ZONE_CHANGED_NEW_AREA");
pcall(self.RegisterEvent, self, "PLAYER_DIFFICULTY_CHANGED");
self:SetMapID(settings.mapID or app.CurrentMapID or app.GetCurrentMapID());
RefreshLocation();
end,
OnSave = function(self, settings)
settings.mapID = self.mapID;
end,
OnRebuild = function(self)
self.data = CachedMapData[self.mapID];
end,
});
-- Uncomment this section if you need to enable Debugger:
--[[
app:GetWindow("Debugger", {
parent = UIParent,
Silent = true,
OnInit = function(self, handlers)
self.AddObject = function(self, info)
-- Bubble Up the Maps
local mapInfo;
local mapID = app.CurrentMapID;
if mapID then
local pos = C_Map_GetPlayerMapPosition(mapID, "player");
if pos then
local px, py = pos:GetXY();
info.coord = { px * 100, py * 100, mapID };
end
repeat
mapInfo = C_Map_GetMapInfo(mapID);
if mapInfo then
info = { ["mapID"] = mapInfo.mapID, ["g"] = { info } };
mapID = mapInfo.parentMapID
end
until not mapInfo or not mapID;
end
MergeClone(self.data.g, info);
MergeObject(self.rawData, info);
self:Update();
end
self.data = {
['text'] = "Session History",
['icon'] = app.asset("WindowIcon_RaidAssistant"),
["description"] = "This keeps a visual record of all of the quests, maps, loot, and vendors that you have come into contact with since the session was started.",
['visible'] = true,
['expanded'] = true,
['back'] = 1,
['options'] = {
{
['text'] = "Clear History",
['icon'] = "Interface\\Icons\\Ability_Rogue_FeignDeath.blp",
["description"] = "Click this to fully clear this window.\n\nNOTE: If you click this by accident, use the dynamic Restore Buttons that this generates to reapply the data that was cleared.\n\nWARNING: If you reload the UI, the data stored in the Reload Button will be lost forever!",
['visible'] = true,
['count'] = 0,
['OnClick'] = function(row, button)
local copy = {};
for i,o in ipairs(self.rawData) do
tinsert(copy, o);
end
if #copy < 1 then
app.print("There is nothing to clear.");
return true;
end
row.ref.count = row.ref.count + 1;
tinsert(self.data.options, {
['text'] = "Restore Button " .. row.ref.count,
['icon'] = app.asset("Button_Reroll"),
["description"] = "Click this to restore your cleared data.\n\nNOTE: Each Restore Button houses different data.\n\nWARNING: This data will be lost forever when you reload your UI!",
['visible'] = true,
['data'] = copy,
['OnClick'] = function(row, button)
for i,info in ipairs(row.ref.data) do
MergeClone(self.data.g, info);
MergeObject(self.rawData, info);
end
self:Update();
return true;
end,
});
wipe(self.rawData);
wipe(self.data.g);
for i=#self.data.options,1,-1 do
tinsert(self.data.g, 1, self.data.options[i]);
end
self:Update();
return true;
end,
},
},
['g'] = {},
};
self.rawData = {};
-- Setup Event Handlers and register for events
self:SetScript("OnEvent", function(self, e, ...)
--print(e, ...);
if e == "ADDON_LOADED" then
-- Only execute for this addon.
local addonName = ...;
if addonName ~= appName then return; end
self:UnregisterEvent("ADDON_LOADED");
if not ATTClassicDebugData then
ATTClassicDebugData = app.GetDataMember("Debugger", {});
app.SetDataMember("Debugger", nil);
end
self.rawData = ATTClassicDebugData;
self.data.g = CreateObject(self.rawData);
for i=#self.data.options,1,-1 do
tinsert(self.data.g, 1, self.data.options[i]);
end
self:Update();
elseif e == "ZONE_CHANGED" or e == "ZONE_CHANGED_NEW_AREA" then
-- Bubble Up the Maps
local mapInfo, info;
local mapID = app.CurrentMapID;
if mapID then
repeat
info = { ["mapID"] = mapID, ["g"] = info and { info } or nil };
mapInfo = C_Map_GetMapInfo(mapID);
if mapInfo then
mapID = mapInfo.parentMapID;
end
until not mapInfo or not mapID;
MergeClone(self.data.g, info);
MergeObject(self.rawData, info);
self:Update();
end
elseif e == "MERCHANT_SHOW" or e == "MERCHANT_UPDATE" then
C_Timer.After(0.6, function()
local guid = UnitGUID("npc");
local ty, zero, server_id, instance_id, zone_uid, npcID, spawn_uid;
if guid then ty, zero, server_id, instance_id, zone_uid, npcID, spawn_uid = strsplit("-",guid); end
if npcID then
npcID = tonumber(npcID);
-- Ignore vendor mount...
if npcID == 62822 then
return true;
end
local numItems = GetMerchantNumItems();
--print("MERCHANT DETAILS", ty, npcID, numItems);
local rawGroups = {};
for i=1,numItems,1 do
local link = GetMerchantItemLink(i);
if link then
local name, texture, cost, quantity, numAvailable, isPurchasable, isUsable, extendedCost = GetMerchantItemInfo(i);
if extendedCost then
cost = {};
local itemCount = GetMerchantItemCostInfo(i);
for j=1,itemCount,1 do
local itemTexture, itemValue, itemLink = GetMerchantItemCostItem(i, j);
if itemLink then
-- print(" ", itemValue, itemLink, gsub(itemLink, "\124", "\124\124"));
local m = itemLink:match("currency:(%d+)");
if m then
-- Parse as a CURRENCY.
tinsert(cost, {"c", tonumber(m), itemValue});
else
-- Parse as an ITEM.
tinsert(cost, {"i", tonumber(itemLink:match("item:(%d+)")), itemValue});
end
end
end
end
-- Parse as an ITEM LINK.
tinsert(rawGroups, {["itemID"] = tonumber(link:match("item:(%d+)")), ["cost"] = cost});
end
end
local info = { [(ty == "GameObject") and "objectID" or "npcID"] = npcID };
info.faction = UnitFactionGroup("npc");
info.text = UnitName("npc");
info.g = rawGroups;
self:AddObject(info);
end
end);
elseif e == "GOSSIP_SHOW" then
local guid = UnitGUID("npc");
if guid then
local type, zero, server_id, instance_id, zone_uid, npcID, spawn_uid = strsplit("-",guid);
if npcID then
npcID = tonumber(npcID);
--print("GOSSIP_SHOW", type, npcID);
if type == "GameObject" then
info = { ["objectID"] = npcID, ["text"] = UnitName("npc") };
else
info = { ["npcID"] = npcID };
info.name = UnitName("npc");
end
info.faction = UnitFactionGroup("npc");
self:AddObject(info);
end
end
elseif e == "QUEST_DETAIL" then
local questStartItemID = ...;
local questID = GetQuestID();
if questID == 0 then return false; end
local npc = "questnpc";
local guid = UnitGUID(npc);
if not guid then
npc = "npc";
guid = UnitGUID(npc);
end
local type, zero, server_id, instance_id, zone_uid, npcID, spawn_uid;
if guid then type, zero, server_id, instance_id, zone_uid, npcID, spawn_uid = strsplit("-",guid); end
-- print("QUEST_DETAIL", questStartItemID, " => Quest #", questID, type, npcID);
local rawGroups = {};
for i=1,GetNumQuestRewards(),1 do
local link = GetQuestItemLink("reward", i);
if link then tinsert(rawGroups, { ["itemID"] = GetItemInfoInstant(link) }); end
end
for i=1,GetNumQuestChoices(),1 do
local link = GetQuestItemLink("choice", i);
if link then tinsert(rawGroups, { ["itemID"] = GetItemInfoInstant(link) }); end
end
for i=1,GetNumQuestLogRewardSpells(questID),1 do
local texture, name, isTradeskillSpell, isSpellLearned, hideSpellLearnText, isBoostSpell, garrFollowerID, genericUnlock, spellID = GetQuestLogRewardSpell(i, questID);
if spellID then
if isTradeskillSpell then
tinsert(rawGroups, { ["recipeID"] = spellID, ["name"] = name });
else
tinsert(rawGroups, { ["spellID"] = spellID, ["name"] = name });
end
end
end
local info = { ["questID"] = questID, ["description"] = GetQuestText(), ["objectives"] = GetObjectiveText(), ["g"] = rawGroups };
if questStartItemID and questStartItemID > 0 then info.itemID = questStartItemID; end
if npcID then
npcID = tonumber(npcID);
if type == "GameObject" then
info = { ["objectID"] = npcID, ["text"] = UnitName(npc), ["g"] = { info } };
else
info.qgs = {npcID};
info.name = UnitName(npc);
end
info.faction = UnitFactionGroup(npc);
end
self:AddObject(info);
elseif e == "CHAT_MSG_LOOT" then
local msg, player, a, b, c, d, e, f, g, h, i, j, k, l = ...;
local itemString = string.match(msg, "item[%-?%d:]+");
if itemString then
self:AddObject({ ["itemID"] = GetItemInfoInstant(itemString) });
end
end
end);
self:RegisterEvent("ADDON_LOADED");
self:RegisterEvent("GOSSIP_SHOW");
self:RegisterEvent("QUEST_DETAIL");
self:RegisterEvent("TRADE_SKILL_LIST_UPDATE");
self:RegisterEvent("ZONE_CHANGED_NEW_AREA");
self:RegisterEvent("ZONE_CHANGED");
self:RegisterEvent("MERCHANT_SHOW");
self:RegisterEvent("MERCHANT_UPDATE");
self:RegisterEvent("CHAT_MSG_LOOT");
--self:RegisterAllEvents();
end,
OnUpdate = function(self, ...)
-- Update the window and all of its row data
if self.data.OnUpdate then self.data.OnUpdate(self.data); end
for i,g in ipairs(self.data.g) do
if g.OnUpdate then g.OnUpdate(g); end
end
self.data.index = 0;
self.data.back = 1;
self.data.indent = 0;
BuildGroups(self.data);
UpdateWindow(self, true);
end
});
]]--
app:GetWindow("ItemFilter", {
parent = UIParent,
Silent = true,
OnUpdate = function(self, ...)
if not self.initialized then
self.initialized = true;
self.dirty = true;
-- Item Filter
local actions = {
['text'] = "Item Filters",
['icon'] = app.asset("Category_ItemSets"),
["description"] = "You can search the ATT Database by using a item filter.",
['visible'] = true,
['expanded'] = true,
['back'] = 1,
['OnUpdate'] = function(data)
if not self.dirty then return nil; end
self.dirty = nil;
local g = {};
tinsert(g, 1, data.setItemFilter);
if #data.results > 0 then
for i,result in ipairs(data.results) do
tinsert(g, result);
end
end
data.g = g;
if #g > 0 then
for i,entry in ipairs(g) do
entry.indent = nil;
end
data.indent = 0;
data.visible = true;
BuildGroups(data);
app.UpdateGroups(data, data.g);
if not data.expanded then
data.expanded = true;
ExpandGroupsRecursively(data, true);
end
end
-- Update the groups without forcing Debug Mode.
local visibilityFilter = app.VisibilityFilter;
app.VisibilityFilter = app.ObjectVisibilityFilter;
BuildGroups(self.data);
UpdateWindow(self, true);
app.VisibilityFilter = visibilityFilter;
end,
['g'] = {},
['results'] = {},
['setItemFilter'] = {
['text'] = "Set Item Filter",
['icon'] = "Interface\\Icons\\INV_MISC_KEY_12",
['description'] = "Click this to change the item filter you want to search for within ATT.",
['visible'] = true,
['OnClick'] = function(row, button)
app:ShowPopupDialogWithEditBox("Which Item Filter would you like to search for?", "", function(text)
text = string.lower(text);
local f = tonumber(text);
if tostring(f) ~= text then
-- The string form did not match, the filter must have been by name.
for id,filter in pairs(L["FILTER_ID_TYPES"]) do
if string.match(string.lower(filter), text) then
f = tonumber(id);
break;
end
end
end
if f then
self.data.results = app:BuildSearchResponse(app:GetDataCache().g, "f", f);
row.ref.f = f;
self.dirty = true;
end
wipe(searchCache);
self:Update();
end);
return true;
end,
['OnUpdate'] = app.AlwaysShowUpdate,
},
};
self.Reset = function()
self.data = actions;
end
self:Reset();
end
-- Update the window and all of its row data
if self.data.OnUpdate then self.data.OnUpdate(self.data, self); end
UpdateWindow(self, true);
end
});
app:GetWindow("ItemFinder", {
parent = UIParent,
Silent = true,
OnUpdate = function(self, ...)
if not self.initialized then
self.initialized = true;
self.data = {
text = "Item Finder",
icon = app.asset("WindowIcon_RaidAssistant"),
description = "This is a contribution debug tool. NOT intended to be used by the majority of the player base.\n\nUsing this tool will lag your WoW every 5 seconds. Not sure why - likely a bad Blizzard Database thing.",
visible = true,
expanded = true,
progress = 0,
total = 0,
back = 1,
currentItemID = 60000,
minimumItemID = 0,
g = {
{
text = "Update Now",
icon = app.asset("Button_Reroll"),
description = "Click this to update the listing. Doing so shall remove all invalid, grey, or white items.",
visible = true,
fails = 0,
OnClick = function(row, button)
self:Update(true);
return true;
end,
OnUpdate = app.AlwaysShowUpdate,
},
},
OnUpdate = function(header)
local g = header.g;
if g then
local count = #g;
if count > 0 then
for i=count,1,-1 do
if g[i].collected then
tremove(g, i);
end
end
end
for count=#g,100 do
local i = header.currentItemID - 1;
if i > header.minimumItemID then
header.currentItemID = i;
tinsert(g, app.CreateItemHarvester(i, {
parent = header
}));
end
end
self:DelayedUpdate(true);
self.delayRemaining = 1;
end
end
};
end
self.data.progress = 0;
self.data.total = 0;
UpdateGroups(self.data, self.data.g);
UpdateWindow(self, ...);
if self.data.OnUpdate then self.data.OnUpdate(self.data); end
end
});
app:GetWindow("Tradeskills", {
parent = UIParent,
Silent = true,
AllowCompleteSound = true,
OnInit = function(self, handlers)
SLASH_ATTSKILLS1 = "/attskills";
SLASH_ATTSKILLS2 = "/atttradeskill";
SLASH_ATTSKILLS3 = "/attprofession";
SLASH_ATTSKILLS4 = "/attprof";
SlashCmdList["ATTSKILLS"] = function(cmd)
self:Toggle();
end
self:SetMovable(false);
self:SetClampedToScreen(false);
self.wait = 5;
self.cache = {};
self.header = {
['text'] = "Profession List",
['icon'] = "Interface\\Icons\\INV_Scroll_04",
["description"] = "Open your professions to cache them.",
['visible'] = true,
['expanded'] = true,
["indent"] = 0,
['back'] = 1,
['g'] = { },
};
self.data = self.header;
self.previousCraftSkillID = 0;
self.previousTradeSkillID = 0;
self.CacheRecipes = function(self)
-- If it's not yours, don't take credit for it.
if IsTradeSkillLinked and IsTradeSkillLinked() then
return;
end
if C_TradeSkillUI then
if (C_TradeSkillUI.IsTradeSkillLinked and C_TradeSkillUI.IsTradeSkillLinked())
or (C_TradeSkillUI.IsTradeSkillGuild and C_TradeSkillUI.IsTradeSkillGuild()) then
return;
end
end
-- Cache Learned Spells
local skillCache = SearchForFieldContainer("spellID");
if skillCache then
-- Cache learned recipes and reagents
local reagentCache = app.GetDataMember("Reagents", {});
local learned, craftSkillID, tradeSkillID = 0, 0, 0;
rawset(app.SpellNameToSpellID, 0, nil);
app.GetSpellName(0);
if CraftFrame and CraftFrame:IsVisible() then
-- Crafting Skills (Enchanting and Beast Training Only)
local craftSkillName, craftSkillLevel, craftSkillMaxLevel = GetCraftDisplaySkillLine();
if craftSkillName and craftSkillName ~= "UNKNOWN" then
local shouldShowSpellRanks = craftSkillLevel and craftSkillLevel ~= math.max(300, craftSkillMaxLevel);
craftSkillID = app.SpellNameToSpellID[craftSkillName] or 0;
if craftSkillID == 0 then
app.print("Could not find spellID for", craftSkillName, GetLocale(), "! Please report this to the ATT Discord!");
end
elseif CraftFrameTitleText then
craftSkillName = CraftFrameTitleText:GetText();
craftSkillID = app.SpellNameToSpellID[craftSkillName] or 0;
if craftSkillID == 0 then
app.print("Could not find spellID for", craftSkillName, GetLocale(), "! Please report this to the ATT Discord!");
end
else
craftSkillID = 0;
end
end
if TradeSkillFrame and TradeSkillFrame:IsVisible() then
-- Trade Skills (Non-Enchanting)
local tradeSkillName, tradeSkillLevel, tradeSkillMaxLevel = GetTradeSkillLine();
if tradeSkillName and tradeSkillName ~= "UNKNOWN" then
local shouldShowSpellRanks = tradeSkillLevel and tradeSkillLevel ~= math.max(300, tradeSkillMaxLevel);
tradeSkillID = app.SpellNameToSpellID[tradeSkillName] or 0;
if tradeSkillID == 2656 then -- Smelting, point this to Mining.
tradeSkillID = 2575;
elseif tradeSkillID == 0 then
app.print("Could not find spellID for", tradeSkillName, GetLocale(), "! Please report this to the ATT Discord!");
end
else
tradeSkillID = 0;
end
end
if Skillet and Skillet.currentTrade and tradeSkillID == 0 and craftSkillID == 0 then
if Skillet.isCraft then
craftSkillID = Skillet.currentTrade;
else
tradeSkillID = Skillet.currentTrade;
end
end
if craftSkillID ~= 0 then
local spellName = GetSpellInfo(craftSkillID);
for _,spellID in pairs(app.SkillIDToSpellID) do
if GetSpellInfo(spellID) == spellName then
craftSkillID = spellID;
break;
end
end
local numberOfCrafts, spellID = GetNumCrafts();
for craftIndex = 1,numberOfCrafts do
spellID = 0;
local craftName, craftSubSpellName, craftType, numAvailable, isExpanded, trainingPointCost, requiredLevel = GetCraftInfo(craftIndex);
if craftType == "optimal" or craftType == "medium" or craftType == "easy" or craftType == "trivial" or craftType == "used" or craftType == "none" then
spellID = craftSubSpellName and (select(7, GetSpellInfo(craftName, craftSubSpellName)) or app.SpellNameToSpellID[craftName .. " (" .. craftSubSpellName .. ")"]) or app.SpellNameToSpellID[craftName];
if spellID then
if spellID == 44153 then spellID = 44155; -- Fix the Flying Machine spellID.
elseif spellID == 44151 then spellID = 44157; -- Fix the Turbo Flying Machine spellID.
elseif spellID == 20583 then spellID = 24492; end -- Fix rank 1 Nature Resistance.
app.CurrentCharacter.SpellRanks[spellID] = shouldShowSpellRanks and app.CraftTypeToCraftTypeID(craftType) or nil;
if not app.CurrentCharacter.Spells[spellID] then
app.SetCollectedForSubType(nil, "Spells", "Recipes", spellID, true);
learned = learned + 1;
end
if not skillCache[spellID] then
app.print("Missing " .. craftName .. " (Spell ID #" .. spellID .. ") in ATT Database. Please report it!");
skillCache[spellID] = { {} };
end
else
app.print("Missing " .. craftName .. " spellID in ATT Database. Please report it!");
end
if craftType ~= "none" then
-- Attempt to harvest the item associated with this craft.
GameTooltip.SetCraftSpell(ATTCNPCHarvester, craftIndex);
local link, craftedItemID = select(2, ATTCNPCHarvester:GetItem());
if link then craftedItemID = GetItemInfoInstant(link); end
-- Cache the Reagents used to make this item.
for i=1,GetCraftNumReagents(craftIndex) do
local itemID = GetItemInfoInstant(GetCraftReagentItemLink(craftIndex, i));
if itemID then
-- Make sure a cache table exists for this item.
local _, _, reagentCount = GetCraftReagentInfo(craftIndex, i);
if not reagentCache[itemID] then reagentCache[itemID] = { {}, {} }; end
-- Index 1: The Recipe Skill IDs
if spellID then reagentCache[itemID][1][spellID] = reagentCount; end
-- Index 2: The Crafted Item IDs
if craftedItemID then reagentCache[itemID][2][craftedItemID] = reagentCount; end
end
end
end
end
end
end
if tradeSkillID ~= 0 then
local spellName = GetSpellInfo(tradeSkillID);
for _,spellID in pairs(app.SkillIDToSpellID) do
if GetSpellInfo(spellID) == spellName then
tradeSkillID = spellID;
break;
end
end
local numTradeSkills = GetNumTradeSkills();
for skillIndex = 1,numTradeSkills do
local skillName, skillType, numAvailable, isExpanded = GetTradeSkillInfo(skillIndex);
if skillType == "optimal" or skillType == "medium" or skillType == "easy" or skillType == "trivial" or skillType == "used" or skillType == "none" then
local spellID = app.SpellNameToSpellID[skillName];
if spellID then
if spellID == 44153 then spellID = 44155; -- Fix the Flying Machine spellID.
elseif spellID == 44151 then spellID = 44157; -- Fix the Turbo Flying Machine spellID.
elseif spellID == 61451 then spellID = 60969; -- Fix the Flying Carpet spellID.
elseif spellID == 61309 then spellID = 60971; -- Fix the Magnificent Flying Carpet spellID.
elseif spellID == 75596 then spellID = 75597; -- Fix the Frosty Flying Carpet spellID.
elseif spellID == 20583 then spellID = 24492; end -- Fix rank 1 Nature Resistance.
app.CurrentCharacter.SpellRanks[spellID] = shouldShowSpellRanks and app.CraftTypeToCraftTypeID(skillType) or nil;
if not app.CurrentCharacter.Spells[spellID] then
app.SetCollectedForSubType(nil, "Spells", "Recipes", spellID, true);
learned = learned + 1;
end
if not skillCache[spellID] then
app.print("Missing " .. (skillName or "[??]") .. " (Spell ID #" .. spellID .. ") in ATT Database. Please report it!");
skillCache[spellID] = { {} };
end
else
app.print("Missing " .. (skillName or "[??]") .. " spellID in ATT Database. Please report it!");
end
-- Cache the Reagents used to make this item.
local tradeSkillItemLink = GetTradeSkillItemLink(skillIndex);
if tradeSkillItemLink then
local craftedItemID = GetItemInfoInstant(tradeSkillItemLink);
for i=1,GetTradeSkillNumReagents(skillIndex) do
local reagentCount = select(3, GetTradeSkillReagentInfo(skillIndex, i));
local itemID = GetItemInfoInstant(GetTradeSkillReagentItemLink(skillIndex, i));
-- Make sure a cache table exists for this item.
-- Index 1: The Recipe Skill IDs
-- Index 2: The Crafted Item IDs
if not reagentCache[itemID] then reagentCache[itemID] = { {}, {} }; end
if spellID then reagentCache[itemID][1][spellID] = reagentCount; end
if craftedItemID then reagentCache[itemID][2][craftedItemID] = reagentCount; end
end
end
end
end
end
-- Open the Tradeskill list for this Profession
if app.Categories.Professions and (craftSkillID ~= 0 or tradeSkillID ~= 0) and (craftSkillID ~= self.previousCraftSkillID or tradeSkillID ~= self.previousTradeSkillID) then
self.previousCraftSkillID = craftSkillID;
self.previousTradeSkillID = tradeSkillID;
local g = {};
for i,group in ipairs(app.Categories.Professions) do
if group.spellID == craftSkillID or group.spellID == tradeSkillID then
local cache = self.cache[group.spellID];
if not cache then
cache = CloneReference(group);
self.cache[group.spellID] = cache;
local searchResults = ResolveSymbolicLink(group);
if searchResults and #searchResults then
for j,o in ipairs(searchResults) do
tinsert(cache.g, o);
end
end
end
tinsert(g, cache);
end
end
if #g > 0 then
if #g == 1 then
self.data = g[1];
else
self.data = self.header;
self.data.g = g;
for i,entry in ipairs(g) do
entry.indent = nil;
end
end
self.data.indent = 0;
self.data.visible = true;
if not self.data.expanded then
self.data.expanded = true;
ExpandGroupsRecursively(self.data, true);
end
self:Rebuild();
end
end
-- If something new was "learned", then refresh the data.
if learned > 0 then
app.print("Cached " .. learned .. " known recipes!");
app:RefreshDataQuietly("TradeSkills::CacheRecipes", true);
end
end
end
self.RefreshRecipes = function(self)
if app.Settings.Collectibles.Recipes then
self.wait = 5;
app:StartATTCoroutine("RefreshingRecipes", function()
while self.wait > 0 do
self.wait = self.wait - 1;
coroutine.yield();
end
while not self:IsVisible() do
coroutine.yield();
end
wipe(searchCache);
self:CacheRecipes();
end);
end
end
-- Skillet support.
self.SkilletSupported = nil;
-- TSM Shenanigans
self.TSMCraftingVisible = nil;
self.SetTSMCraftingVisible = function(self, visible)
visible = not not visible;
if visible == self.TSMCraftingVisible then
return;
end
self.TSMCraftingVisible = visible;
self:UpdateFrameVisibility();
app:StartATTCoroutine("UpdateTradeSkills", function()
while InCombatLockdown() do coroutine.yield(); end
coroutine.yield();
self:Update();
end);
end
self.UpdateDefaultFrameVisibility = function(self)
-- Skillet compatibility
if SkilletFrame then
if not self.SkilletSupported then
Skillet["ATTC"] = { ["Update"] = function() self:Update(); end };
Skillet:RegisterUpdatePlugin("ATTC");
self.SkilletSupported = true;
end
self:SetParent(SkilletFrame);
self:SetPoint("TOPLEFT", SkilletFrame, "TOPRIGHT", 0, 0);
self:SetPoint("BOTTOMLEFT", SkilletFrame, "BOTTOMRIGHT", 0, 0);
self:SetMovable(false);
return true;
elseif CraftFrame and CraftFrame:IsVisible() then
-- Default Alignment on the Craft UI.
self:ClearAllPoints();
self:SetPoint("TOPLEFT", CraftFrame, "TOPRIGHT", -37, -11);
self:SetPoint("BOTTOMLEFT", CraftFrame, "BOTTOMRIGHT", -37, 72);
self:SetMovable(false);
return true;
elseif TradeSkillFrame and TradeSkillFrame:IsVisible() then
-- Default Alignment on the TradeSkill UI.
self:ClearAllPoints();
self:SetPoint("TOPLEFT", TradeSkillFrame, "TOPRIGHT", -37, -11);
self:SetPoint("BOTTOMLEFT", TradeSkillFrame, "BOTTOMRIGHT", -37, 72);
self:SetMovable(false);
return true;
end
end
self.UpdateFrameVisibility = function(self)
self:SetMovable(true);
self:ClearAllPoints();
if self.TSMCraftingVisible and self.cachedTSMFrame then
if self.cachedTSMFrame.queue and self.cachedTSMFrame.queue:IsShown() then
self:SetPoint("TOPLEFT", self.cachedTSMFrame.queue, "TOPRIGHT", 0, 0);
self:SetPoint("BOTTOMLEFT", self.cachedTSMFrame.queue, "BOTTOMRIGHT", 0, 0);
else
self:SetPoint("TOPLEFT", self.cachedTSMFrame, "TOPRIGHT", 0, 0);
self:SetPoint("BOTTOMLEFT", self.cachedTSMFrame, "BOTTOMRIGHT", 0, 0);
end
self:SetMovable(false);
return true;
elseif self:UpdateDefaultFrameVisibility() then
return true;
else
self:SetMovable(false);
app:StartATTCoroutine("TSMWHY", function()
while InCombatLockdown() or not TradeSkillFrame do coroutine.yield(); end
app:StartATTCoroutine("TSMWHYPT2", function()
local thing = self.TSMCraftingVisible;
self.TSMCraftingVisible = nil;
self:SetTSMCraftingVisible(thing);
end);
end);
end
end
-- Setup Event Handlers and register for events
local updateTradeSkill = function()
-- If it's not yours, don't take credit for it.
if IsTradeSkillLinked and IsTradeSkillLinked() then
return;
end
if C_TradeSkillUI then
if (C_TradeSkillUI.IsTradeSkillLinked and C_TradeSkillUI.IsTradeSkillLinked())
or (C_TradeSkillUI.IsTradeSkillGuild and C_TradeSkillUI.IsTradeSkillGuild()) then
return;
end
end
if self.TSMCraftingVisible == nil then
self:SetTSMCraftingVisible(false);
end
self:UpdateFrameVisibility();
if app.Settings:GetTooltipSetting("Auto:ProfessionList") then
self:SetVisible(true);
end
RefreshSkills();
self:RefreshRecipes();
end
handlers.CRAFT_SHOW = updateTradeSkill;
handlers.TRADE_SKILL_SHOW = updateTradeSkill;
pcall(self.RegisterEvent, self, "CRAFT_SHOW");
self:RegisterEvent("TRADE_SKILL_SHOW");
local tradeSkillClose = function()
app:StartATTCoroutine("TSMWHY3", function()
self:RefreshRecipes();
if not self:UpdateFrameVisibility() then
self:SetVisible(false);
end
end);
end
handlers.CRAFT_CLOSE = tradeSkillClose;
handlers.TRADE_SKILL_CLOSE = tradeSkillClose;
pcall(self.RegisterEvent, self, "CRAFT_CLOSE");
self:RegisterEvent("TRADE_SKILL_CLOSE");
local newSpellLearned = function(self, spellID)
if spellID then
if not app.CurrentCharacter.Spells[spellID] then
local searchResults, spell = SearchForField("spellID", spellID);
if #searchResults > 0 then
spell = searchResults[1];
for i=2,#searchResults,1 do
local searchResult = searchResults[i];
if not searchResult.itemID then
spell = searchResult;
end
end
else
spell = app.CreateSpell(spellID);
end
if spell.f == app.FilterConstants.RECIPES then
app.SetCollectedForSubType(spell, "Spells", "Recipes", spellID, true);
else
app.SetCollected(spell, "Spells", spellID, true);
end
app:RefreshDataQuietly("NEW_SPELL_LEARNED", true);
else
self:RefreshRecipes();
end
end
end
handlers.NEW_RECIPE_LEARNED = newSpellLearned;
handlers.LEARNED_SPELL_IN_TAB = newSpellLearned;
self:RegisterEvent("LEARNED_SPELL_IN_TAB");
self:RegisterEvent("NEW_RECIPE_LEARNED");
-- Default Update refreshes
pcall(self.RegisterEvent, self, "CRAFT_UPDATE");
self:RegisterEvent("TRADE_SKILL_LIST_UPDATE");
self:RegisterEvent("SKILL_LINES_CHANGED");
end,
OnUpdate = function(self, ...)
if TSM_API and TSMAPI_FOUR then
if not self.cachedTSMFrame then
for i,f in ipairs({UIParent:GetChildren()}) do
if f.headerBgCenter then
self.cachedTSMFrame = f;
local oldSetVisible = f.SetVisible;
local oldShow = f.Show;
local oldHide = f.Hide;
f.SetVisible = function(s, visible)
oldSetVisible(s, visible);
self:SetTSMCraftingVisible(visible);
end
f.Hide = function(s)
oldHide(s);
self:SetTSMCraftingVisible(false);
end
f.Show = function(s)
oldShow(s);
self:SetTSMCraftingVisible(true);
end
if self.gettinMadAtDumbNamingConventions then
TSMAPI_FOUR.UI.NewElement = self.OldNewElement;
self.gettinMadAtDumbNamingConventions = nil;
self.OldNewElement = nil;
end
self:SetTSMCraftingVisible(f:IsShown());
return;
end
end
if not self.gettinMadAtDumbNamingConventions then
self.gettinMadAtDumbNamingConventions = true;
self.OldNewElement = TSMAPI_FOUR.UI.NewElement;
TSMAPI_FOUR.UI.NewElement = function(...)
app:StartATTCoroutine("UpdateTradeSkills", function()
while InCombatLockdown() do coroutine.yield(); end
coroutine.yield();
self:Update();
end);
return self.OldNewElement(...);
end
end
end
elseif TSMCraftingTradeSkillFrame then
if not self.cachedTSMFrame then
local f = TSMCraftingTradeSkillFrame;
self.cachedTSMFrame = f;
local oldSetVisible = f.SetVisible;
local oldShow = f.Show;
local oldHide = f.Hide;
f.SetVisible = function(s, visible)
oldSetVisible(s, visible);
self:SetTSMCraftingVisible(visible);
end
f.Hide = function(s)
oldHide(s);
self:SetTSMCraftingVisible(false);
end
f.Show = function(s)
oldShow(s);
self:SetTSMCraftingVisible(true);
end
if f.queueBtn then
local setScript = f.queueBtn.SetScript;
f.queueBtn.SetScript = function(s, e, callback)
if e == "OnClick" then
setScript(s, e, function(...)
if callback then callback(...); end
local thing = self.TSMCraftingVisible;
self.TSMCraftingVisible = nil;
self:SetTSMCraftingVisible(thing);
end);
else
setScript(s, e, callback);
end
end
f.queueBtn:SetScript("OnClick", f.queueBtn:GetScript("OnClick"));
end
self:SetTSMCraftingVisible(f:IsShown());
return;
end
end
UpdateWindow(self, ...);
end,
});
-- Addon Message Handling
app:RegisterEvent("CHAT_MSG_ADDON");
app.events.CHAT_MSG_ADDON = function(prefix, text, channel, sender, target, zoneChannelID, localID, name, instanceID, ...)
if prefix == "ATTC" then
--print(prefix, text, channel, sender, target, zoneChannelID, localID, name, instanceID, ...)
local args = { strsplit("\t", text) };
local cmd = args[1];
if cmd then
local a = args[2];
if cmd == "?" then -- Query Request
local response;
if a then
if a == "a" then
response = a;
for i=3,#args,1 do
local b = tonumber(args[i]);
response = response .. "\t" .. b .. "\t" .. (app.CurrentCharacter.Achievements[b] and 1 or 0);
end
elseif a == "e" then
response = a;
for i=3,#args,1 do
local b = tonumber(args[i]);
response = response .. "\t" .. b .. "\t" .. (app.CurrentCharacter.Exploration[b] and 1 or 0);
end
elseif a == "f" then
response = a;
for i=3,#args,1 do
local b = tonumber(args[i]);
response = response .. "\t" .. b .. "\t" .. (app.CurrentCharacter.Factions[b] and 1 or 0);
end
elseif a == "fp" then
response = a;
for i=3,#args,1 do
local b = tonumber(args[i]);
response = response .. "\t" .. b .. "\t" .. (app.CurrentCharacter.FlightPaths[b] and 1 or 0);
end
elseif a == "p" then
response = a;
for i=3,#args,1 do
local b = tonumber(args[i]);
response = response .. "\t" .. b .. "\t" .. (app.CurrentCharacter.BattlePets[b] and 1 or 0);
end
elseif a == "q" then
response = a;
for i=3,#args,1 do
local b = tonumber(args[i]);
response = response .. "\t" .. b .. "\t" .. (IsQuestFlaggedCompleted(b) and 1 or 0);
end
--[[
elseif a == "s" then
response = "s";
for i=3,#args,1 do
local b = tonumber(args[i]);
response = response .. "\t" .. b .. "\t" .. (ATTAccountWideData.Sources[b] or 0);
end
]]--
elseif a == "sp" then
response = a;
for i=3,#args,1 do
local b = tonumber(args[i]);
response = response .. "\t" .. b .. "\t" .. (app.CurrentCharacter.Spells[b] and 1 or 0);
end
elseif a == "t" then
response = a;
for i=3,#args,1 do
local b = tonumber(args[i]);
response = response .. "\t" .. b .. "\t" .. (app.CurrentCharacter.Titles[b] and 1 or 0);
end
elseif a == "toy" then
response = a;
for i=3,#args,1 do
local b = tonumber(args[i]);
response = response .. "\t" .. b .. "\t" .. (ATTAccountWideData.Toys[b] and 1 or 0);
end
end
else
local character = app.CurrentCharacter;
if character then
local data = character.PrimeData;
if data then
response = "ATTC\t" .. (data.progress or 0) .. "\t" .. (data.total or 0) .. "\t" .. data.modeString;
end
end
end
if response then SendResponseMessage("!\t" .. response, sender); end
elseif cmd == "!" then -- Query Response
if a == "ATTC" then
print(target .. ": " .. GetProgressColorText(tonumber(args[3]), tonumber(args[4])) .. " " .. args[5]);
end
elseif cmd == "to" then -- To Command
local myName = UnitName("player");
local name,server = strsplit("-", a);
if myName == name and (not server or GetRealmName() == server) then
app.events.CHAT_MSG_ADDON(prefix, strsub(text, 5 + strlen(a)), "WHISPER", sender);
end
end
end
end
end
SLASH_ATTGUILD1 = "/attguild";
SlashCmdList["ATTGUILD"] = function(cmd)
C_ChatInfo.SendAddonMessage("ATTC", "?", "GUILD");
end
SLASH_ATTRAID1 = "/attraid";
SlashCmdList["ATTRAID"] = function(cmd)
C_ChatInfo.SendAddonMessage("ATTC", "?", "RAID");
end
SLASH_ATTYELL1 = "/attyell";
SLASH_ATTYELL2 = "/attrohduh";
SlashCmdList["ATTYELL"] = function(cmd)
C_ChatInfo.SendAddonMessage("ATTC", "?", "YELL");
end
SLASH_ATTWHO1 = "/attu";
SLASH_ATTWHO2 = "/attyou";
SLASH_ATTWHO3 = "/attwho";
SlashCmdList["ATTWHO"] = function(cmd)
local name,server = UnitName("target");
if name then
if UnitIsPlayer("target") then
SendResponseMessage("?", server and (name .. "-" .. server) or name);
else
local cmd = "creatureid:" .. select(6, strsplit("-", UnitGUID("target")));
local group = GetCachedSearchResults(cmd, SearchForLink, cmd);
if group then app:CreateMiniListForGroup(group); end
end
end
end
-- Game Events that trigger computation updates.
app.events.PLAYER_LEVEL_UP = function(newLevel)
app.Level = newLevel;
app:RefreshDataCompletely("PLAYER_LEVEL_UP");
end
-- Startup Event
app:RegisterEvent("ADDON_LOADED");
app:RegisterEvent("VARIABLES_LOADED");
app.events.ADDON_LOADED = function(addonName)
-- Only execute for this addon.
if addonName ~= appName then return; end
app:UnregisterEvent("ADDON_LOADED");
AllTheThingsAD = _G["AllTheThingsAD"]; -- For account-wide data.
if not AllTheThingsAD then
AllTheThingsAD = _G["ATTClassicAD"];
if AllTheThingsAD then
_G["ATTClassicAD"] = nil;
else
AllTheThingsAD = { };
end
_G["AllTheThingsAD"] = AllTheThingsAD;
end
app:UpdateWindowColors();
LibStub:GetLibrary("LibDataBroker-1.1"):NewDataObject(L["TITLE"], {
type = "launcher",
icon = app.asset("logo_32x32"),
OnClick = AllTheThings_MinimapButtonOnClick,
OnEnter = AllTheThings_MinimapButtonOnEnter,
OnLeave = AllTheThings_MinimapButtonOnLeave,
});
-- 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.GarrisonBuildings then currentCharacter.GarrisonBuildings = {}; end
if not currentCharacter.Lockouts then currentCharacter.Lockouts = {}; end
if not currentCharacter.Quests then currentCharacter.Quests = {}; end
if not currentCharacter.RWP then currentCharacter.RWP = {}; 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.Toys then currentCharacter.Toys = {}; 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;
-- Convert over the deprecated Characters table.
local characters = GetDataMember("Characters");
if characters then
for guid,text in pairs(characters) do
if not characterData[guid] then
characterData[guid] = { ["text"] = text };
end
end
end
-- Convert over the deprecated DeathsPerCharacter table.
local deathsPerCharacter = GetDataMember("DeathsPerCharacter");
if deathsPerCharacter then
for guid,deaths in pairs(deathsPerCharacter) do
local character = characterData[guid];
if not character then
character = { ["guid"] = guid };
characterData[guid] = character;
end
character.Deaths = deaths;
end
end
-- Convert over the deprecated ActiveSkillsPerCharacter table.
local activeSkillsPerCharacter = GetDataMember("ActiveSkillsPerCharacter");
if activeSkillsPerCharacter then
for guid,skills in pairs(activeSkillsPerCharacter) do
local character = characterData[guid];
if not character then
character = { ["guid"] = guid };
characterData[guid] = character;
end
character.ActiveSkills = skills;
end
end
-- Convert over the deprecated CollectedFlightPathsPerCharacter table.
local collectedFlightPathsPerCharacter = GetDataMember("CollectedFlightPathsPerCharacter");
if collectedFlightPathsPerCharacter then
for guid,flightPaths in pairs(collectedFlightPathsPerCharacter) do
local character = characterData[guid];
if not character then
character = { ["guid"] = guid };
characterData[guid] = character;
end
character.FlightPaths = flightPaths;
end
end
-- Convert over the deprecated CollectedFactionsPerCharacter table.
local collectedFactionsPerCharacter = GetDataMember("CollectedFactionsPerCharacter");
if collectedFactionsPerCharacter then
for guid,factions in pairs(collectedFactionsPerCharacter) do
local character = characterData[guid];
if not character then
character = { ["guid"] = guid };
characterData[guid] = character;
end
character.Factions = factions;
end
end
-- Convert over the deprecated lockouts table.
local lockouts = GetDataMember("lockouts");
if lockouts then
for guid,locks in pairs(lockouts) do
local character = characterData[guid];
if not character then
character = { ["guid"] = guid };
characterData[guid] = character;
end
character.Lockouts = locks;
end
end
-- Convert over the deprecated CollectedQuestsPerCharacter table.
local collectedQuestsPerCharacter = GetDataMember("CollectedQuestsPerCharacter");
if collectedQuestsPerCharacter then
for guid,quests in pairs(collectedQuestsPerCharacter) do
local character = characterData[guid];
if not character then
character = { ["guid"] = guid };
characterData[guid] = character;
end
character.Quests = quests;
end
end
-- Convert over the deprecated CollectedSpellsPerCharacter table.
local collectedSpellsPerCharacter = GetDataMember("CollectedSpellsPerCharacter");
if collectedSpellsPerCharacter then
for guid,spells in pairs(collectedSpellsPerCharacter) do
local character = characterData[guid];
if not character then
character = { ["guid"] = guid };
characterData[guid] = character;
end
character.Spells = spells;
end
end
-- Convert over the deprecated SpellRanksPerCharacter table.
local spellRanksPerCharacter = GetDataMember("SpellRanksPerCharacter");
if spellRanksPerCharacter then
for guid,ranks in pairs(spellRanksPerCharacter) do
local character = characterData[guid];
if not character then
character = { ["guid"] = guid };
characterData[guid] = character;
end
character.SpellRanks = ranks;
end
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.GarrisonBuildings then accountWideData.GarrisonBuildings = {}; end
if not accountWideData.Illusions then accountWideData.Illusions = {}; end
if not accountWideData.Quests then accountWideData.Quests = {}; end
if not accountWideData.RWP then accountWideData.RWP = {}; end
if not accountWideData.Spells then accountWideData.Spells = {}; end
if not accountWideData.Titles then accountWideData.Titles = {}; end
if not accountWideData.Toys then accountWideData.Toys = {}; end
-- Account Wide Settings
local accountWideSettings = app.Settings.AccountWide;
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;
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;
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.SetAccountCollected = SetAccountCollected;
app.SetAccountCollectedForSubType = SetAccountCollectedForSubType;
app.SetCollected = SetCollected;
app.SetCollectedForSubType = SetCollectedForSubType;
-- Convert over the deprecated account wide tables.
local data = GetDataMember("Deaths");
if data then accountWideData.Deaths = data; end
data = GetDataMember("CollectedFactions");
if data then accountWideData.Factions = data; end
data = GetDataMember("CollectedFlightPaths");
if data then accountWideData.FlightPaths = data; end
data = GetDataMember("CollectedQuests");
if data then accountWideData.Quests = data; end
data = GetDataMember("CollectedSpells");
if data then accountWideData.Spells = data; end
-- Check to see if we have a leftover ItemDB cache
GetDataMember("GroupQuestsByGUID", {});
GetDataMember("ValidSuffixesPerItemID", {});
-- Clean up settings
local oldsettings = {};
for i,key in ipairs({
"GroupQuestsByGUID",
"LocalizedCategoryNames",
"LocalizedFlightPathNames",
"Position",
"Reagents",
"SoftReserves",
"SoftReservePersistence",
"ValidSuffixesPerItemID",
}) do
oldsettings[key] = AllTheThingsAD[key];
end
wipe(AllTheThingsAD);
for key,value in pairs(oldsettings) do
AllTheThingsAD[key] = value;
end
-- Wipe the Debugger Data
ATTClassicDebugData = nil;
-- Tooltip Settings
app.Settings:Initialize();
end
app.events.VARIABLES_LOADED = function()
app:StartATTCoroutine("Startup", function()
coroutine.yield();
-- Prepare the Sound Pack!
app.Audio:ReloadSoundPack();
-- Cache some things
app.CurrentMapID = app.GetCurrentMapID();
-- Mark all previously completed quests.
if C_QuestLog_GetAllCompletedQuestIDs then
local completedQuests = C_QuestLog_GetAllCompletedQuestIDs();
if completedQuests and #completedQuests > 0 then
for i,questID in ipairs(completedQuests) do
CompletedQuests[questID] = true;
end
end
else
GetQuestsCompleted(CompletedQuests);
end
wipe(DirtyQuests);
app.events.UPDATE_INSTANCE_INFO();
C_ChatInfo.RegisterAddonMessagePrefix("ATTC");
-- Setup the Saved Variables if they aren't already.
local savedVariables = AllTheThingsSavedVariables;
if not AllTheThingsSavedVariables then
savedVariables = {};
AllTheThingsSavedVariables = savedVariables;
end
local windowSettings = savedVariables.Windows;
if not windowSettings then
windowSettings = {};
savedVariables.Windows = windowSettings;
end
LoadSettingsForWindows(windowSettings);
-- Now that all the windows are loaded, cache flight paths!
app.CacheFlightPathData();
-- Execute the OnReady handlers.
for i,handler in ipairs(app.EventHandlers.OnReady) do
handler();
end
-- Mark that we're ready now!
app.IsReady = true;
end);
end