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.

5934 lines
214 KiB

--------------------------------------------------------------------------------
-- A L L T H E T H I N G S --
--------------------------------------------------------------------------------
-- Copyright 2017-2025 Dylan Fortune (Crieve-Sargeras) --
--------------------------------------------------------------------------------
-- App locals
local appName, app = ...;
local L = app.L;
local AssignChildren, GetRelativeValue, IsQuestFlaggedCompleted, GetRelativeGroup
= app.AssignChildren, app.GetRelativeValue, app.IsQuestFlaggedCompleted, app.GetRelativeGroup
-- Abbreviations
L.ABBREVIATIONS[L.UNSORTED .. " %> " .. L.UNSORTED] = "|T" .. app.asset("WindowIcon_Unsorted") .. ":0|t " .. L.SHORTTITLE .. " %> " .. L.UNSORTED;
-- Binding Localizations
BINDING_HEADER_ALLTHETHINGS = L.TITLE
BINDING_NAME_ALLTHETHINGS_TOGGLEACCOUNTMODE = L.TOGGLE_ACCOUNT_MODE
BINDING_NAME_ALLTHETHINGS_TOGGLECOMPLETIONISTMODE = L.TOGGLE_COMPLETIONIST_MODE
BINDING_NAME_ALLTHETHINGS_TOGGLEDEBUGMODE = L.TOGGLE_DEBUG_MODE
BINDING_NAME_ALLTHETHINGS_TOGGLEFACTIONMODE = L.TOGGLE_FACTION_MODE
BINDING_NAME_ALLTHETHINGS_TOGGLELOOTMODE = L.TOGGLE_LOOT_MODE
BINDING_HEADER_ALLTHETHINGS_PREFERENCES = PREFERENCES
BINDING_NAME_ALLTHETHINGS_TOGGLECOMPLETEDTHINGS = L.TOGGLE_COMPLETEDTHINGS
BINDING_NAME_ALLTHETHINGS_TOGGLECOMPLETEDGROUPS = L.TOGGLE_COMPLETEDGROUPS
BINDING_NAME_ALLTHETHINGS_TOGGLECOLLECTEDTHINGS = L.TOGGLE_COLLECTEDTHINGS
BINDING_NAME_ALLTHETHINGS_TOGGLEBOEITEMS = L.TOGGLE_BOEITEMS
BINDING_NAME_ALLTHETHINGS_TOGGLESOURCETEXT = L.TOGGLE_SOURCETEXT
BINDING_HEADER_ALLTHETHINGS_MODULES = L.MODULES
BINDING_NAME_ALLTHETHINGS_TOGGLEMAINLIST = L.TOGGLE_MAINLIST
BINDING_NAME_ALLTHETHINGS_TOGGLEMINILIST = L.TOGGLE_MINILIST
BINDING_NAME_ALLTHETHINGS_TOGGLE_PROFESSION_LIST = L.TOGGLE_PROFESSION_LIST
BINDING_NAME_ALLTHETHINGS_TOGGLE_RAID_ASSISTANT = L.TOGGLE_RAID_ASSISTANT
BINDING_NAME_ALLTHETHINGS_TOGGLE_WORLD_QUESTS_LIST = L.TOGGLE_WORLD_QUESTS_LIST
BINDING_NAME_ALLTHETHINGS_TOGGLERANDOM = L.TOGGLE_RANDOM
BINDING_NAME_ALLTHETHINGS_REROLL_RANDOM = L.REROLL_RANDOM
-- Performance Cache
local print,rawget,rawset,tostring,ipairs,pairs,tonumber,wipe,select,setmetatable,getmetatable,tinsert,tremove,type,math_floor,GetTime
= print,rawget,rawset,tostring,ipairs,pairs,tonumber,wipe,select,setmetatable,getmetatable,tinsert,tremove,type,math.floor,GetTime
-- Global WoW API Cache
local C_Map_GetMapInfo = C_Map.GetMapInfo;
local InCombatLockdown = _G.InCombatLockdown;
local IsInInstance = IsInInstance
-- WoW API Cache;
local GetSpellName = app.WOWAPI.GetSpellName;
local GetTradeSkillTexture = app.WOWAPI.GetTradeSkillTexture;
local C_TradeSkillUI = C_TradeSkillUI;
local C_TradeSkillUI_GetCategories, C_TradeSkillUI_GetCategoryInfo, C_TradeSkillUI_GetRecipeInfo, C_TradeSkillUI_GetRecipeSchematic, C_TradeSkillUI_GetTradeSkillLineForRecipe
= C_TradeSkillUI.GetCategories, C_TradeSkillUI.GetCategoryInfo, C_TradeSkillUI.GetRecipeInfo, C_TradeSkillUI.GetRecipeSchematic, C_TradeSkillUI.GetTradeSkillLineForRecipe;
-- App & Module locals
local ArrayAppend = app.ArrayAppend
local CacheFields, SearchForField, SearchForFieldContainer, SearchForObject
= app.CacheFields, app.SearchForField, app.SearchForFieldContainer, app.SearchForObject
local IsRetrieving = app.Modules.RetrievingData.IsRetrieving;
local GetProgressColorText = app.Modules.Color.GetProgressColorText;
local CleanLink = app.Modules.Item.CleanLink
local TryColorizeName = app.TryColorizeName;
local MergeProperties = app.MergeProperties
local wipearray = app.wipearray
local DESCRIPTION_SEPARATOR = app.DESCRIPTION_SEPARATOR;
local ATTAccountWideData;
local
CreateObject,
MergeObject,
NestObject,
MergeObjects,
NestObjects,
PriorityNestObjects
=
app.__CreateObject,
app.MergeObject,
app.NestObject,
app.MergeObjects,
app.NestObjects,
app.PriorityNestObjects
-- Color Lib;
local Colorize = app.Modules.Color.Colorize;
-- Coroutine Helper Functions
local Push = app.Push;
local StartCoroutine = app.StartCoroutine;
local Callback = app.CallbackHandlers.Callback;
local DelayedCallback = app.CallbackHandlers.DelayedCallback;
local AfterCombatCallback = app.CallbackHandlers.AfterCombatCallback;
app.FillRunner = app.CreateRunner("fill");
local LocalizeGlobal = app.LocalizeGlobal
local LocalizeGlobalIfAllowed = app.LocalizeGlobalIfAllowed
local contains = app.contains;
local indexOf = app.indexOf;
-- Data Lib
local AllTheThingsAD = {}; -- For account-wide data.
do -- TradeSkill Functionality
local tradeSkillSpecializationMap = app.SkillDB.Specializations
local specializationTradeSkillMap = app.SkillDB.BaseSkills
local tradeSkillMap = app.SkillDB.Conversion
local function GetBaseTradeSkillID(skillID)
return tradeSkillMap[skillID] or skillID;
end
local function GetTradeSkillSpecialization(skillID)
return tradeSkillSpecializationMap[skillID];
end
app.GetTradeSkillLine = function()
local profInfo = C_TradeSkillUI.GetBaseProfessionInfo();
return GetBaseTradeSkillID(profInfo.professionID);
end
app.GetSpecializationBaseTradeSkill = function(specializationID)
return specializationTradeSkillMap[specializationID];
end
-- Refreshes the known Trade Skills/Professions of the current character (app.CurrentCharacter.Professions)
local function RefreshTradeSkillCache()
local cache = app.CurrentCharacter.Professions;
wipe(cache);
-- "Professions" that anyone can "know"
for _,skillID in ipairs(app.SkillDB.AlwaysAvailable) do
cache[skillID] = true
end
-- app.PrintDebug("RefreshTradeSkillCache");
local prof1, prof2, archaeology, fishing, cooking, firstAid = GetProfessions();
for i,j in ipairs({prof1 or 0, prof2 or 0, archaeology or 0, fishing or 0, cooking or 0, firstAid or 0}) do
if j ~= 0 then
local prof = select(7, GetProfessionInfo(j));
cache[GetBaseTradeSkillID(prof)] = true;
-- app.PrintDebug("KnownProfession",j,GetProfessionInfo(j));
local specializations = GetTradeSkillSpecialization(prof);
if specializations ~= nil then
for _,spellID in pairs(specializations) do
if spellID and app.IsSpellKnownHelper(spellID) then
cache[spellID] = true;
end
end
end
end
end
end
app.AddEventHandler("OnStartup", RefreshTradeSkillCache)
app.AddEventHandler("OnStartup", function()
local conversions = app.Settings.InformationTypeConversionMethods;
conversions.professionName = function(skillID)
local texture = GetTradeSkillTexture(skillID or 0)
local name = GetSpellName(app.SkillDB.SkillToSpell[skillID] or 0) or C_TradeSkillUI.GetTradeSkillDisplayName(skillID) or RETRIEVING_DATA
return texture and "|T"..texture..":0|t "..name or name
end;
end);
app.AddEventRegistration("SKILL_LINES_CHANGED", function()
-- app.PrintDebug("SKILL_LINES_CHANGED")
-- seems to be a reliable way to notice a player has changed professions? not sure how else often it actually triggers... hopefully not too excessive...
DelayedCallback(RefreshTradeSkillCache, 2);
end)
end -- TradeSkill Functionality
local GetSpecsString, GetGroupItemIDWithModID, GroupMatchesParams, GetClassesString
= app.GetSpecsString, app.GetGroupItemIDWithModID, app.GroupMatchesParams, app.GetClassesString
do
local ContainsLimit, ContainsExceeded;
local Indicator, GetProgressTextForRow, GetUnobtainableTexture
app.AddEventHandler("OnLoad", function()
GetProgressTextForRow = app.GetProgressTextForRow
Indicator = app.GetIndicatorIcon
GetUnobtainableTexture = app.GetUnobtainableTexture
end)
local MaxLayer = 4
local Indents = {
" ",
}
for i=2,MaxLayer do
Indents[i] = Indents[i-1].." "
end
local ContainsTypesIndicators
app.AddEventHandler("OnStartup", function() ContainsTypesIndicators = app.Modules.Fill.Settings.Icons end)
local function BuildContainsInfo(root, entries, indent, layer)
local subgroups = root and root.g
if not subgroups or #subgroups == 0 then return end
for _,group in ipairs(subgroups) do
-- If there's progress to display for a non-sourceIgnored group, then let's summarize a bit better.
if group.visible and not group.sourceIgnored and not group.skipContains then
-- Special case to ignore 'container' layers where the container is a Header which matches the ItemID of the parent
if group.headerID and group.headerID == root.itemID then
BuildContainsInfo(group, entries, indent, layer)
else
-- Count it, but don't actually add it to entries if it meets the limit
if #entries >= ContainsLimit then
ContainsExceeded = ContainsExceeded + 1;
else
-- Insert into the display.
-- app.PrintDebug("INCLUDE",app.Debugging,GetProgressTextForRow(group),group.hash,group.key,group.key and group[group.key])
local o = { group = group, right = GetProgressTextForRow(group, true) };
local indicator = ContainsTypesIndicators[group.filledType] or Indicator(group);
o.prefix = indicator and (Indents[indent]:sub(3) .. "|T" .. indicator .. ":0|t ") or Indents[indent]
entries[#entries + 1] = o
end
-- Only go down one more level.
if layer < MaxLayer then
BuildContainsInfo(group, entries, indent + 1, layer + 1);
end
end
-- else
-- app.PrintDebug("EXCLUDE",app.Debugging,GetProgressTextForRow(group),group.hash,group.key,group.key and group[group.key])
end
end
end
-- Fields on groups which can be utilized in tooltips to show additional Source location info for that group (by order of priority)
local TooltipSourceFields = {
"professionID",
"instanceID",
"mapID",
"maps",
"npcID",
"questID"
};
local SourceLocationSettingsKey = setmetatable({
creatureID = "SourceLocations:Creatures",
npcID = "SourceLocations:Creatures",
}, {
__index = function(t, key)
return "SourceLocations:Things";
end
});
local UnobtainableTexture = " |T"..L.UNOBTAINABLE_ITEM_TEXTURES[1]..":0|t"
local NotCurrentCharacterTexture = " |T"..L.UNOBTAINABLE_ITEM_TEXTURES[0]..":0|t"
local RETRIEVING_DATA = RETRIEVING_DATA
local function AddContainsData(group, tooltipInfo)
local key = group.key
local thingCheck = app.ThingKeys[key]
-- only show Contains on Things
if not thingCheck or (app.ActiveRowReference and thingCheck ~= true) then return end
local working = group.working
-- Sort by the heirarchy of the group if not the raw group of an ATT list
if not working and not app.ActiveRowReference then
app.Sort(group.g, app.SortDefaults.Hierarchy, true);
end
-- app.PrintDebug("SummarizeThings",app:SearchLink(group),group.g and #group.g,working)
local entries = {};
-- app.Debugging = "CONTAINS-"..group.hash;
ContainsLimit = app.Settings:GetTooltipSetting("ContainsCount") or 25;
ContainsExceeded = 0;
BuildContainsInfo(group, entries, 1, 1)
-- app.Debugging = nil;
-- app.PrintDebug(entries and #entries,"contains entries")
if #entries > 0 then
local left, right;
tinsert(tooltipInfo, { left = L.CONTAINS });
local item, entry;
local RecursiveParentField = app.GetRelativeValue
for i=1,#entries do
item = entries[i];
entry = item.group;
if not entry.objectiveID then
left = entry.text;
if not left or IsRetrieving(left) then
left = RETRIEVING_DATA
if not working then
local AsyncRefreshFunc = entry.AsyncRefreshFunc
if AsyncRefreshFunc then
AsyncRefreshFunc(entry)
else
-- app.PrintDebug("No Async Refresh Func for TT Type!",entry.__type)
app.ReshowGametooltip()
end
end
working = true
end
left = TryColorizeName(entry, left);
-- app.PrintDebug("Entry#",i,app:SearchLink(entry),app.GenerateSourcePathForTooltip(entry))
-- If this entry has a specific Class requirement and is not itself a 'Class' header, tack that on as well
if entry.c and entry.key ~= "classID" and #entry.c == 1 then
left = left .. " [" .. TryColorizeName(entry, app.ClassInfoByID[entry.c[1]].name) .. "]";
end
if entry.icon then item.prefix = item.prefix .. "|T" .. entry.icon .. ":0|t "; end
-- If this entry has specialization requirements, let's attempt to show the specialization icons.
right = item.right;
local specs = entry.specs;
if specs and #specs > 0 then
right = GetSpecsString(specs, false, false) .. right;
else
local c = entry.c;
if c and #c > 0 then
right = GetClassesString(c, false, false) .. right;
end
end
-- If this entry has customCollect requirements, list them for clarity
if entry.customCollect then
for i,c in ipairs(entry.customCollect) do
local reason = L.CUSTOM_COLLECTS_REASONS[c];
local icon_color_str = reason.icon.." |c"..reason.color..reason.text;
if i > 1 then
right = icon_color_str .. " / " .. right;
else
right = icon_color_str .. " " .. right;
end
end
end
-- If this entry is an Item, show additional Source information for that Item (since it needs to be acquired in a specific location most-likely)
if entry.itemID and key ~= "npcID" and key ~= "encounterID" then
-- Add the Zone name
local field, id;
for _,v in ipairs(TooltipSourceFields) do
id = RecursiveParentField(entry, v, true);
-- print("check",v,id)
if id then
field = v;
break;
end
end
if field then
local locationGroup, locationName;
-- convert maps
if field == "maps" then
-- if only a few maps, list them all
local count = #id;
if count == 1 then
locationName = app.GetMapName(id[1]);
else
-- instead of listing individual zone names, just list zone count for brevity
local names = {__count=0}
local name
for j=1,count,1 do
name = app.GetMapName(id[j]);
if name and not names[name] then
names.__count = names.__count + 1
end
end
locationName = "["..names.__count.." "..BRAWL_TOOLTIP_MAPS.."]"
-- old: list 3 zones/+++
-- local mapsConcat, names, name = {}, {}, nil;
-- for j=1,count,1 do
-- name = app.GetMapName(id[j]);
-- if name and not names[name] then
-- names[name] = true;
-- mapsConcat[#mapsConcat + 1] = name
-- end
-- end
-- -- 1 unique map name displayed
-- if #mapsConcat < 2 then
-- locationName = app.TableConcat(mapsConcat, nil, nil, "/");
-- else
-- mapsConcat[2] = "+"..(count - 1);
-- locationName = app.TableConcat(mapsConcat, nil, nil, "/", 1, 2);
-- end
end
else
locationGroup = SearchForObject(field, id, "field") or (id and field == "mapID" and C_Map_GetMapInfo(id));
locationName = locationGroup and TryColorizeName(locationGroup, locationGroup.name);
end
-- print("contains info",entry.itemID,field,id,locationGroup,locationName)
if locationName then
-- Add the immediate parent group Vendor name
local rawParent, sParent = rawget(entry, "parent"), entry.sourceParent;
-- the source entry is different from the raw parent and the search context, then show the source parent text for reference
if sParent and sParent.text and not GroupMatchesParams(rawParent, sParent.key, sParent[sParent.key]) and not GroupMatchesParams(sParent, key, id) then
local parentText = sParent.text;
if IsRetrieving(parentText) then
working = true;
end
right = locationName .. " > " .. parentText .. " " .. right;
else
right = locationName .. " " .. right;
end
-- else
-- print("No Location name for item",entry.itemID,id,field)
end
end
end
-- If this entry is an Achievement Criteria (whose raw parent is not the Achievement) then show the Achievement
if entry.criteriaID and entry.achievementID then
local rawParent = rawget(entry, "parent");
if not rawParent or rawParent.achievementID ~= entry.achievementID then
local critAch = SearchForObject("achievementID", entry.achievementID, "key");
left = left .. " > " .. (critAch and critAch.text or "???");
end
end
tinsert(tooltipInfo, { left = item.prefix .. left, right = right });
end
end
if ContainsExceeded > 0 then
tinsert(tooltipInfo, { left = (L.AND_MORE):format(ContainsExceeded) });
end
end
return working
end
app.AddEventHandler("OnLoad", function()
app.Settings.CreateInformationType("SummarizeThings", {
text = "SummarizeThings",
priority = 2.9, HideCheckBox = true,
Process = function(t, reference, tooltipInfo)
if reference.g then
if AddContainsData(reference, tooltipInfo) then
reference.working = true
end
end
end
})
end)
local SourceSearcher = app.SourceSearcher
local function AddSourceLinesForTooltip(tooltipInfo, paramA, paramB)
-- Create a list of sources
-- app.PrintDebug("SourceLocations",paramA,paramB,SourceLocationSettingsKey[paramA])
if not app.ThingKeys[paramA] then return end
local settings = app.Settings
if not settings:GetTooltipSetting("SourceLocations") or not settings:GetTooltipSetting(SourceLocationSettingsKey[paramA]) then return end
local text, parent, right
local character, unavailable, unobtainable = {}, {}, {}
local showUnsorted = settings:GetTooltipSetting("SourceLocations:Unsorted");
local showCompleted = settings:GetTooltipSetting("SourceLocations:Completed");
local FilterSettings, FilterInGame, FilterCharacter, FirstParent
= app.RecursiveGroupRequirementsFilter, app.Modules.Filter.Filters.InGame, app.RecursiveCharacterRequirementsFilter, app.GetRelativeGroup
local abbrevs = L.ABBREVIATIONS;
local sourcesToShow
-- paramB is the modItemID for itemID searches, so we may have to fallback to the base itemID if nothing sourced for the modItemID
-- TODO: Rings from raid showing all difficulties, need fallback matching for items... modItemID, modID, itemID
-- using a second return, directSources, to indicate the SourceSearcher has returned the Sources rather than the Things
local allReferences, directSources = SourceSearcher[paramA](paramA,paramB)
-- app.PrintDebug(directSources and "Source count" or "Search count",#allReferences,paramA,paramB,GetItemIDAndModID(paramB))
for _,j in ipairs(allReferences or app.EmptyTable) do
parent = directSources and j or j.parent
-- app.PrintDebug("source:",app:SearchLink(j),parent and parent.parent,showCompleted or not app.IsComplete(j))
if parent and parent.parent
and (showCompleted or not app.IsComplete(j))
then
text = app.GenerateSourcePathForTooltip(parent);
-- app.PrintDebug("SourceLocation",text,FilterInGame(j),FilterSettings(parent),FilterCharacter(parent))
if showUnsorted or (not text:match(L.UNSORTED) and not text:match(L.HIDDEN_QUEST_TRIGGERS)) then
-- doesn't meet current unobtainable filters from the Thing itself and its parent chain
if not FilterInGame(j) or not FilterInGame(parent) then
unobtainable[#unobtainable + 1] = text..UnobtainableTexture
else
-- something user would currently see in a list or not
sourcesToShow = FilterSettings(parent) and character or unavailable
-- from obtainable, different character source
if not FilterCharacter(parent) then
sourcesToShow[#sourcesToShow + 1] = text..NotCurrentCharacterTexture
else
-- check if this needs a status icon even though it's being shown
right = GetUnobtainableTexture(FirstParent(parent, "e", true) or FirstParent(parent, "u", true) or parent)
or (parent.rwp and app.asset("status-prerequisites"))
if right then
sourcesToShow[#sourcesToShow + 1] = text.." |T" .. right .. ":0|t"
else
sourcesToShow[#sourcesToShow + 1] = text
end
end
end
end
end
end
-- app.PrintDebug("Sources count",#character,#unobtainable)
-- if in Debug, add any unobtainable & unavailable sources
if app.MODE_DEBUG then
-- app.PrintDebug("+unavailable",#unavailable,"+unobtainable",#unobtainable)
app.ArrayAppend(character, unavailable, unobtainable)
elseif #character == 0 and not (paramA == "npcID" or paramA == "creatureID" or paramA == "encounterID") then
-- no sources available to the character, add any unavailable/unobtainable sources
if #unavailable > 0 then
-- app.PrintDebug("+unavailable",#unavailable)
app.ArrayAppend(character, unavailable)
elseif #unobtainable > 0 then
-- app.PrintDebug("+unobtainable",#unobtainable)
app.ArrayAppend(character, unobtainable)
end
end
if #character > 0 then
local listing = {};
local maximum = settings:GetTooltipSetting("Locations");
local count = 0;
app.Sort(character, app.SortDefaults.Strings);
for _,text in ipairs(character) do
-- since the strings are sorted, we only need to add ones that are not equal to the previously-added one
-- instead of checking all existing strings
if listing[#listing] ~= text then
count = count + 1;
if count <= maximum then
listing[#listing + 1] = text
-- app.PrintDebug("add source",text)
end
-- else app.PrintDebug("exclude source by last match",text)
end
end
if count > maximum then
listing[#listing + 1] = (L.AND_OTHER_SOURCES):format(count - maximum)
end
if #listing > 0 then
local wrap = settings:GetTooltipSetting("SourceLocations:Wrapping");
local working
for _,text in ipairs(listing) do
for source,replacement in pairs(abbrevs) do
text = text:gsub(source, replacement);
end
if not working and IsRetrieving(text) then working = true; end
local left, right = DESCRIPTION_SEPARATOR:split(text);
tooltipInfo[#tooltipInfo + 1] = { left = left, right = right, wrap = wrap }
end
tooltipInfo.hasSourceLocations = true;
return working
end
end
end
app.AddEventHandler("OnLoad", function()
local SourceShowKeys = app.CloneDictionary(app.ThingKeys, {
-- Specific keys which we don't want to list Sources but are considered Things
npcID = false,
creatureID = false,
encounterID = false,
explorationID = false,
})
app.Settings.CreateInformationType("SourceLocations", {
priority = 2.7,
text = "Source Locations",
HideCheckBox = true,
Process = function(t, reference, tooltipInfo)
local key = reference.key
local id = key == "itemID" and reference.modItemID or reference[key]
if key and id and SourceShowKeys[key] then
if tooltipInfo.hasSourceLocations then return end
if AddSourceLinesForTooltip(tooltipInfo, key, id) then
reference.working = true
end
end
end
})
end)
local unpack = unpack
local function GetSearchResults(method, paramA, paramB, options)
-- app.PrintDebug("GetSearchResults",method,paramA,paramB)
if not method then
print("GetSearchResults: Invalid method: nil");
return nil, true;
end
if not paramA then
print("GetSearchResults: Invalid paramA: nil");
return nil, true;
end
-- If we are searching for only one parameter, it is a raw link.
local rawlink;
if paramB then paramB = tonumber(paramB);
else rawlink = paramA; end
local RecursiveCharacterRequirementsFilter, RecursiveGroupRequirementsFilter
= app.RecursiveCharacterRequirementsFilter, app.RecursiveGroupRequirementsFilter
-- Call to the method to search the database.
local group, a, b
if options and options.AppendSearchParams then
group, a, b = method(paramA, paramB, unpack(options.AppendSearchParams))
else
group, a, b = method(paramA, paramB)
end
-- app.PrintDebug("GetSearchResults:method",group and #group,a,b,paramA,paramB)
if group then
if a then paramA = a; end
if b then paramB = b; end
if paramA == "modItemID" then paramA = "itemID" end
-- Move all post processing here?
if #group > 0 then
-- For Creatures, Objects and Encounters that are inside of an instance, we only want the data relevant for the instance + difficulty.
if paramA == "npcID" or paramA == "creatureID" or paramA == "encounterID" or paramA == "objectID" then
local subgroup = {};
for _,j in ipairs(group) do
if not j.ShouldExcludeFromTooltip then
tinsert(subgroup, j);
end
end
group = subgroup;
elseif paramA == "azeriteessenceID" then
local regroup = {};
local rank = options and options.Rank
if app.MODE_ACCOUNT then
for i,j in ipairs(group) do
if j.rank == rank and app.RecursiveUnobtainableFilter(j) then
if j.mapID or j.parent == nil or j.parent.parent == nil then
tinsert(regroup, setmetatable({["g"] = {}}, { __index = j }));
else
tinsert(regroup, j);
end
end
end
else
for i,j in ipairs(group) do
if j.rank == rank and RecursiveCharacterRequirementsFilter(j) and app.RecursiveUnobtainableFilter(j) and RecursiveGroupRequirementsFilter(j) then
if j.mapID or j.parent == nil or j.parent.parent == nil then
tinsert(regroup, setmetatable({["g"] = {}}, { __index = j }));
else
tinsert(regroup, j);
end
end
end
end
group = regroup;
elseif paramA == "titleID" or paramA == "followerID" then
-- Don't do anything
local regroup = {};
if app.MODE_ACCOUNT then
for i,j in ipairs(group) do
if app.RecursiveUnobtainableFilter(j) then
tinsert(regroup, setmetatable({["g"] = {}}, { __index = j }));
end
end
else
for i,j in ipairs(group) do
if RecursiveCharacterRequirementsFilter(j) and app.RecursiveUnobtainableFilter(j) and RecursiveGroupRequirementsFilter(j) then
tinsert(regroup, setmetatable({["g"] = {}}, { __index = j }));
end
end
end
group = regroup;
end
end
else
group = {};
end
-- Determine if this is a cache for an item
if rawlink and not paramB then
local itemString = CleanLink(rawlink)
if itemString:match("item") then
-- app.PrintDebug("Rawlink SourceID",sourceID,rawlink)
local _, itemID, enchantId, gemId1, gemId2, gemId3, gemId4, suffixId, uniqueId, linkLevel, specializationID, upgradeId, linkModID, numBonusIds, bonusID1 = (":"):split(itemString);
if itemID then
itemID = tonumber(itemID);
local modID = tonumber(linkModID) or 0;
if modID == 0 then modID = nil; end
local bonusID = (tonumber(numBonusIds) or 0) > 0 and tonumber(bonusID1) or 3524;
if bonusID == 3524 then bonusID = nil; end
local sourceID = app.GetSourceID(rawlink);
if sourceID then
paramA = "sourceID"
paramB = sourceID
-- app.PrintDebug("use sourceID params",paramA,paramB)
else
paramA = "itemID";
paramB = GetGroupItemIDWithModID(nil, itemID, modID, bonusID) or itemID;
-- app.PrintDebug("use itemID params",paramA,paramB)
end
end
else
local kind, id = (":"):split(rawlink);
kind = kind:lower();
if id then id = tonumber(id); end
if kind == "itemid" then
paramA = "itemID";
paramB = id;
elseif kind == "questid" then
paramA = "questID";
paramB = id;
elseif kind == "creatureid" or kind == "npcid" then
paramA = "npcID";
paramB = id;
elseif kind == "achievementid" then
paramA = "achievementID";
paramB = id;
end
end
end
-- Create clones of the search results
if not group.g then
-- Clone all the non-ignored groups so that things don't get modified in the Source
-- app.PrintDebug("Cloning Roots for",paramA,paramB,"#group",group and #group);
local cloned = {};
for _,o in ipairs(group) do
-- app.PrintDebug("Clone:",app:SearchLink(o),GetRelativeValue(o, "sourceIgnored"),app.GetRelativeRawWithField(o, "sourceIgnored"),app.GenerateSourcePathForTooltip(o))
if not GetRelativeValue(o, "sourceIgnored") then
cloned[#cloned + 1] = CreateObject(o)
end
end
-- replace the Source references with the cloned references
group = cloned;
local clearSourceParent = #group > 1;
-- Find or Create the root group for the search results, and capture the results which need to be nested instead
local root, filtered
local nested = {};
-- app.PrintDebug("Find Root for",paramA,paramB,"#group",group and #group);
-- check for Item groups in a special way to account for extra ID's
if paramA == "itemID" then
local refinedMatches = app.GroupBestMatchingItems(group, paramB);
if refinedMatches then
-- move from depth 3 to depth 1 to find the set of items which best matches for the root
for depth=3,1,-1 do
if refinedMatches[depth] then
-- app.PrintDebug("refined",depth,#refinedMatches[depth])
if not root then
for _,o in ipairs(refinedMatches[depth]) do
-- object meets filter criteria and is exactly what is being searched
if RecursiveCharacterRequirementsFilter(o) then
-- app.PrintDebug("filtered root");
if root then
if filtered then
-- app.PrintDebug("merge root",app:SearchLink(o));
-- app.PrintTable(o)
MergeProperties(root, o, filtered);
-- other root content will be nested after
MergeObjects(nested, o.g);
else
local otherRoot = root;
-- app.PrintDebug("replace root",app:SearchLink(otherRoot));
root = o;
MergeProperties(root, otherRoot);
-- previous root content will be nested after
MergeObjects(nested, otherRoot.g);
end
else
root = o;
-- app.PrintDebug("first root",app:SearchLink(o));
end
filtered = true
else
-- app.PrintDebug("unfiltered root",app:SearchLink(o),o.modItemID,paramB);
if root then MergeProperties(root, o, true);
else root = o; end
end
end
else
for _,o in ipairs(refinedMatches[depth]) do
-- Not accurate matched enough to be the root, so it will be nested
-- app.PrintDebug("nested",app:SearchLink(o))
nested[#nested + 1] = o
end
end
end
end
end
else
for _,o in ipairs(group) do
-- If the obj "is" the root obj
-- app.PrintDebug(o.key,o[o.key],o.modItemID,"=parent>",o.parent and o.parent.key,o.parent and o.parent.key and o.parent[o.parent.key],o.parent and o.parent.text);
if GroupMatchesParams(o, paramA, paramB) then
-- object meets filter criteria and is exactly what is being searched
if RecursiveCharacterRequirementsFilter(o) then
-- app.PrintDebug("filtered root");
if root then
if filtered then
-- app.PrintDebug("merge root",o.key,o[o.key]);
-- app.PrintTable(o)
MergeProperties(root, o, filtered);
-- other root content will be nested after
MergeObjects(nested, o.g);
else
local otherRoot = root;
-- app.PrintDebug("replace root",otherRoot.key,otherRoot[otherRoot.key]);
-- app.PrintTable(o)
root = o;
MergeProperties(root, otherRoot);
-- previous root content will be nested after
MergeObjects(nested, otherRoot.g);
end
else
-- app.PrintDebug("first root",o.key,o[o.key]);
-- app.PrintTable(o)
root = o;
end
filtered = true
else
-- app.PrintDebug("unfiltered root",o.key,o[o.key],o.modItemID,paramB);
if root then MergeProperties(root, o, true);
else root = o; end
end
else
-- Not the root, so it will be nested
-- app.PrintDebug("nested")
nested[#nested + 1] = o
end
end
end
if not root then
-- app.PrintDebug("Create New Root",paramA,paramB)
if paramA == "criteriaID" then
local critID, achID = (":"):split(paramB)
root = CreateObject({ [paramA] = tonumber(critID), achievementID = tonumber(achID) })
else
root = CreateObject({ [paramA] = paramB })
end
root.missing = true
end
-- If rawLink exists, import it into the root
if rawlink then app.ImportRawLink(root, rawlink); end
-- Ensure the param values are consistent with the new root object values (basically only affects creatureID)
paramA, paramB = root.key, root[root.key];
-- Special Case for itemID, need to use the modItemID for accuracy in item matching
if root.itemID then
if paramA ~= "sourceID" then
paramA = "itemID"
paramB = root.modItemID or paramB
end
-- if our item root has a bonusID, then we will rely on upgrade module to provide any upgrade
-- raw groups with 'up' will never be sourced with a bonusID
local bonusID = root.bonusID
if bonusID ~= 3524 and bonusID or 0 > 0 then
root.up = nil
end
end
-- app.PrintDebug("Root",root.key,root[root.key],root.modItemID,root.up,root._up);
-- app.PrintTable(root)
-- app.PrintDebug("Root Collect",root.collectible,root.collected,root.collectibleAsCost,root.hasUpgrade);
-- app.PrintDebug("params",paramA,paramB);
-- app.PrintDebug(#nested,"Nested total");
if #nested > 0 then
-- Nest the objects by matching filter priority if it's not a currency
if paramA ~= "currencyID" then
PriorityNestObjects(root, nested, nil, RecursiveCharacterRequirementsFilter, RecursiveGroupRequirementsFilter)
else
-- do roughly the same logic for currency, but will not add the skipped objects afterwards
local added = {};
for i,o in ipairs(nested) do
-- If the obj meets the recursive group filter
if RecursiveCharacterRequirementsFilter(o) then
-- Merge the obj into the merged results
-- app.PrintDebug("Merge object",o.key,o[o.key])
added[#added + 1] = o
end
end
-- Nest the added objects
NestObjects(root, added)
end
end
-- if not root.key then
-- app.PrintDebug("UNKNOWN ROOT GROUP",paramA,paramB)
-- app.PrintTable(root)
-- end
-- Single group which matches the root, then collapse it
-- This could only happen if a Thing is literally listed underneath itself...
if root.g and #root.g == 1 then
local o = root.g[1];
-- if not o.key then
-- app.PrintDebug("UNKNOWN OBJECT GROUP",paramA,paramB)
-- app.PrintTable(o)
-- end
if o.key then
local okey, rootkey = o.key, root.key
-- print("Check Single",root.key,root.key and root[root.key],o.key and root[o.key],o.key,o.key and o[o.key],root.key and o[root.key])
-- Heroic Tusks of Mannoroth triggers this logic
if (root[okey] == o[okey]) or (root[rootkey] == o[rootkey]) then
-- print("Single group")
root.g = nil;
MergeProperties(root, o, true);
end
end
end
-- Replace as the group
group = root;
-- Ensure some specific relative values are captured in the base group
-- can make this a loop if there ends up being more needed...
group.difficultyID = GetRelativeValue(group, "difficultyID");
-- Ensure no weird parent references attached to the base search result if there were multiple search results
group.parent = nil;
if clearSourceParent then
group.sourceParent = nil;
end
-- app.PrintDebug(group.g and #group.g,"Merge total");
-- app.PrintDebug("Final Group",group.key,group[group.key],group.collectible,group.collected,app:SearchLink(group.sourceParent));
-- app.PrintDebug("Group Type",group.__type)
-- Special cases
-- This was added in https://github.com/ATTWoWAddon/AllTheThings/commit/97dfc7dd9d228f149635e7fcbccd1c22549316a4
-- in Dec 2020. I'm trying to figure out why other than forcibly reducing some Achievement tooltip size...
-- Going to remove this and see if any complaints since showing more accurate Achievement information than Blizz
-- default tooltip is probably a nicety - Runaway 2025-10-18
-- Don't show nested criteria of achievements (unless loading popout/row content)
-- if group.g and group.key == "achievementID" and app.GetSkipLevel() < 2 then
-- local noCrits = {};
-- -- print("achieve group",#group.g)
-- for i=1,#group.g do
-- if group.g[i].key ~= "criteriaID" then
-- tinsert(noCrits, group.g[i]);
-- end
-- end
-- group.g = noCrits;
-- -- print("achieve nocrits",#group.g)
-- end
-- Fill the search result but not if the search itself was skipped (Mark of Honor) or indicated to skip
if not options or not options.SkipFill then
-- Fill up the group
app.FillGroups(group)
end
-- Only need to build groups from the top level
AssignChildren(group);
-- delete sub-groups if there are none
elseif #group.g == 0 then
group.g = nil;
end
app.TopLevelUpdateGroup(group, true);
group.isBaseSearchResult = true;
return group, group.working
end
app.GetCachedSearchResults = function(method, paramA, paramB, options)
-- app.print("GCSR",paramA,paramB,options and options.IgnoreCache)
if options then
if options.IgnoreCache then
return GetSearchResults(method, paramA, paramB, options)
end
-- add a 10sec cache window to lookups
-- probably long enough that any repeated use is smoother, but short enough that user actions would typically result in fresh lookups
if options.ShortCache then
local time = math_floor(GetTime() / 10)
return app.GetCachedData((paramB and paramA..":"..paramB or paramA)..time, GetSearchResults, method, paramA, paramB, options);
end
end
return app.GetCachedData(paramB and paramA..":"..paramB or paramA, GetSearchResults, method, paramA, paramB, options);
end
end -- Search results Lib
(function()
-- Keys for groups which are in-game 'Things'
app.ThingKeys = {
-- filterID = true,
flightpathID = true,
-- professionID = true,
-- categoryID = true,
-- mapID = true,
conduitID = true,
currencyID = true,
itemID = true,
toyID = true,
sourceID = true,
speciesID = true,
recipeID = true,
runeforgepowerID = true,
spellID = true,
missionID = true,
mountID = true,
mountmodID = true,
illusionID = true,
questID = true,
objectID = true,
artifactID = true,
azeriteessenceID = true,
followerID = true,
factionID = true,
titleID = true,
campsiteID = true,
decorID = true,
garrisonbuildingID = true,
achievementID = true, -- special handling
criteriaID = true, -- special handling
-- 1 - Specific keys which we don't want to list Contains data on row reference tooltips but are considered Things
npcID = 1,
creatureID = 1,
encounterID = 1,
explorationID = 1,
};
local SpecificSources = {
headerID = {
[app.HeaderConstants.COMMON_BOSS_DROPS] = true,
[app.HeaderConstants.COMMON_VENDOR_ITEMS] = true,
[app.HeaderConstants.DROPS] = true,
},
}
local KeepSourced = {
criteriaID = true
}
local SourceSearcher = app.SourceSearcher
local function GetThingSources(field, value)
if field == "achievementID" then
return SearchForField(field, value)
end
if field == "itemID" then
-- allow extra return val (indicates directSources)
return SourceSearcher.itemID(field, value)
end
-- ignore extra return vals
local results = app.SearchForLink(field..":"..value)
return results
end
-- TODO: probably have parser generate CraftedItemDB for simpler use
local function GetCraftingOutputRecipes(thing)
local recipeIDs
local itemID = thing.itemID
for reagent,recipes in pairs(app.ReagentsDB) do
for recipeID,info in pairs(recipes) do
if info[1] == itemID then
if recipeIDs then recipeIDs[#recipeIDs + 1] = recipeID
else recipeIDs = { recipeID } end
end
end
end
return recipeIDs
end
-- Builds a 'Source' group from the parent of the group (or other listings of this group) and lists it under the group itself for
local function BuildSourceParent(group)
-- only show sources for Things or specific of other types
if not group or not group.key then return; end
local groupKey, thingKeys = group.key, app.ThingKeys;
local thingCheck = thingKeys[groupKey];
local specificSource = SpecificSources[groupKey]
if specificSource then
specificSource = specificSource[group[groupKey]];
end
-- group with some Source-able data can be treated as specific Source
if not specificSource and (
group.npcID or group.crs or group.providers
) then
specificSource = true;
end
if not thingCheck and not specificSource then return; end
-- pull all listings of this 'Thing'
local keyValue = group[groupKey];
local isDirectSources
local things = specificSource and { group }
if not things then
things, isDirectSources = GetThingSources(groupKey, keyValue)
end
-- app.PrintDebug("BuildSourceParent",group.hash,thingCheck,specificSource,keyValue,#things,isDirectSources)
-- if app.Debugging then
-- local sourceGroup = app.CreateRawText("DEBUG THINGS", {
-- ["OnUpdate"] = app.AlwaysShowUpdate,
-- ["skipFill"] = true,
-- ["g"] = {},
-- })
-- NestObjects(sourceGroup, things, true)
-- NestObject(group, sourceGroup, nil, 1)
-- end
if things then
local groupHash = group.hash;
local parents = {};
local isAchievement = groupKey == "achievementID";
local parentKey, parent;
-- collect all possible parent groups for all instances of this Thing
for _,thing in ipairs(things) do
if isDirectSources then
parents[#parents + 1] = CreateObject(thing)
elseif isAchievement or GroupMatchesParams(thing, groupKey, keyValue) then
---@class ATTTempParentObject
---@field key string
---@field hash string
---@field npcID number
---@field creatureID number
---@field _keepSource boolean
---@field parent ATTTempParentObject
parent = thing.parent;
while parent do
-- app.PrintDebug("parent",parent.text,parent.key)
parentKey = parent.key;
if parentKey and parent[parentKey] and parent.hash ~= groupHash then
-- only show certain types of parents as sources.. typically 'Game World Things'
-- or if the parent is directly tied to an NPC
if thingKeys[parentKey] or parent.npcID then
-- add the parent for display later
parent = CreateObject(parent, true)
parents[#parents + 1] = parent
-- achievement criteria can nest inside their Source for clarity
if isAchievement and KeepSourced[thing.key] then
NestObject(parent, thing, true)
end
break
-- or a map
elseif parent.mapID then
parent = app.CreateVisualHeaderWithGroups(CreateObject(parent, true))
parents[#parents + 1] = parent
-- achievement criteria can nest inside their Source for clarity
if isAchievement and KeepSourced[thing.key] then
NestObject(parent, thing, true)
end
break
-- or a header with tagged NCPs
elseif parent.headerID and parent.crs then
local npcs = parent.crs
for i=1,#npcs do
parents[#parents + 1] = CreateObject(SearchForObject("npcID", npcs[i], "field") or {["npcID"] = npcs[i]}, true)
end
break
end
end
-- move to the next parent if the current parent is not a valid 'Thing'
parent = parent.parent;
end
-- Things tagged with an npcID should show that NPC as a Source
if thing.key ~= "npcID" and thing.npcID and thing.hash ~= group.hash then
local parentNPC = CreateObject(SearchForObject("npcID", thing.npcID, "field") or {["npcID"] = thing.npcID}, true)
parents[#parents + 1] = parentNPC
-- achievement criteria can nest inside their Source for clarity
if isAchievement and KeepSourced[thing.key] then
NestObject(parentNPC, thing, true)
end
end
-- Things tagged with many npcIDs should show all those NPCs as a Source
if thing.crs then
-- app.PrintDebug("thing.crs",#thing.crs)
local parentNPC;
for _,npcID in ipairs(thing.crs) do
parentNPC = CreateObject(SearchForObject("npcID", npcID, "field") or {["npcID"] = npcID}, true)
parents[#parents + 1] = parentNPC
-- achievement criteria can nest inside their Source for clarity
if isAchievement and KeepSourced[thing.key] then
NestObject(parentNPC, thing, true)
end
end
end
-- Things tagged with providers should show the providers as a Source
if thing.providers then
local type, id;
for _,p in ipairs(thing.providers) do
type, id = p[1], p[2];
-- app.PrintDebug("Root Provider",type,id);
---@type any
local pRef = (type == "i" and SearchForObject("itemID", id, "field"))
or (type == "o" and SearchForObject("objectID", id, "field"))
or (type == "n" and SearchForObject("npcID", id, "field"))
or (type == "s" and SearchForObject("spellID", id, "field"));
if pRef then
pRef = CreateObject(pRef, true);
parents[#parents + 1] = pRef
else
pRef = (type == "i" and app.CreateItem(id))
or (type == "o" and app.CreateObject(id))
or (type == "n" and app.CreateNPC(id))
or (type == "s" and app.CreateSpell(id));
parents[#parents + 1] = pRef
end
-- achievement criteria can nest inside their Source for clarity
if isAchievement and thing.key == "criteriaID" then
NestObject(pRef, thing, true)
end
end
end
-- Things tagged with qgs should show the quest givers as a Source
if thing.qgs then
for _,id in ipairs(thing.qgs) do
-- app.PrintDebug("Root Provider",type,id);
local pRef = SearchForObject("npcID", id, "field");
if pRef then
pRef = CreateObject(pRef, true);
parents[#parents + 1] = pRef
else
pRef = app.CreateNPC(id);
parents[#parents + 1] = pRef
end
end
end
-- Things which are a Item output of one or more Crafting Recipes should show those Recipes as a Source
if thing.itemID then
local recipes = GetCraftingOutputRecipes(thing)
if recipes then
for _,recipeID in ipairs(recipes) do
local pRef = SearchForObject("recipeID", recipeID, "field");
if pRef then
pRef = CreateObject(pRef, true);
parents[#parents + 1] = pRef
else
pRef = app.CreateRecipe(recipeID);
parents[#parents + 1] = pRef
end
pRef.OnUpdate = app.AlwaysShowUpdate
end
end
end
-- Things tagged with 'sourceQuests' should show the quests as a Source (if the Thing itself is not a raw Quest)
-- if thing.sourceQuests and groupKey ~= "questID" then
-- local questRef;
-- for _,sq in ipairs(thing.sourceQuests) do
-- questRef = SearchForObject("questID", sq) or {["questID"] = sq};
-- tinsert(parents, questRef);
-- end
-- end
end
end
-- Raw Criteria include their containing Achievement as the Source
-- re-popping this Achievement will do normal Sources for all the Criteria and be useful
if groupKey == "criteriaID" then
local achID = group.achievementID;
parent = CreateObject(SearchForObject("achievementID", achID, "key") or { achievementID = achID }, true)
-- app.PrintDebug("add achievement for empty criteria",achID)
parents[#parents + 1] = parent
end
if #parents == 0 then return end
-- if there are valid parent groups for sources, merge them into a 'Source(s)' group
-- app.PrintDebug("Found parents",#parents)
local sourceGroup = app.CreateRawText(L.SOURCES, {
description = L.SOURCES_DESC,
icon = 134441,
OnUpdate = app.AlwaysShowUpdate,
sourceIgnored = true,
skipFull = true,
SortPriority = -3.0,
g = {},
IgnorePopout=true,
})
for _,parent in ipairs(parents) do
-- if there's nothing nested under the parent, then force it to be visible
-- otherwise the visibility can be driven by the nested thing
parent.OnSetVisibility = not parent.g and app.AlwaysShowUpdate or nil -- TODO: filter actual unobtainable sources...
end
PriorityNestObjects(sourceGroup, parents, nil, app.RecursiveCharacterRequirementsFilter, app.RecursiveGroupRequirementsFilter);
NestObject(group, sourceGroup, nil, 1);
end
end
app.AddEventHandler("OnNewPopoutGroup", BuildSourceParent)
end)();
-- Synchronization Functions
(function()
local C_CreatureInfo_GetRaceInfo = C_CreatureInfo.GetRaceInfo;
local SendAddonMessage = app.WOWAPI.SendAddonMessage
local outgoing,incoming,queue,active = {},{},{},nil;
local whiteListedFields = { --[["Achievements",]] "Artifacts", "AzeriteEssenceRanks", "BattlePets", "Exploration", "Factions", "FlightPaths", "Followers", "GarrisonBuildings", "Quests", "Spells", "Titles" };
app.CharacterSyncTables = whiteListedFields;
local function splittoarray(sep, inputstr)
local t = {};
for str in inputstr:gmatch("([^" .. (sep or "%s") .. "]+)") do
tinsert(t, str);
end
return t;
end
local function processQueue()
if #queue > 0 and not active then
local data = queue[1];
tremove(queue, 1);
active = data[1];
app.print("Updating " .. data[2] .. " from " .. data[3] .. "...");
SendAddonMessage("ATT", "!\tsyncsum\t" .. data[1], "WHISPER", data[3]);
end
end
function app:AcknowledgeIncomingChunks(sender, uid, total)
local incomingFromSender = incoming[sender];
if not incomingFromSender then
incomingFromSender = {};
incoming[sender] = incomingFromSender;
end
incomingFromSender[uid] = { ["chunks"] = {}, ["total"] = total };
SendAddonMessage("ATT", "chksack\t" .. uid, "WHISPER", sender);
end
local function ProcessIncomingChunk(sender, uid, index, chunk)
if not (chunk and index and uid and sender) then return false; end
local incomingFromSender = incoming[sender];
if not incomingFromSender then return false; end
local incomingForUID = incomingFromSender[uid];
if not incomingForUID then return false; end
incomingForUID.chunks[index] = chunk;
if index < incomingForUID.total then
if index % 25 == 0 then app.print("Syncing " .. index .. " / " .. incomingForUID.total); end
return true;
end
incomingFromSender[uid] = nil;
local msg = "";
for i=1,incomingForUID.total,1 do
msg = msg .. incomingForUID.chunks[i];
end
-- app:ShowPopupDialogWithMultiLineEditBox(msg);
local characters = splittoarray("\t", msg);
for _,characterString in ipairs(characters) do
local data = splittoarray(":", characterString);
local guid = data[1];
local character = ATTCharacterData[guid];
if not character then
character = {};
character.guid = guid;
ATTCharacterData[guid] = character;
end
character.name = data[2];
character.lvl = tonumber(data[3]);
character.text = data[4];
if data[5] ~= "" and data[5] ~= " " then character.realm = data[5]; end
if data[6] ~= "" and data[6] ~= " " then character.factionID = tonumber(data[6]); end
if data[7] ~= "" and data[7] ~= " " then character.classID = tonumber(data[7]); end
if data[8] ~= "" and data[8] ~= " " then character.raceID = tonumber(data[8]); end
character.lastPlayed = tonumber(data[9]);
character.Deaths = tonumber(data[10]);
if character.classID then character.class = app.ClassInfoByID[character.classID].file; end
if character.raceID then character.race = C_CreatureInfo_GetRaceInfo(character.raceID).clientFileString; end
for i=11,#data,1 do
local piece = splittoarray("/", data[i]);
local key = piece[1];
local field = {};
character[key] = field;
for j=2,#piece,1 do
local index = tonumber(piece[j]);
if index then field[index] = 1; end
end
end
app.print("Update complete for " .. character.text .. ".");
end
app:RecalculateAccountWideData();
app.Settings:Refresh();
active = nil;
processQueue();
return false;
end
function app:AcknowledgeIncomingChunk(sender, uid, index, chunk)
if chunk and ProcessIncomingChunk(sender, uid, index, chunk) then
SendAddonMessage("ATT", "chkack\t" .. uid .. "\t" .. index .. "\t1", "WHISPER", sender);
else
SendAddonMessage("ATT", "chkack\t" .. uid .. "\t" .. index .. "\t0", "WHISPER", sender);
end
end
function app:SendChunk(sender, uid, index, success)
local outgoingForSender = outgoing[sender];
if outgoingForSender then
local chunksForUID = outgoingForSender.uids[uid];
if chunksForUID and success == 1 then
local chunk = chunksForUID[index];
if chunk then
SendAddonMessage("ATT", "chk\t" .. uid .. "\t" .. index .. "\t" .. chunk, "WHISPER", sender);
end
else
outgoingForSender.uids[uid] = nil;
end
end
end
function app:IsAccountLinked(sender)
return AllTheThingsAD.LinkedAccounts[sender] or AllTheThingsAD.LinkedAccounts[("-"):split(sender)];
end
local function DefaultSyncCharacterData(allCharacters, key)
local characterData
local data = ATTAccountWideData[key];
wipe(data);
for guid,character in pairs(allCharacters) do
characterData = character[key];
if characterData then
for index,_ in pairs(characterData) do
data[index] = 1;
end
end
end
end
-- Used for data which is defaulted as Account-learned, but has Character-learned exceptions
local function PartialSyncCharacterData(allCharacters, key)
local characterData
local data = ATTAccountWideData[key];
-- wipe account data saved based on character data
for id,completion in pairs(data) do
if completion == 2 then
data[id] = nil
end
end
for guid,character in pairs(allCharacters) do
characterData = character[key];
if characterData then
for id,_ in pairs(characterData) do
-- character-based completion in account data saved as 2 for these types
data[id] = 2
end
end
end
end
local function RankSyncCharacterData(allCharacters, key)
local characterData
local data = ATTAccountWideData[key];
wipe(data);
local oldRank;
for guid,character in pairs(allCharacters) do
characterData = character[key];
if characterData then
for index,rank in pairs(characterData) do
oldRank = data[index];
if not oldRank or oldRank < rank then
data[index] = rank;
end
end
end
end
end
local function SyncCharacterQuestData(allCharacters, key)
local characterData
local data = ATTAccountWideData[key];
-- don't completely wipe quest data, some questID are marked as 'complete' due to other restrictions on the account
-- so we want to maintain those even though no character actually has it completed
-- TODO: perhaps in the future we can instead treat these quests as 'uncollectible' for the account rather than 'complete'
-- TODO: once these quests are no longer assigned as completion == 2 we can then use the PartialSyncCharacterData for Quests
-- and make sure AccountWide quests are instead saved directly into ATTAccountWideData when completed
-- and cleaned from individual Character caches here during sync
for questID,completion in pairs(data) do
if completion ~= 2 then
data[questID] = nil
-- else app.PrintDebug("not-reset",questID,completion)
end
end
for guid,character in pairs(allCharacters) do
characterData = character[key];
if characterData then
for index,_ in pairs(characterData) do
data[index] = 1;
end
end
end
end
-- TODO: individual Classes should be able to add the proper functionality here to determine the account-wide
-- collection states of a 'Thing', if the refresh can't account for it
-- i.e. Mounts... 99% account-wide by default, like 5 per character. don't want to save 900+ id's for
-- each character just to sync into account data properly
local SyncFunctions = setmetatable({
AzeriteEssenceRanks = RankSyncCharacterData,
Quests = SyncCharacterQuestData,
Mounts = PartialSyncCharacterData,
BattlePets = PartialSyncCharacterData,
}, { __index = function(t, key)
if contains(whiteListedFields, key) then
return DefaultSyncCharacterData
end
end })
function app:RecalculateAccountWideData()
local allCharacters = ATTCharacterData;
local syncFunc;
for key,data in pairs(ATTAccountWideData) do
syncFunc = SyncFunctions[key];
if syncFunc then
-- app.PrintDebug("Sync:",key)
syncFunc(allCharacters, key);
end
end
local deaths = 0;
for guid,character in pairs(allCharacters) do
if character.Deaths then
deaths = deaths + character.Deaths;
end
end
ATTAccountWideData.Deaths = deaths;
end
app.AddEventHandler("OnRecalculateDone", app.RecalculateAccountWideData)
function app:ReceiveSyncRequest(sender, battleTag)
if battleTag ~= select(2, BNGetInfo()) then
-- Check to see if the character/account is linked.
if not (app:IsAccountLinked(sender) or AllTheThingsAD.LinkedAccounts[battleTag]) then
return false;
end
end
-- Whitelist the character name, if not already. (This is needed for future sync methods)
AllTheThingsAD.LinkedAccounts[sender] = true;
-- Generate the sync string (there may be several depending on how many alts there are)
-- TODO: use app.TableConcat()
-- local msgs = {};
local msg = "?\tsyncsum";
for guid,character in pairs(ATTCharacterData) do
if character.lastPlayed then
local charsummary = "\t" .. guid .. ":" .. character.lastPlayed;
if (msg:len() + charsummary:len()) < 255 then
msg = msg .. charsummary;
else
SendAddonMessage("ATT", msg, "WHISPER", sender);
msg = "?\tsyncsum" .. charsummary;
end
end
end
SendAddonMessage("ATT", msg, "WHISPER", sender);
end
function app:ReceiveSyncSummary(sender, summary)
if app:IsAccountLinked(sender) then
local first = #queue == 0;
for i,data in ipairs(summary) do
local guid,lastPlayed = (":"):split(data);
local character = ATTCharacterData[guid];
if not character or not character.lastPlayed or (character.lastPlayed < tonumber(lastPlayed)) and guid ~= active then
tinsert(queue, { guid, character and character.text or guid, sender });
end
end
if first then processQueue(); end
end
end
function app:ReceiveSyncSummaryResponse(sender, summary)
if app:IsAccountLinked(sender) then
local rawMsg;
for i,guid in ipairs(summary) do
local character = ATTCharacterData[guid];
if character then
-- Put easy character data into a raw data string
local rawData = character.guid .. ":" .. character.name .. ":" .. character.lvl .. ":" .. character.text .. ":" .. (character.realm or " ") .. ":" .. (character.factionID or " ") .. ":" .. (character.classID or " ") .. ":" .. (character.raceID or " ") .. ":" .. character.lastPlayed .. ":" .. character.Deaths;
for i,field in ipairs(whiteListedFields) do
if character[field] then
rawData = rawData .. ":" .. field;
for index,value in pairs(character[field]) do
if value then
rawData = rawData .. "/" .. index;
end
end
end
end
if not rawMsg then
rawMsg = rawData;
else
rawMsg = rawMsg .. "\t" .. rawData;
end
end
end
if rawMsg then
-- Send Addon Message Back
local length = rawMsg:len();
local chunks = {};
for i=1,length,241 do
tinsert(chunks, rawMsg:sub(i, math.min(length, i + 240)));
end
local outgoingForSender = outgoing[sender];
if not outgoingForSender then
outgoingForSender = { ["total"] = 0, ["uids"] = {}};
outgoing[sender] = outgoingForSender;
end
local uid = outgoingForSender.total + 1;
outgoingForSender.uids[uid] = chunks;
outgoingForSender.total = uid;
-- Send Addon Message Back
SendAddonMessage("ATT", "chks\t" .. uid .. "\t" .. #chunks, "WHISPER", sender);
end
end
end
function app:Synchronize(automatically)
-- Update the last played timestamp. This ensures the sync process does NOT destroy unsaved progress on this character.
local battleTag = select(2, BNGetInfo());
if battleTag then
app.CurrentCharacter.lastPlayed = time();
local any, msg = false, "?\tsync\t" .. battleTag;
for playerName,allowed in pairs(AllTheThingsAD.LinkedAccounts) do
if allowed and not playerName:find("#") then
SendAddonMessage("ATT", msg, "WHISPER", playerName);
any = true;
end
end
if not any and not automatically then
app.print("You need to link a character or BNET account in the settings first before you can Sync accounts.");
end
end
end
function app:SynchronizeWithPlayer(playerName)
-- Update the last played timestamp. This ensures the sync process does NOT destroy unsaved progress on this character.
local battleTag = select(2, BNGetInfo());
if battleTag then
app.CurrentCharacter.lastPlayed = time();
SendAddonMessage("ATT", "?\tsync\t" .. battleTag, "WHISPER", playerName);
end
end
app.AddEventHandler("OnReady", function()
-- Attempt to register for the addon message prefix.
-- NOTE: This is only used by this old sync module and will be removed at some point.
C_ChatInfo.RegisterAddonMessagePrefix("ATT");
if app.Settings:GetTooltipSetting("Auto:Sync") then
app:Synchronize(true)
end
end);
end)();
do -- Main Data
-- Returns {name,icon} for a known HeaderConstants NPCID
local function SimpleHeaderGroup(npcID, t)
if t then
t.name = L.HEADER_NAMES[npcID]
t.icon = L.HEADER_ICONS[npcID]
if t.suffix then
t.name = t.name .. " (".. t.suffix ..")"
t.suffix = nil
end
else
t = {
name = L.HEADER_NAMES[npcID],
icon = L.HEADER_ICONS[npcID]
}
end
return t
end
function app:GetDataCache()
if not app.Categories then
return nil;
end
-- app.PrintMemoryUsage("app:GetDataCache init")
-- not really worth moving this into a Class since it's literally allowed to be used once
local DefaultRootKeys = {
__type = function(t) return "ROOT" end,
title = function(t)
return t.modeString .. DESCRIPTION_SEPARATOR .. t.untilNextPercentage
end,
progressText = function(t)
if not rawget(t,"TLUG") and app.CurrentCharacter then
local primeData = app.CurrentCharacter.PrimeData
if primeData then
return GetProgressColorText(primeData.progress, primeData.total)
end
end
return GetProgressColorText(t.progress, t.total)
end,
modeString = function(t)
return app.Settings:GetModeString()
end,
untilNextPercentage = function(t)
if not rawget(t,"TLUG") and app.CurrentCharacter then
local primeData = app.CurrentCharacter.PrimeData
if primeData then
return app.Modules.Color.GetProgressTextToNextPercent(primeData.progress, primeData.total)
end
end
return app.Modules.Color.GetProgressTextToNextPercent(t.progress, t.total)
end,
visible = app.ReturnTrue,
}
app.CloneDictionary(app.BaseClass.__class, DefaultRootKeys)
-- Update the Row Data by filtering raw data (this function only runs once)
local rootData = setmetatable({
key = "ROOT",
text = L.TITLE,
icon = app.asset("logo_32x32"),
preview = app.asset("Discord_2_128"),
description = L.DESCRIPTION,
font = "GameFontNormalLarge",
expanded = true,
g = {},
}, {
__index = function(t, key)
local defaultKeyFunc = DefaultRootKeys[key]
if defaultKeyFunc then return defaultKeyFunc(t) end
end,
__newindex = function(t, key, val)
-- app.PrintDebug("Top-Root-Set",rawget(t,"TLUG"),key,val)
if key == "visible" then
return;
end
-- until the Main list receives a top-level update
if not rawget(t,"TLUG") then
-- ignore setting progress/total values
if key == "progress" or key == "total" then
return;
end
end
rawset(t, key, val);
end
});
local g, db = rootData.g, nil;
-----------------------------------------
-- P R I M A R Y C A T E G O R I E S --
-----------------------------------------
-- Dungeons & Raids
db = app.CreateRawText(GROUP_FINDER);
db.g = app.Categories.Instances;
db.icon = app.asset("Category_D&R");
tinsert(g, db);
-- Delves
if app.Categories.Delves then
tinsert(g, app.CreateCustomHeader(app.HeaderConstants.DELVES, app.Categories.Delves));
end
-- Zones
if app.Categories.Zones then
db = app.CreateRawText(BUG_CATEGORY2);
db.g = app.Categories.Zones;
db.icon = app.asset("Category_Zones")
tinsert(g, db);
end
-- World Drops
if app.Categories.WorldDrops then
db = app.CreateCustomHeader(app.HeaderConstants.WORLD_DROPS, app.Categories.WorldDrops)
db.isWorldDropCategory = true;
tinsert(g, db);
end
-- Group Finder
if app.Categories.GroupFinder then
db = app.CreateRawText(DUNGEONS_BUTTON);
db.g = app.Categories.GroupFinder;
db.icon = app.asset("Category_GroupFinder")
tinsert(g, db);
end
-- Expansion Features
if app.Categories.ExpansionFeatures then
local text = GetCategoryInfo(15301)
db = app.CreateRawText(text);
db.g = app.Categories.ExpansionFeatures;
db.lvl = 10;
db.description = "These expansion features are new systems or ideas by Blizzard which are spread over multiple zones. For the ease of access & for the sake of reducing numbers, these are tagged as expansion features.\nIf an expansion feature is limited to 1 zone, it will continue being listed only under its respective zone.";
db.icon = app.asset("Category_ExpansionFeatures");
tinsert(g, db);
end
-- Holidays
if app.Categories.Holidays then
db = app.CreateCustomHeader(app.HeaderConstants.HOLIDAYS, app.Categories.Holidays);
db.isHolidayCategory = true;
db.difficultyID = 19 -- 'Event' difficulty, allows auto-expand logic to find it when queueing special holiday dungeons
db.SortType = "EventStart";
tinsert(g, db);
end
-- Events
if app.Categories.WorldEvents then
db = app.CreateRawText(BATTLE_PET_SOURCE_7);
db.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.";
db.icon = app.asset("Category_Event");
db.g = app.Categories.WorldEvents;
tinsert(g, db);
end
-- Promotions
if app.Categories.Promotions then
db = app.CreateRawText(BATTLE_PET_SOURCE_8);
db.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.";
db.icon = app.asset("Category_Promo");
db.g = app.Categories.Promotions;
db.isPromotionCategory = true;
tinsert(g, db);
end
-- Pet Battles
if app.Categories.PetBattles then
db = app.CreateCustomHeader(app.HeaderConstants.PET_BATTLES);
db.g = app.Categories.PetBattles;
tinsert(g, db);
end
-- PvP
if app.Categories.PVP then
db = app.CreateCustomHeader(app.HeaderConstants.PVP, app.Categories.PVP);
db.isPVPCategory = true;
tinsert(g, db);
end
-- Craftables
if app.Categories.Craftables then
db = app.CreateRawText(LOOT_JOURNAL_LEGENDARIES_SOURCE_CRAFTED_ITEM);
db.g = app.Categories.Craftables;
db.DontEnforceSkillRequirements = true;
db.icon = app.asset("Category_Crafting");
tinsert(g, db);
end
-- Professions
if app.Categories.Professions then
db = app.CreateCustomHeader(app.HeaderConstants.PROFESSIONS, app.Categories.Professions);
tinsert(g, db);
end
-- Secrets
if app.Categories.Secrets then
db = app.CreateCustomHeader(app.HeaderConstants.SECRETS, app.Categories.Secrets);
tinsert(g, db);
end
-- Housing
if app.Categories.Housing then
tinsert(g, app.CreateCustomHeader(app.HeaderConstants.HOUSING, app.Categories.Housing));
end
-----------------------------------------
-- L I M I T E D C A T E G O R I E S --
-----------------------------------------
-- Character
if app.Categories.Character then
db = app.CreateRawText(CHARACTER);
db.g = app.Categories.Character;
db.icon = app.asset("Category_ItemSets");
tinsert(g, db);
end
---------------------------------------
-- M A R K E T C A T E G O R I E S --
---------------------------------------
-- Black Market
if app.Categories.BlackMarket then tinsert(g, app.Categories.BlackMarket[1]); end
-- In-Game Store
if app.Categories.InGameShop then
db = app.CreateCustomHeader(app.HeaderConstants.IN_GAME_SHOP, app.Categories.InGameShop);
tinsert(g, db);
end
-- Trading Post
if app.Categories.TradingPost then
db = app.CreateRawText(TRANSMOG_SOURCE_7);
db.g = app.Categories.TradingPost;
db.icon = app.asset("Category_TradingPost");
tinsert(g, db);
end
-- Track Deaths!
tinsert(g, app:CreateDeathClass());
-- Yourself.
tinsert(g, app.CreateUnit("player", {
["description"] = L.DEBUG_LOGIN,
["races"] = { app.RaceIndex },
["c"] = { app.ClassIndex },
["r"] = app.FactionID,
["collected"] = 1,
["nmr"] = false,
["OnUpdate"] = function(self)
self.lvl = app.Level;
if app.MODE_DEBUG then
self.collectible = true;
else
self.collectible = false;
end
end
}));
-- Module-based Groups
app.HandleEvent("OnAddExtraMainCategories", g)
-- app.PrintMemoryUsage()
-- app.PrintDebug("Begin Cache Prime")
CacheFields(rootData);
-- app.PrintDebugPrior("Ended Cache Prime")
-- app.PrintMemoryUsage()
-- Achievements
if app.Categories.Achievements then
db = app.CreateCustomHeader(app.HeaderConstants.ACHIEVEMENTS, app.Categories.Achievements);
db.sourceIgnored = 1; -- everything in this category is now cloned!
for _, o in ipairs(db.g) do
o.sourceIgnored = nil
end
CacheFields(db, true, "Achievements")
tinsert(g, db);
end
-- Create Dynamic Groups Button
tinsert(g, app.CreateRawText(L.CLICK_TO_CREATE_FORMAT:format(L.DYNAMIC_CATEGORY_LABEL), {
icon = app.asset("Interface_CreateDynamic"),
OnUpdate = app.AlwaysShowUpdate,
sourceIgnored = true,
-- ["OnClick"] = function(row, button)
-- could implement logic to auto-populate all dynamic groups like before... will see if people complain about individual generation
-- end,
-- Top-Level Dynamic Categories
g = {
-- Future Unobtainable
app.CreateDynamicHeader("rwp", {
dynamic_withsubgroups = true,
dynamic_value = app.GameBuildVersion,
dynamic_searchcriteria = {
SearchValueCriteria = {
-- only include 'rwp' search results where the value is >= the current game version
function(o,field,value)
local rwp = o[field]
if not rwp then return end
return rwp >= value
end
}
},
name = L.FUTURE_UNOBTAINABLE,
description = L.FUTURE_UNOBTAINABLE_TOOLTIP,
icon = app.asset("Interface_Future_Unobtainable")
}),
-- Recently Added
app.CreateDynamicHeader("awp", {
dynamic_value = app.GameBuildVersion,
dynamic_withsubgroups = true,
name = L.NEW_WITH_PATCH,
description = L.NEW_WITH_PATCH_TOOLTIP,
icon = app.asset("Interface_Newly_Added")
}),
-- Achievements
app.CreateDynamicHeader("achievementID", SimpleHeaderGroup(app.HeaderConstants.ACHIEVEMENTS)),
-- Artifacts
app.CreateDynamicHeader("artifactID", SimpleHeaderGroup(app.HeaderConstants.ARTIFACTS)),
-- Azerite Essences
app.CreateDynamicHeader("azeriteessenceID", SimpleHeaderGroup(app.HeaderConstants.AZERITE_ESSENCES)),
-- Battle Pets
app.CreateDynamicHeader("speciesID", {
name = AUCTION_CATEGORY_BATTLE_PETS,
icon = app.asset("Category_PetJournal")
}),
-- Campsites
app.CreateDynamicHeader("campsiteID", {
name = WARBAND_SCENES,
icon = app.asset("Category_Campsites")
}),
-- Character Unlocks
app.CreateDynamicHeader("characterUnlock", {
name = L.CHARACTERUNLOCKS_CHECKBOX,
icon = app.asset("Category_ItemSets")
}),
-- Conduits
app.CreateDynamicHeader("conduitID", SimpleHeaderGroup(app.HeaderConstants.CONDUITS, {suffix=EXPANSION_NAME8})),
-- Currencies
app.CreateDynamicHeaderByValue("currencyID", {
dynamic_withsubgroups = true,
name = CURRENCY,
icon = app.asset("Interface_Vendor")
}),
-- Decor
app.CreateDynamicHeader("decorID", {
name = CATALOG_SHOP_TYPE_DECOR,
icon = app.asset("Category_Housing")
}),
-- Factions
app.CreateDynamicHeaderByValue("factionID", {
dynamic_withsubgroups = true,
name = L.FACTIONS,
icon = app.asset("Category_Factions")
}),
-- Flight Paths
app.CreateDynamicHeader("flightpathID", {
name = L.FLIGHT_PATHS,
icon = app.asset("Category_FlightPaths")
}),
-- Followers
app.CreateDynamicHeader("followerID", SimpleHeaderGroup(app.HeaderConstants.FOLLOWERS)),
-- Garrison Buildings
-- TODO: doesn't seem to work...
-- app.CreateDynamicHeader("garrisonbuildingID", SimpleHeaderGroup(app.HeaderConstants.BUILDINGS)),
-- Heirlooms
app.CreateDynamicHeader("heirloomID", SimpleHeaderGroup(app.HeaderConstants.HEIRLOOMS)),
-- Illusions
app.CreateDynamicHeader("illusionID", {
name = L.FILTER_ID_TYPES[103],
icon = app.asset("Category_Illusions")
}),
-- Mounts
app.CreateDynamicHeader("mountID", {
name = MOUNTS,
icon = app.asset("Category_Mounts")
}),
-- Mount Mods
app.CreateDynamicHeader("mountmodID", SimpleHeaderGroup(app.HeaderConstants.MOUNT_MODS)),
-- Pet Battles
app.CreateDynamicHeader("pb", SimpleHeaderGroup(app.HeaderConstants.PET_BATTLES, {dynamic_withsubgroups = true})),
-- Professions
app.CreateDynamicHeaderByValue("professionID", {
dynamic_withsubgroups = true,
dynamic_valueField = "requireSkill",
name = TRADE_SKILLS,
icon = app.asset("Category_Professions")
}),
-- Runeforge Powers
app.CreateDynamicHeader("runeforgepowerID", SimpleHeaderGroup(app.HeaderConstants.LEGENDARIES, {suffix=EXPANSION_NAME8})),
-- Titles
app.CreateDynamicHeader("titleID", {
name = PAPERDOLL_SIDEBAR_TITLES,
icon = app.asset("Category_Titles")
}),
-- Toys
app.CreateDynamicHeader("toyID", {
name = TOY_BOX,
icon = app.asset("Category_ToyBox")
}),
-- Various Quest groups
app.CreateCustomHeader(app.HeaderConstants.QUESTS, {
visible = true,
OnUpdate = app.AlwaysShowUpdate,
g = {
-- Breadcrumbs
app.CreateDynamicHeader("isBreadcrumb", {
name = L.BREADCRUMBS,
icon = 134051
}),
-- Dailies
app.CreateDynamicHeader("isDaily", {
name = DAILY,
icon = app.asset("Interface_Questd")
}),
-- Weeklies
app.CreateDynamicHeader("isWeekly", {
name = CALENDAR_REPEAT_WEEKLY,
icon = app.asset("Interface_Questw")
}),
-- HQTs
app.CreateDynamicHeader("isHQT", {
name = MINIMAP_TRACKING_HIDDEN_QUESTS,
icon = app.asset("Interface_Quest"),
}),
-- All Quests
-- this works but..... bad idea instead use /att list type=quest limit=79000
-- app.CreateDynamicHeaderByValue("questID", {
-- dynamic_withsubgroups = true,
-- name = QUESTS_LABEL,
-- icon = app.asset("Interface_Quest_header")
-- }),
}
}),
},
}));
-- The Main Window's Data
-- app.PrintMemoryUsage("Prime.Data Ready")
local primeWindow = app:GetWindow("Prime");
primeWindow:SetData(rootData);
-- app.PrintMemoryUsage("Prime Window Data Building...")
primeWindow:BuildData();
-- Function to build a hidden window's data
local AllHiddenWindows = {}
local function BuildHiddenWindowData(name, icon, description, category, flags)
if not app.Categories[category] then return end
local windowData = app.CreateRawText(Colorize(name, flags and flags.Color or app.Colors.ChatLinkError), app.Categories[category])
windowData.title = name .. DESCRIPTION_SEPARATOR .. app.Version
windowData.icon = app.asset(icon)
windowData.description = description
windowData.font = "GameFontNormalLarge"
for k, v in pairs(flags or app.EmptyTable) do
windowData[k] = v
end
CacheFields(windowData, true)
AllHiddenWindows[#AllHiddenWindows + 1] = windowData
-- Filter for Never Implemented things
if category == "NeverImplemented" then
app.AssignFieldValue(windowData, "u", 1)
end
local window = app:GetWindow(category)
window.AdHoc = true
window:SetData(windowData)
window:BuildData()
end
-- Build all the hidden window's data
BuildHiddenWindowData(L.UNSORTED, "WindowIcon_Unsorted", L.UNSORTED_DESC, "Unsorted", { _missing = true, _unsorted = true, _nosearch = true })
BuildHiddenWindowData(L.NEVER_IMPLEMENTED, "status-unobtainable", L.NEVER_IMPLEMENTED_DESC, "NeverImplemented", { _nyi = true, _nosearch = true })
BuildHiddenWindowData(L.HIDDEN_ACHIEVEMENT_TRIGGERS, "Category_Achievements", L.HIDDEN_ACHIEVEMENT_TRIGGERS_DESC, "HiddenAchievementTriggers", { _hqt = true, _nosearch = true, Color = app.Colors.ChatLinkHQT })
BuildHiddenWindowData(L.HIDDEN_CURRENCY_TRIGGERS, "Interface_Vendor", L.HIDDEN_CURRENCY_TRIGGERS_DESC, "HiddenCurrencyTriggers", { _hqt = true, _nosearch = true, Color = app.Colors.ChatLinkHQT })
BuildHiddenWindowData(L.HIDDEN_QUEST_TRIGGERS, "Interface_Quest", L.HIDDEN_QUEST_TRIGGERS_DESC, "HiddenQuestTriggers", { _hqt = true, _nosearch = true, Color = app.Colors.ChatLinkHQT })
BuildHiddenWindowData(L.SOURCELESS, "WindowIcon_Unsorted", L.SOURCELESS_DESC, "Sourceless", { _missing = true, _unsorted = true, _nosearch = true, Color = app.Colors.TooltipWarning })
-- app.PrintMemoryUsage("Hidden Windows Data Done")
-- a single Unsorted window to collect all base Unsorted windows
-- TODO: migrate this logic once Window creation is revised
app.ChatCommands.Add("all-hidden", function(args)
local window = app:GetWindow("all-hidden")
if window and not window.HasPendingUpdate then window:Toggle() return true end
-- local allHiddenSearch = app:BuildTargettedSearchResponse(AllUnsortedGroups, "_nosearch", true, nil, {ParentInclusionCriteria={},SearchCriteria={},SearchValueCriteria={}})
local windowData = app.CreateRawText(Colorize("All-Hidden", app.Colors.ChatLinkError), {
-- clone all unhidden groups into this window
g = CreateObject(AllHiddenWindows),
title = "All-Hidden" .. DESCRIPTION_SEPARATOR .. app.Version,
icon = app.asset("status-unobtainable"),
description = "All Hidden ATT Content",
font = "GameFontNormalLarge",
AdHoc = true
})
window:SetData(windowData)
window:BuildData()
window:Toggle()
return true
end, {
"Usage : /att all-hidden",
"Provides a single command to open all Hidden content in a single window",
})
-- StartCoroutine("VerifyRecursionUnsorted", function() app.VerifyCache(); end, 5);
-- app.PrintMemoryUsage("Finished loading data cache")
-- app.PrintMemoryUsage()
app.GetDataCache = function()
-- app.PrintDebug("Cached data cache")
return rootData;
end
return rootData;
end
end -- Dynamic/Main Data
-- Store the Custom Windows Update functions which are required by specific Windows
(function()
local customWindowUpdates = { params = {} };
-- Returns the Custom Update function based on the Window suffix if existing
function app:CustomWindowUpdate(suffix)
return customWindowUpdates[suffix];
end
-- Retrieves the value of the specific attribute for the given window suffix
app.GetCustomWindowParam = function(suffix, name)
local params = customWindowUpdates.params[suffix];
-- app.PrintDebug("GetCustomWindowParam",suffix,name,params and params[name])
return params and params[name] or nil;
end
-- Defines the value of the specific attribute for the given window suffix
app.SetCustomWindowParam = function(suffix, name, value)
local params = customWindowUpdates.params;
if params[suffix] then params[suffix][name] = value;
else params[suffix] = { [name] = value } end
-- app.PrintDebug("SetCustomWindowParam",suffix,name,params[suffix][name])
end
-- Removes the custom attributes for a given window suffix
app.ResetCustomWindowParam = function(suffix)
customWindowUpdates.params[suffix] = nil;
-- app.PrintDebug("ResetCustomWindowParam",suffix)
end
-- Allows externally adding custom window update logic which doesn't exist already
app.AddCustomWindowOnUpdate = function(customName, onUpdate)
if customWindowUpdates[customName] then
app.print("Cannot replace Custom Window: "..customName)
end
-- app.print("Added",customName)
customWindowUpdates[customName] = onUpdate
end
app.AddCustomWindowOnUpdate("Bounty", function(self, force, got)
if not self.initialized then
if not app:GetDataCache() then -- This module requires a valid data cache to function correctly.
return;
end
self.initialized = true;
self:SetData(app.CreateCustomHeader(app.HeaderConstants.UI_BOUNTY_WINDOW, {
["visible"] = true,
["g"] = app:BuildSearchResponse("isBounty"),
}))
self:BuildData();
self.ExpandInfo = { Expand = true, Manual = true };
end
if self:IsVisible() then
--[[
-- Update the groups without forcing Debug Mode.
local visibleState = app.Modules.Filter.Get.Visible();
app.Modules.Filter.Set.Visible()
self:BuildData();
self:DefaultUpdate(force);
app.Modules.Filter.Set.Visible(visibleState)
]]--
-- Force Debug Mode
local rawSettings = app.Settings:GetRawSettings("General");
local debugMode = app.MODE_DEBUG;
if not debugMode then
rawSettings.DebugMode = true;
app.Settings:UpdateMode();
end
self:DefaultUpdate(force);
if not debugMode then
rawSettings.DebugMode = debugMode;
app.Settings:UpdateMode();
end
end
end)
app.AddCustomWindowOnUpdate("CosmicInfuser", function(self, force)
if self:IsVisible() then
if not self.initialized then
self.initialized = true;
force = true;
local g = {};
local rootData = app.CreateRawText("Cosmic Infuser", {
icon = app.asset("Category_Zones"),
description = "This window helps debug when we're missing map IDs in the addon.",
OnUpdate = app.AlwaysShowUpdate,
back = 1,
indent = 0,
visible = true,
expanded = true,
g = g,
})
-- Cache all maps by their ID number, starting with maps we reference in our DB.
local mapsByID = {};
for mapID,cachedMaps in pairs(app.SearchForFieldContainer("mapID")) do
if not mapsByID[mapID] then
local mapObject = app.CreateMap(mapID, {
mapInfo = C_Map_GetMapInfo(mapID),
collectible = true,
collected = true,
statistic = tostring(#cachedMaps),
});
mapsByID[mapID] = mapObject;
mapObject.g = {}; -- Doing this prevents the CreateMap function from creating an exploration header.
end
end
-- Go through all of the possible maps, including only maps that have C_Map data.
for mapID=1,10000,1 do
if not mapsByID[mapID] then
local mapInfo = C_Map_GetMapInfo(mapID);
if mapInfo then
local mapObject = app.CreateMap(mapID, {
mapInfo = mapInfo,
collectible = true,
collected = false
});
mapsByID[mapID] = mapObject;
mapObject.g = {}; -- Doing this prevents the CreateMap function from creating an exploration header.
end
end
end
-- Iterate through the maps we have cached, determine their parents and link them together.
-- Also push them on to the stack.
for mapID,mapObject in pairs(mapsByID) do
local parent = rootData;
if mapObject.mapInfo then
local parentMapID = mapObject.mapInfo.parentMapID;
if parentMapID and parentMapID > 0 then
local parentMapObject = mapsByID[parentMapID];
if parentMapObject then
parent = parentMapObject;
else
print("Failed to find parent map in the mapsByID table!", parentMapID);
end
end
end
mapObject.parent = parent;
tinsert(parent.g, mapObject);
end
-- Sort the maps by number of relative maps, then by name if matching.
app.Sort(g, function(a, b)
local aSize, bSize = #a.g, #b.g;
if aSize > bSize then
return true;
elseif bSize == aSize then
return b.name > a.name;
else
return false;
end
end, true);
-- Now finally, clear out unused gs.
for i,mapObject in ipairs(g) do
if #mapObject.g < 1 then
mapObject.g = nil;
end
end
self:SetData(rootData);
end
-- Update the window and all of its row data
self:DefaultUpdate(force);
end
end)
app.AddCustomWindowOnUpdate("CurrentInstance", function(self, force, got)
-- app.PrintDebug("CurrentInstance:Update",force,got)
if not self.initialized then
force = true;
self.initialized = true;
self.CurrentMaps = {};
self.mapID = -1;
self.IsSameMapID = function(self)
return self.CurrentMaps[self.mapID];
end
self.SetMapID = function(self, mapID)
-- app.PrintDebug("SetMapID",mapID)
self.mapID = mapID;
self:SetVisible(true);
self:Update();
end
-- local C_Map_GetMapChildrenInfo = C_Map.GetMapChildrenInfo;
-- Wraps a given object such that it can act as an unfiltered Header of the base group
local CreateWrapVisualHeader = app.CreateVisualHeaderWithGroups
-- Returns the consolidated data format for the next header level
-- Headers are forced not collectible, and will have their content sorted, and can be copied from the existing Source header
local function CreateHeaderData(group, header)
-- copy an uncollectible version of the existing header
if header then
-- special case for Difficulty headers, need to be actual difficulty groups to merge properly with any existing
if header.difficultyID then
header = CreateObject(header, true)
header.g = { group }
return header
end
-- special case for Map auto-headers, ignore re-nesting a Map header of the current Map
if header.type == "m" and header.keyval == self.mapID then
return group
end
header = CreateWrapVisualHeader(header, {group})
header.SortType = "Global"
return header
else
return { g = { group }, ["collectible"] = false, SortType = "Global" };
end
end
-- set of keys for headers which can be nested in the minilist automatically, but not confined to a direct top header
local subGroupKeys = {
"filterID",
"professionID",
"raceID",
"eventID",
"instanceID",
"achievementID",
};
-- set of keys for headers which can be nested in the minilist within an Instance automatically, but not confined to a direct top header
local subGroupInstanceKeys = {
"filterID",
"professionID",
"raceID",
"eventID",
"achievementID",
};
-- Headers possible in a hierarchy that should just be ignored
local ignoredHeaders = app.HeaderData.IGNOREINMINILIST or app.EmptyTable;
local function BuildDiscordMapInfoTable(id, mapInfo)
-- Builds a table to be used in the SetupReportDialog to display text which is copied into Discord for player reports
mapInfo = mapInfo or C_Map_GetMapInfo(id)
local info = {
"### missing-map"..":"..id,
"```elixir", -- discord fancy box start
"L:"..app.Level.." R:"..app.RaceID.." ("..app.Race..") C:"..app.ClassIndex.." ("..app.Class..")",
id and ("mapID:"..id.." ("..(mapInfo.name or ("Map ID #" .. id))..")") or "mapID:??",
"real-name:"..(GetRealZoneText() or "?"),
"sub-name:"..(GetSubZoneText() or "?"),
};
local mapID = mapInfo.parentMapID
while mapID do
mapInfo = C_Map_GetMapInfo(mapID)
if mapInfo then
tinsert(info, "> parentMapID:"..mapID.." ("..(mapInfo.name or "??")..")")
mapID = mapInfo.parentMapID;
else break
end
end
local position, coord = id and C_Map.GetPlayerMapPosition(id, "player"), nil;
if position then
local x,y = position:GetXY();
coord = (math_floor(x * 1000) / 10) .. ", " .. (math_floor(y * 1000) / 10);
end
tinsert(info, coord and ("coord:"..coord) or "coord:??");
if app.GameBuildVersion >= 100000 then -- Only include this after Dragonflight
local acctUnlocks = {
IsQuestFlaggedCompleted(72366) and "DF_CA" or "N", -- Dragonflight Campaign Complete
IsQuestFlaggedCompleted(75658) and "DF_ZC" or "N", -- Dragonflight Zaralek Caverns Complete
IsQuestFlaggedCompleted(79573) and "WW_CA" or "N", -- The War Within Campaign Complete
}
tinsert(info, "unlocks:"..app.TableConcat(acctUnlocks, nil, nil, "/"))
end
tinsert(info, "lq:"..(app.TableConcat(app.MostRecentQuestTurnIns or app.EmptyTable, nil, nil, "<") or ""));
local inInstance, instanceType = IsInInstance()
tinsert(info, "instance:"..(inInstance and "true" or "false")..":"..(instanceType or ""))
tinsert(info, "ver:"..app.Version);
tinsert(info, "build:"..app.GameBuildVersion);
tinsert(info, "```"); -- discord fancy box end
return info
end
(function()
local results, groups, nested, header, headerKeys, difficultyGroup, nextParent, headerID, isInInstance
local rootGroups, mapGroups = {}, {};
self.MapCache = setmetatable({}, { __mode = "kv" })
local function TrySwapFromCache()
-- window to keep cached maps/not re-build & update them
local expired = GetTimePreciseSec() - 60
for mapID,mapData in pairs(self.MapCache) do
-- app.PrintDebug("Check expired cached map",mapID,mapData._lastshown,expired)
if mapData._lastshown < expired then
-- app.PrintDebug("Removed cached map",mapID,mapData._lastshown,expired)
self.MapCache[mapID] = nil
end
end
local mapID = self.mapID
header = self.MapCache[mapID]
if not header then return end
if not header._maps[mapID] then
-- app.PrintDebug("cache maps cleared! rebuild new for",mapID)
self.MapCache[mapID] = nil
return
end
-- app.PrintDebug("Loaded cached Map",mapID)
header._lastshown = GetTimePreciseSec()
self:SetData(header)
self.CurrentMaps = header._maps
-- app.PrintTable(self.CurrentMaps)
-- Reset the Fill if needed
if not header._fillcomplete then
-- app.PrintDebug("Re-fill cached Map",mapID)
app.SetSkipLevel(2);
app.FillGroups(header);
app.SetSkipLevel(0);
end
Callback(self.Update, self);
return true
end
app.AddEventHandler("OnSettingsNeedsRefresh", function()
-- if settings change that requrie refresh, wipe cached maps
wipe(self.MapCache)
end)
self.Rebuild = function(self)
-- Reset the minilist Runner before building new data
self:GetRunner().Reset()
if TrySwapFromCache() then return end
-- app.PrintDebug("Rebuild",self.mapID);
local currentMaps, mapID = {}, self.mapID
-- Get all results for this map
results = SearchForField("mapID", mapID)
-- app.PrintDebug("Rebuild#",#results);
if results and #results > 0 then
-- I tend to like this way of finding sub-maps, but it does mean we rely on Blizzard and get whatever maps they happen to claim
-- are children of a given map... sometimes has weird results like scenarios during quests being considered children in
-- other zones. Since it can give us special top-level maps (Anniversary AV) also as children of other top-level maps (Hillsbarad)
-- we would need to filter the results and add them properly into the results below via sub-groups if they are maps themselves
-- local submapinfos = ArrayAppend(C_Map_GetMapChildrenInfo(mapID, 5), C_Map_GetMapChildrenInfo(mapID, 6))
-- if submapinfos then
-- for _,mapInfo in ipairs(submapinfos) do
-- subresults = SearchForField("mapID", mapInfo.mapID)
-- app.PrintDebug("Adding Sub-Map Results:",mapInfo.mapID,mapInfo.mapType,#subresults)
-- results = ArrayAppend(results, subresults)
-- end
-- end
-- See if there are any sub-maps we should also include by way of the 'maps' field on the 'real' map for this id
local rootMap, result
for i=1,#results do
result = results[i]
if result.key == "mapID" and result.mapID == mapID then
rootMap = result
break;
end
end
local rootMaps = rootMap and rootMap.maps
if rootMaps then
local subresults, subMapID
for i=1,#rootMaps do
subMapID = rootMaps[i]
if subMapID ~= mapID then
subresults = SearchForField("mapID", subMapID)
-- app.PrintDebug("Adding Sub-Map Results:",subMapID,#subresults)
results = ArrayAppend(results, subresults)
end
end
end
-- Simplify the returned groups
groups = {};
wipearray(rootGroups);
wipearray(mapGroups);
header = { mapID = mapID, g = groups }
currentMaps[mapID] = true;
isInInstance = IsInInstance();
headerKeys = isInInstance and subGroupInstanceKeys or subGroupKeys;
local group, groupmapID, groupmaps
-- split search results by whether they represent the 'root' of the minilist or some other mapped content
for i=1,#results do
-- do not use any raw Source groups in the final list
group = CreateObject(results[i]);
groupmapID = group.mapID
groupmaps = group.maps
-- Instance/Map/Class/Header(of current map) groups are allowed as root of minilist
if (group.instanceID or (groupmapID and (group.key == "mapID" or (group.key == "headerID" and groupmapID == mapID))) or group.key == "classID")
-- and actually match this minilist...
-- only if this group mapID matches the minilist mapID directly or by maps
and (groupmapID == mapID or (groupmaps and contains(groupmaps, mapID))) then
rootGroups[#rootGroups + 1] = group
else
mapGroups[#mapGroups + 1] = group
end
end
-- first merge all root groups into the list
local groupMaps
for i=1,#rootGroups do
group = rootGroups[i]
groupMaps = group.maps
if groupMaps then
for i=1,#groupMaps do
currentMaps[groupMaps[i]] = true;
end
end
-- app.PrintDebug("Merge as Root",group.hash)
MergeProperties(header, group, true);
NestObjects(header, group.g);
end
local externalMaps = {}
-- then merge all mapped groups into the list
for i=1,#mapGroups do
group = mapGroups[i]
-- app.PrintDebug("Mapping:",app:SearchLink(group))
nested = nil;
difficultyGroup = nil
-- Get the header chain for the group
nextParent = group.parent;
-- Cache the difficultyGroup, if there is one and we are in an actual instance where the group is being mapped
if isInInstance then
difficultyGroup = GetRelativeGroup(nextParent, "difficultyID")
-- app.PrintDebug("difficultyGroup:",app:SearchLink(difficultyGroup))
end
-- Building the header chain for each mapped Thing
while nextParent do
headerID = nextParent.headerID
if headerID then
-- all Headers implicitly are allowed as visual headers in minilist unless explicitly ignored
if not ignoredHeaders[headerID] then
group = CreateHeaderData(group, nextParent);
nested = true;
end
elseif nextParent.isMinilistHeader then
group = CreateHeaderData(group, nextParent);
nested = true;
else
for i=1,#headerKeys do
if nextParent[headerKeys[i]] then
-- create the specified group Type header
group = CreateHeaderData(group, nextParent);
nested = true;
break;
end
end
end
nextParent = nextParent.parent;
end
-- really really special cases...
-- Battle Pets get an additional raw Filter nesting
if not nested and group.key == "speciesID" then
group = app.CreateFilter(101, CreateHeaderData(group));
end
-- If relative to a difficultyGroup, then merge it into one.
if difficultyGroup then
group = CreateHeaderData(group, difficultyGroup);
-- remove the name sorttype from the difficulty-based header
group.SortType = nil
-- link the difficulty group to the current window header so that it assumes its expected hash
group.parent = header
group.sourceParent = nil
end
-- If we're trying to map in another 'map', nest it into a special group for external maps
if group.instanceID or group.mapID then
externalMaps[#externalMaps + 1] = group
group = nil
end
if group then
-- app.PrintDebug("Merge as Mapped:",app:SearchLink(group))
MergeObject(groups, group)
end
end
-- Nest our external maps into a special header to reduce minilist header spam
if #externalMaps > 0 then
local externalMapHeader = app.CreateRawText(TRACKER_FILTER_REMOTE_ZONES, {icon=450908,description=L.REMOTE_ZONES_DESCRIPTION,external=true})
externalMapHeader.SortType = "Global";
NestObjects(externalMapHeader, externalMaps)
MergeObject(groups, externalMapHeader)
end
if #rootGroups == 0 then
-- if only one group in the map root, then shift it up as the map root instead
local headerGroups = header.g;
if #headerGroups == 1 then
local topGroup = headerGroups[1]
if not topGroup.external
-- only shift up certain group types
and (topGroup.instanceID or topGroup.classID or topGroup.mapID)
then
header.g = nil;
-- don't persist the parent links since this will now be a minilist root
topGroup.parent = nil
topGroup.sourceParent = nil
MergeProperties(header, topGroup, true);
NestObjects(header, topGroup.g);
end
else
app.PrintDebug("No root Map groups!",mapID)
end
end
header.u = nil;
header.e = nil;
if header.instanceID then
header = app.CreateInstance(header.instanceID, header);
else
if header.classID then
header = app.CreateCharacterClass(header.classID, header);
else
header = app.CreateMap(header.mapID, header);
end
-- sort top level by name if not in an instance
header.SortType = "Global";
end
-- Swap out the map data for the header.
self:SetData(header);
header._maps = currentMaps
header._lastshown = GetTimePreciseSec()
-- app.PrintDebug("Saved cached Map",mapID,header._lastshown)
self.MapCache[mapID] = header
-- Fill up the groups that need to be filled!
app.SetSkipLevel(2);
app.FillGroups(header);
app.SetSkipLevel(0);
self:BuildData();
self.CurrentMaps = currentMaps;
-- Make sure to scroll to the top when being rebuilt
self.ScrollBar:SetValue(1);
else
-- If we don't have any data cached for this mapID and it exists in game, report it to the chat window.
self.CurrentMaps = {[mapID]=true};
local mapInfo = C_Map_GetMapInfo(mapID);
if mapInfo then
-- only report for mapIDs which actually exist
mapID = self.mapID
-- Linkify the output
local popupID = "map-" .. mapID
app:SetupReportDialog(popupID, "Missing Map: " .. mapID, BuildDiscordMapInfoTable(mapID, mapInfo))
app.report(app:Linkify(app.Version.." (Click to Report) No data found for this Location!", app.Colors.ChatLinkError, "dialog:" .. popupID));
end
self:SetData(app.CreateMap(mapID, {
["text"] = L.MINI_LIST .. " [" .. mapID .. "]",
["icon"] = 237385,
["description"] = L.MINI_LIST_DESC,
["visible"] = true,
["g"] = {
app.CreateRawText(L.UPDATE_LOCATION_NOW, {
["icon"] = 134269,
["description"] = L.UPDATE_LOCATION_NOW_DESC,
["OnClick"] = function(row, button)
Callback(app.LocationTrigger)
return true;
end,
["OnUpdate"] = app.AlwaysShowUpdate,
}),
},
}));
self:BuildData();
end
-- app.PrintDebugPrior("RB-Done")
return true;
end
end)()
self.RefreshLocation = function(show)
-- Acquire the new map ID.
local mapID = app.CurrentMapID;
-- app.PrintDebug("RefreshLocation",mapID)
-- can't really do anything about this from here anymore
if not mapID then return end
-- don't auto-load minimap to anything higher than a 'Zone' if we are in an instance, unless it has no parent?
if IsInInstance() then
local mapInfo = app.CurrentMapInfo;
if mapInfo and mapInfo.parentMapID and (mapInfo.mapType or 0) < 3 then
-- app.PrintDebug("Don't load Large Maps in minilist")
return;
end
end
-- Cache that we're in the current map ID.
-- app.PrintDebug("new map");
self.mapID = mapID;
if show then
self:SetVisible(true)
end
-- force update when showing the minilist
Callback(self.Update, self);
end
end
if self:IsVisible() then
-- Update the window and all of its row data
if not self:IsSameMapID() then
-- app.PrintDebug("Leaving map",self.data.mapID)
self.data._lastshown = GetTimePreciseSec()
force = self:Rebuild();
else
-- Update the mapID into the data for external reference in case not rebuilding
self.data.mapID = self.mapID;
end
self:DefaultUpdate(force, got);
end
end)
app.AddCustomWindowOnUpdate("ItemFilter", function(self, force)
if self:IsVisible() then
if not app:GetDataCache() then -- This module requires a valid data cache to function correctly.
return;
end
if not self.initialized then
self.initialized = true;
function self:Clear()
local temp = self.data.g[1];
wipe(self.data.g);
tinsert(self.data.g, temp);
end
function self:Search(field, value)
value = value or true
-- app.PrintDebug("Search",field,value)
local results = app:BuildSearchResponse(field, value, {g=true});
-- app.PrintDebug("Results",#results)
ArrayAppend(self.data.g, results);
self.data.text = L.ITEM_FILTER_TEXT..(" [%s=%s]"):format(field,tostring(value == app.Modules.Search.SearchNil and "nil" or value));
end
-- Item Filter
local data = app.CreateRawText(L.ITEM_FILTER_TEXT, {
['icon'] = app.asset("Category_ItemSets"),
["description"] = L.ITEM_FILTER_DESCRIPTION,
['visible'] = true,
['back'] = 1,
['g'] = {
app.CreateRawText(L.ITEM_FILTER_BUTTON_TEXT, {
['icon'] = 134246,
['description'] = L.ITEM_FILTER_BUTTON_DESCRIPTION,
['visible'] = true,
['OnUpdate'] = app.AlwaysShowUpdate,
['OnClick'] = function(row, button)
app:ShowPopupDialogWithEditBox(L.ITEM_FILTER_POPUP_TEXT, "", function(input)
local text = input:lower();
local f = tonumber(text);
if text ~= "" and tostring(f) ~= text then
text = text:gsub("-", "%%-");
-- app.PrintDebug("search match",text)
-- The string form did not match, the filter must have been by name.
for id,filter in pairs(L.FILTER_ID_TYPES) do
if filter:lower():match(text) then
f = tonumber(id);
break;
end
end
end
self:Clear();
if f then
self:Search("f", f);
else
-- direct field search
local field, value = ("="):split(input);
value = tonumber(value) or value;
if value and value ~= "" then
-- allows performing a value search when looking for 'nil'
if value == "nil" then
value = app.Modules.Search.SearchNil;
-- use proper bool values if specified
elseif value == "true" then
value = true;
elseif value == "false" then
value = false;
end
self:Search(field, value);
else
self:Search(field);
end
end
-- maybe local table of common fields from lowercase -> match
self:BuildData();
self:Update(true);
end);
return true;
end,
}),
},
});
self:SetData(data);
self:BuildData();
end
self:DefaultUpdate(force);
end
end)
app.AddCustomWindowOnUpdate("NWP", function(self, force)
if not self.initialized then
if not app:GetDataCache() then -- This module requires a valid data cache to function correctly.
return;
end
self.initialized = true;
local TypeGroupOverrides = {
visible = true
}
local function OnUpdate_RemoveEmptyDynamic(t)
-- nothing to show so don't be visible
if not t.g or #t.g == 0 then
return
end
local o
for i=#t.g,1,-1 do
o = t.g[i]
if o.__empty then
tremove(t.g, i)
end
end
if #t.g == 0 then
return
end
t.visible = true
return true
end
local function CreateTypeGroupsForHeader(header, searchResults)
-- TODO: professions would be more complex since it's so many sub-groups to organize
-- maybe just simpler to look for the 'requireSkill' field and put all those results into one 'Professions' group?
-- app.PrintDebug("Creating type group header",header.name, header.id, searchResults and #searchResults)
local typeGroup = app.CreateRawText(header.name, header)
local headerDataWithinPatch = app:BuildTargettedSearchResponse(searchResults, header.id, nil, {g=true})
-- app.PrintDebug("Found",#headerDataWithinPatch,"search groups for",header.id)
NestObjects(typeGroup, headerDataWithinPatch)
-- did we populate nothing?
if not typeGroup.g or #typeGroup.g == 0 then
typeGroup.__empty = true
else
app.AssignChildren(typeGroup)
end
Callback(app.DirectGroupUpdate, typeGroup.parent)
return typeGroup
end
local function CreateNWPWindow()
-- Fetch search results
local searchResults = app:BuildSearchResponse("awp", app.GameBuildVersion)
-- Create the dynamic category
local dynamicCategory = app.CreateRawText(L.CLICK_TO_CREATE_FORMAT:format(L.DYNAMIC_CATEGORY_LABEL), {
icon = app.asset("Interface_CreateDynamic"),
OnUpdate = OnUpdate_RemoveEmptyDynamic,
g = {}
})
-- Dynamic category headers
-- TODO: If possible, change the creation of names and icons to SimpleHeaderGroup to take the localized names
local headers = {
{ id = "achievementID", name = ACHIEVEMENTS, icon = app.asset("Category_Achievements") },
{ id = "sourceID", name = "Appearances", icon = 135276 },
{ id = "artifactID", name = ITEM_QUALITY6_DESC, icon = app.asset("Weapon_Type_Artifact") },
{ id = "azeriteessenceID", name = SPLASH_BATTLEFORAZEROTH_8_2_0_FEATURE2_TITLE, icon = app.asset("Category_AzeriteEssences") },
{ id = "speciesID", name = AUCTION_CATEGORY_BATTLE_PETS, icon = app.asset("Category_PetJournal") },
{ id = "campsiteID", name = WARBAND_SCENES, icon = app.asset("Category_Campsites") },
{ id = "characterUnlock", name = CHARACTER .. " " .. UNLOCK .. "s", icon = app.asset("Category_ItemSets") },
{ id = "conduitID", name = GetSpellName(348869) .. " (" .. EXPANSION_NAME8 .. ")", icon = 3601566 },
{ id = "currencyID", name = CURRENCY, icon = app.asset("Interface_Vendor") },
{ id = "decorID", name = CATALOG_SHOP_TYPE_DECOR, icon = app.asset("Category_Housing") },
{ id = "explorationID", name = "Exploration", icon = app.asset("Category_Exploration") },
{ id = "factionID", name = L.FACTIONS, icon = app.asset("Category_Factions") },
{ id = "flightpathID", name = L.FLIGHT_PATHS, icon = app.asset("Category_FlightPaths") },
{ id = "followerID", name = GARRISON_FOLLOWERS, icon = app.asset("Category_Followers") },
{ id = "heirloomID", name = HEIRLOOMS, icon = app.asset("Weapon_Type_Heirloom") },
{ id = "illusionID", name = L.FILTER_ID_TYPES[103], icon = app.asset("Category_Illusions") },
{ id = "mountID", name = MOUNTS, icon = app.asset("Category_Mounts") },
{ id = "mountmodID", name = "Mount Mods", icon = 975744 },
-- TODO: Add professions here using the byValue probably
{ id = "questID", name = TRACKER_HEADER_QUESTS, icon = app.asset("Interface_Quest_header") },
{ id = "runeforgepowerID", name = LOOT_JOURNAL_LEGENDARIES .. " (" .. EXPANSION_NAME8 .. ")", icon = app.asset("Weapon_Type_Legendary") },
{ id = "titleID", name = PAPERDOLL_SIDEBAR_TITLES, icon = app.asset("Category_Titles") },
{ id = "toyID", name = TOY_BOX, icon = app.asset("Category_ToyBox") },
}
-- Loop through the dynamic headers and insert them into the "g" field of dynamic category
for _, header in ipairs(headers) do
header.parent = dynamicCategory
dynamicCategory.g[#dynamicCategory.g + 1] = app.DelayLoadedObject(CreateTypeGroupsForHeader, "text", TypeGroupOverrides, header, searchResults)
end
-- Merge searchResults with dynamicCategory
tinsert(searchResults, dynamicCategory)
return searchResults
end
local NWPwindow = app.CreateRawText(L.NEW_WITH_PATCH, {
icon = app.asset("Interface_Newly_Added"),
description = L.NEW_WITH_PATCH_TOOLTIP,
visible = true,
back = 1,
g = CreateNWPWindow(),
})
self:SetData(NWPwindow);
self:BuildData();
end
if self:IsVisible() then
self:DefaultUpdate(force);
end
end)
app.AddCustomWindowOnUpdate("awp", function(self, force) -- TODO: Change this to remember window data of each expansion (param) and dont make new windows infinitely
-- Patch Interface Build tables
local CLASSIC = {10100,10200,10300,10400,10500,10600,10700,10800,10900,10903,11000,11100,11101,11102,11200,11201}
-- Classic was using different build numbers originally, so these are made up to make a correct timeline search
local TBC = {20000,20001,20003,20005,20006,20007,20008,20010,20012,20100,20101,
20102,20103,20200,20202,20203,20300,20302,20303,20400,20401,20402,20403}
-- TBC Patch 2.0.10 and 2.0.12 did not have a valid build numbers, so these are made up to make a correct timeline search
local WRATH = {30002,30003,30008,30100,30101,30102,30103,30200,30202,30300,30302,30303,30305}
local CATA = {40001,40003,40006,40100,40200,40202,40300,40302}
local MOP = {50004,50100,50200,50300,50400,50402,50407}
local WOD = {60002,60003,60100,60102,60200,60202,60203,60204}
local LEGION = {70003,70100,70105,70200,70205,70300,70302,70305}
local BFA = {80001,80100,80105,80200,80205,80300,80307}
local SL = {90001,90002,90005,90100,90105,90200,90205,90207}
local DF = {100000,100002,100005,100007,100100,100105,100107,100200,100205,100206,100207}
local TWW = {110000,110002,110005,110007,110100,110105,110107,110200,110205,110207}
local MID = {120000, 120001}
-- Locals
local param = {}
local foundExpansion = false
local expansionHeader, patchString, majorVersion, middleDigits, lastDigits, formattedPatch
-- Table to map expansion shortcuts to their respective parameters and headers
local expansions = {
classic = {param = CLASSIC, header = 1},
tbc = {param = TBC, header = 2},
wotlk = {param = WRATH, header = 3},
cata = {param = CATA, header = 4},
mop = {param = MOP, header = 5},
wod = {param = WOD, header = 6},
legion = {param = LEGION, header = 7},
bfa = {param = BFA, header = 8},
sl = {param = SL, header = 9},
df = {param = DF, header = 10},
tww = {param = TWW, header = 11},
mid = {param = MID, header = 12}
}
-- Function for dynamic groups
local function GetSearchCriteriaForPatch(patch)
local dynamic_searchcriteria = {
SearchValueCriteria = {
-- Only include 'awp' search results where the value is equal to the patch
function(o, field, value)
local awp = o[field]
if not awp then return end
return (app.GetRelativeValue(o, "awp") or 0) == patch
end
},
}
return dynamic_searchcriteria
end
-- Iterate over the expansions and check for the selected one
for k, v in pairs(expansions) do
if app.GetCustomWindowParam("awp", k) == true then
param = v.param
expansionHeader = v.header
foundExpansion = true
break
end
end
-- If no expansion was found, print an error message
if foundExpansion == false then
app.print("Unknown expansion shortcut.")
self:Hide();
elseif not self.initialized then
if not app:GetDataCache() then -- This module requires a valid data cache to function correctly.
return;
end
self.initialized = true;
local TypeGroupOverrides = {
visible = true
}
local function OnUpdate_RemoveEmptyDynamic(t)
-- nothing to show so don't be visible
if not t.g or #t.g == 0 then
return
end
local o
for i=#t.g,1,-1 do
o = t.g[i]
if o.__empty then
tremove(t.g, i)
end
end
if #t.g == 0 then
return
end
t.visible = true
return true
end
local function CreateTypeGroupsForHeader(header, searchResults)
-- TODO: professions would be more complex since it's so many sub-groups to organize
-- maybe just simpler to look for the 'requireSkill' field and put all those results into one 'Professions' group?
-- app.PrintDebug("Creating type group header",header.name, header.id, searchResults and #searchResults)
local typeGroup = app.CreateRawText(header.name, header)
local headerDataWithinPatch = app:BuildTargettedSearchResponse(searchResults, header.id, nil, {g=true})
-- app.PrintDebug("Found",#headerDataWithinPatch,"search groups for",header.id)
NestObjects(typeGroup, headerDataWithinPatch)
-- did we populate nothing?
if not typeGroup.g or #typeGroup.g == 0 then
typeGroup.__empty = true
else
app.AssignChildren(typeGroup)
end
Callback(app.DirectGroupUpdate, typeGroup.parent)
return typeGroup
end
local function CreatePatches(patchTable)
local patchBuild = {}
for _, patch in ipairs(patchTable) do
patchString = tostring(patch)
if math.floor(patch / 10000) < 10 then -- Before Dragonflight
majorVersion = patchString:sub(1, 1) -- "7" -- Patch 7.x.x
middleDigits = patchString:sub(2, 3) -- "02" -- Patch x.2.x
else -- After Dragonflight
majorVersion = patchString:sub(1, 2) -- "10" -- Patch 10.x.x
middleDigits = patchString:sub(3, 4) -- "02" -- Patch x.2.x
end
lastDigits = patchString:sub(-2) -- "02" -- Patch x.x.2
formattedPatch = majorVersion .. "." .. middleDigits .. lastDigits
-- Create the patch header
local patchHeader = app.CreateExpansion(formattedPatch, {g={}})
-- Fetch search results
local searchResults = app:BuildSearchResponse("awp", patch)
NestObjects(patchHeader, searchResults)
-- Create the dynamic category
local dynamicCategory = app.CreateRawText(L.CLICK_TO_CREATE_FORMAT:format(L.DYNAMIC_CATEGORY_LABEL), {
icon = app.asset("Interface_CreateDynamic"),
OnUpdate = OnUpdate_RemoveEmptyDynamic,
g = {}
})
-- Dynamic category headers
-- TODO: If possible, change the creation of names and icons to SimpleHeaderGroup to take the localized names
local headers = {
{ id = "achievementID", name = ACHIEVEMENTS, icon = app.asset("Category_Achievements") },
{ id = "sourceID", name = "Appearances", icon = 135276 },
{ id = "artifactID", name = ITEM_QUALITY6_DESC, icon = app.asset("Weapon_Type_Artifact") },
{ id = "azeriteessenceID", name = SPLASH_BATTLEFORAZEROTH_8_2_0_FEATURE2_TITLE, icon = app.asset("Category_AzeriteEssences") },
{ id = "speciesID", name = AUCTION_CATEGORY_BATTLE_PETS, icon = app.asset("Category_PetJournal") },
{ id = "campsiteID", name = WARBAND_SCENES, icon = app.asset("Category_Campsites") },
{ id = "characterUnlock", name = CHARACTER .. " " .. UNLOCK .. "s", icon = app.asset("Category_ItemSets") },
{ id = "conduitID", name = GetSpellName(348869) .. " (" .. EXPANSION_NAME8 .. ")", icon = 3601566 },
{ id = "currencyID", name = CURRENCY, icon = app.asset("Interface_Vendor") },
{ id = "decorID", name = CATALOG_SHOP_TYPE_DECOR, icon = app.asset("Category_Housing") },
{ id = "explorationID", name = "Exploration", icon = app.asset("Category_Exploration") },
{ id = "factionID", name = L.FACTIONS, icon = app.asset("Category_Factions") },
{ id = "flightpathID", name = L.FLIGHT_PATHS, icon = app.asset("Category_FlightPaths") },
{ id = "followerID", name = GARRISON_FOLLOWERS, icon = app.asset("Category_Followers") },
{ id = "heirloomID", name = HEIRLOOMS, icon = app.asset("Weapon_Type_Heirloom") },
{ id = "illusionID", name = L.FILTER_ID_TYPES[103], icon = app.asset("Category_Illusions") },
{ id = "mountID", name = MOUNTS, icon = app.asset("Category_Mounts") },
{ id = "mountmodID", name = "Mount Mods", icon = 975744 },
-- TODO: Add professions here using the byValue probably
{ id = "questID", name = TRACKER_HEADER_QUESTS, icon = app.asset("Interface_Quest_header") },
{ id = "runeforgepowerID", name = LOOT_JOURNAL_LEGENDARIES .. " (" .. EXPANSION_NAME8 .. ")", icon = app.asset("Weapon_Type_Legendary") },
{ id = "titleID", name = PAPERDOLL_SIDEBAR_TITLES, icon = app.asset("Category_Titles") },
{ id = "toyID", name = TOY_BOX, icon = app.asset("Category_ToyBox") },
}
-- Loop through the dynamic headers and insert them into the "g" field of dynamic category
for _, header in ipairs(headers) do
header.parent = dynamicCategory
dynamicCategory.g[#dynamicCategory.g + 1] = app.DelayLoadedObject(CreateTypeGroupsForHeader, "text", TypeGroupOverrides, header, searchResults)
end
-- Merge patchHeaders and searchResults with dynamicCategory
tinsert(patchHeader.g, dynamicCategory)
-- Insert the final merged patchHeader into patchBuild
tinsert(patchBuild, patchHeader)
end
return patchBuild
end
local AWPwindow = app.CreateRawText(L.ADDED_WITH_PATCH, {
icon = app.asset("Interface_Newly_Added"),
description = L.ADDED_WITH_PATCH_TOOLTIP,
visible = true,
back = 1,
g = {
app.CreateExpansion(expansionHeader, {
expanded=true,
g = CreatePatches(param),
}),
},
})
self:SetData(AWPwindow);
self:BuildData();
end
if self:IsVisible() then
self:DefaultUpdate(force);
end
end)
app.AddCustomWindowOnUpdate("Prime", function(self, ...)
self:DefaultUpdate(...);
-- Write the current character's progress if a top-level update has been completed
local rootData = self.data;
if rootData and rootData.TLUG and rootData.total and rootData.total > 0 then
app.CurrentCharacter.PrimeData = {
progress = rootData.progress,
total = rootData.total,
modeString = rootData.modeString,
};
end
end)
app.AddCustomWindowOnUpdate("RaidAssistant", function(self)
if self:IsVisible() then
if not self.initialized then
self.initialized = true;
self.doesOwnUpdate = true;
-- Define the different window configurations that the mini list will switch to based on context.
local raidassistant, lootspecialization, dungeondifficulty, raiddifficulty, legacyraiddifficulty;
local GetDifficultyInfo, GetInstanceInfo = GetDifficultyInfo, GetInstanceInfo;
local GetSpecialization = app.WOWAPI.GetSpecialization
local GetSpecializationInfo = app.WOWAPI.GetSpecializationInfo
-- Raid Assistant
local switchDungeonDifficulty = function(row, button)
self:SetData(raidassistant);
SetDungeonDifficultyID(row.ref.difficultyID);
Callback(self.Update, self);
return true;
end
local switchRaidDifficulty = function(row, button)
self:SetData(raidassistant);
local myself = self;
local difficultyID = row.ref.difficultyID;
if not self.running then
self.running = true;
else
self.running = false;
end
SetRaidDifficultyID(difficultyID);
StartCoroutine("RaidDifficulty", function()
while InCombatLockdown() do coroutine.yield(); end
while myself.running do
for i=0,150,1 do
if myself.running then
coroutine.yield();
else
break;
end
end
if app.RaidDifficulty == difficultyID then
myself.running = false;
break;
else
SetRaidDifficultyID(difficultyID);
end
end
Callback(self.Update, self);
end);
return true;
end
local switchLegacyRaidDifficulty = function(row, button)
self:SetData(raidassistant);
local myself = self;
local difficultyID = row.ref.difficultyID;
if not self.legacyrunning then
self.legacyrunning = true;
else
self.legacyrunning = false;
end
SetLegacyRaidDifficultyID(difficultyID);
StartCoroutine("LegacyRaidDifficulty", function()
while InCombatLockdown() do coroutine.yield(); end
while myself.legacyrunning do
for i=0,150,1 do
if myself.legacyrunning then
coroutine.yield();
else
break;
end
end
if app.LegacyRaidDifficulty == difficultyID then
myself.legacyrunning = false;
break;
else
SetLegacyRaidDifficultyID(difficultyID);
end
end
Callback(self.Update, self);
end);
return true;
end
local function AttemptResetInstances()
ResetInstances();
end
raidassistant = app.CreateRawText(L.RAID_ASSISTANT, {
icon = app.asset("WindowIcon_RaidAssistant"),
description = L.RAID_ASSISTANT_DESC,
visible = true,
back = 1,
g = {
app.CreateRawText(L.LOOT_SPEC_UNKNOWN, {
['title'] = L.LOOT_SPEC,
["description"] = L.LOOT_SPEC_DESC,
['visible'] = true,
['OnClick'] = function(row, button)
self:SetData(lootspecialization);
Callback(self.Update, self);
return true;
end,
['OnUpdate'] = function(data)
if self.Spec then
local id, name, description, icon, role, class = GetSpecializationInfoByID(self.Spec);
if name then
if GetLootSpecialization() == 0 then name = name .. " (Automatic)"; end
data.text = name;
data.icon = icon;
end
end
end,
}),
app.CreateDifficulty(1, {
['title'] = L.DUNGEON_DIFF,
["description"] = L.DUNGEON_DIFF_DESC,
['visible'] = true,
["trackable"] = false,
['OnClick'] = function(row, button)
self:SetData(dungeondifficulty);
Callback(self.Update, self);
return true;
end,
['OnUpdate'] = function(data)
if app.DungeonDifficulty then
data.difficultyID = app.DungeonDifficulty;
data.name = GetDifficultyInfo(data.difficultyID) or "???";
local name, instanceType, instanceDifficulty, difficultyName = GetInstanceInfo();
if instanceDifficulty and data.difficultyID ~= instanceDifficulty and instanceType == 'party' then
data.name = data.name .. " (" .. (difficultyName or "???") .. ")";
end
end
end,
}),
app.CreateDifficulty(14, {
['title'] = L.RAID_DIFF,
["description"] = L.RAID_DIFF_DESC,
['visible'] = true,
["trackable"] = false,
['OnClick'] = function(row, button)
-- Don't allow you to change difficulties when you're in LFR / Raid Finder
if app.RaidDifficulty == 7 or app.RaidDifficulty == 17 then return true; end
self:SetData(raiddifficulty);
Callback(self.Update, self);
return true;
end,
['OnUpdate'] = function(data)
if app.RaidDifficulty then
data.difficultyID = app.RaidDifficulty;
local name, instanceType, instanceDifficulty, difficultyName = GetInstanceInfo();
if instanceDifficulty and data.difficultyID ~= instanceDifficulty and instanceType == 'raid' then
data.name = (GetDifficultyInfo(data.difficultyID) or "???") .. " (" .. (difficultyName or "???") .. ")";
else
data.name = GetDifficultyInfo(data.difficultyID);
end
end
end,
}),
app.CreateDifficulty(5, {
['title'] = L.LEGACY_RAID_DIFF,
["description"] = L.LEGACY_RAID_DIFF_DESC,
['visible'] = true,
["trackable"] = false,
['OnClick'] = function(row, button)
-- Don't allow you to change difficulties when you're in LFR / Raid Finder
if app.RaidDifficulty == 7 or app.RaidDifficulty == 17 then return true; end
self:SetData(legacyraiddifficulty);
Callback(self.Update, self);
return true;
end,
['OnUpdate'] = function(data)
if app.LegacyRaidDifficulty then
data.difficultyID = app.LegacyRaidDifficulty;
end
end,
}),
app.CreateRawText(L.RESET_INSTANCES, {
['icon'] = app.asset("Button_Reset"),
['description'] = L.RESET_INSTANCES_DESC,
['visible'] = true,
['OnClick'] = function(row, button)
-- make sure the indicator icon is allowed to show
if IsAltKeyDown() then
row.ref.saved = not row.ref.saved;
Callback(self.Update, self);
else
ResetInstances();
end
return true;
end,
['OnUpdate'] = function(data)
data.trackable = data.saved;
data.visible = not IsInGroup() or UnitIsGroupLeader("player");
if data.visible and data.saved then
if IsInInstance() or C_Scenario.IsInScenario() then
data.shouldReset = true;
elseif data.shouldReset then
data.shouldReset = nil;
C_Timer.After(0.5, AttemptResetInstances);
end
end
end,
}),
app.CreateRawText(L.TELEPORT_TO_FROM_DUNGEON, {
['icon'] = 136222,
['description'] = L.TELEPORT_TO_FROM_DUNGEON_DESC,
['visible'] = true,
['OnClick'] = function(row, button)
LFGTeleport(IsInLFGDungeon() and true or false);
return true;
end,
['OnUpdate'] = function(data)
data.visible = IsAllowedToUserTeleport();
end,
}),
app.CreateRawText(L.DELIST_GROUP, {
['icon'] = 252175,
['description'] = L.DELIST_GROUP_DESC,
['visible'] = true,
['OnClick'] = function(row, button)
C_LFGList.RemoveListing();
if GroupFinderFrame:IsVisible() then
PVEFrame_ToggleFrame("GroupFinderFrame")
end
self:SetData(raidassistant);
Callback(self.DefaultUpdate, self, true);
return true;
end,
['OnUpdate'] = function(data)
data.visible = C_LFGList.GetActiveEntryInfo();
end,
}),
app.CreateRawText(L.LEAVE_GROUP, {
['icon'] = 132331,
['description'] = L.LEAVE_GROUP_DESC,
['visible'] = true,
['OnClick'] = function(row, button)
C_PartyInfo.LeaveParty();
if GroupFinderFrame:IsVisible() then
PVEFrame_ToggleFrame("GroupFinderFrame")
end
self:SetData(raidassistant);
Callback(self.DefaultUpdate, self, true);
return true;
end,
['OnUpdate'] = function(data)
data.visible = IsInGroup();
end,
}),
}
})
lootspecialization = app.CreateRawText(L.LOOT_SPEC, {
['icon'] = 1499566,
["description"] = L.LOOT_SPEC_DESC_2,
['OnClick'] = function(row, button)
self:SetData(raidassistant);
Callback(self.Update, self);
return true;
end,
['OnUpdate'] = function(data)
data.g = {};
local numSpecializations = GetNumSpecializations();
if numSpecializations and numSpecializations > 0 then
tinsert(data.g, app.CreateRawText(L.CURRENT_SPEC, {
['title'] = select(2, GetSpecializationInfo(GetSpecialization())),
['icon'] = 1495827,
['id'] = 0,
["description"] = L.CURRENT_SPEC_DESC,
['visible'] = true,
['OnClick'] = function(row, button)
self:SetData(raidassistant);
SetLootSpecialization(row.ref.id);
Callback(self.Update, self);
return true;
end,
}))
for i=1,numSpecializations,1 do
local id, name, description, icon, background, role, primaryStat = GetSpecializationInfo(i);
tinsert(data.g, app.CreateRawText(name, {
['icon'] = icon,
['id'] = id,
["description"] = description,
['visible'] = true,
['OnClick'] = function(row, button)
self:SetData(raidassistant);
SetLootSpecialization(row.ref.id);
Callback(self.Update, self);
return true;
end,
}))
end
end
end,
['visible'] = true,
['back'] = 1,
['g'] = {},
})
dungeondifficulty = app.CreateRawText(L.DUNGEON_DIFF, {
['icon'] = 236530,
["description"] = L.DUNGEON_DIFF_DESC_2,
['OnClick'] = function(row, button)
self:SetData(raidassistant);
Callback(self.Update, self);
return true;
end,
['visible'] = true,
["trackable"] = false,
['back'] = 1,
['g'] = {
app.CreateDifficulty(1, {
['OnClick'] = switchDungeonDifficulty,
["description"] = L.CLICK_TO_CHANGE,
['visible'] = true,
["trackable"] = false,
}),
app.CreateDifficulty(2, {
['OnClick'] = switchDungeonDifficulty,
["description"] = L.CLICK_TO_CHANGE,
['visible'] = true,
["trackable"] = false,
}),
app.CreateDifficulty(23, {
['OnClick'] = switchDungeonDifficulty,
["description"] = L.CLICK_TO_CHANGE,
['visible'] = true,
["trackable"] = false,
})
},
})
raiddifficulty = app.CreateRawText(L.RAID_DIFF, {
['icon'] = 236530,
["description"] = L.RAID_DIFF_DESC_2,
['OnClick'] = function(row, button)
self:SetData(raidassistant);
Callback(self.Update, self);
return true;
end,
['visible'] = true,
["trackable"] = false,
['back'] = 1,
['g'] = {
app.CreateDifficulty(14, {
['OnClick'] = switchRaidDifficulty,
["description"] = L.CLICK_TO_CHANGE,
['visible'] = true,
["trackable"] = false,
}),
app.CreateDifficulty(15, {
['OnClick'] = switchRaidDifficulty,
["description"] = L.CLICK_TO_CHANGE,
['visible'] = true,
["trackable"] = false,
}),
app.CreateDifficulty(16, {
['OnClick'] = switchRaidDifficulty,
["description"] = L.CLICK_TO_CHANGE,
['visible'] = true,
["trackable"] = false,
})
},
})
legacyraiddifficulty = app.CreateRawText(L.LEGACY_RAID_DIFF, {
['icon'] = 236530,
["description"] = L.LEGACY_RAID_DIFF_DESC_2,
['OnClick'] = function(row, button)
self:SetData(raidassistant);
Callback(self.Update, self);
return true;
end,
['visible'] = true,
["trackable"] = false,
['back'] = 1,
['g'] = {
app.CreateDifficulty(3, {
['OnClick'] = switchLegacyRaidDifficulty,
["description"] = L.CLICK_TO_CHANGE,
['visible'] = true,
["trackable"] = false,
}),
app.CreateDifficulty(5, {
['OnClick'] = switchLegacyRaidDifficulty,
["description"] = L.CLICK_TO_CHANGE,
['visible'] = true,
["trackable"] = false,
}),
app.CreateDifficulty(4, {
['OnClick'] = switchLegacyRaidDifficulty,
["description"] = L.CLICK_TO_CHANGE,
['visible'] = true,
["trackable"] = false,
}),
app.CreateDifficulty(6, {
['OnClick'] = switchLegacyRaidDifficulty,
["description"] = L.CLICK_TO_CHANGE,
['visible'] = true,
["trackable"] = false,
}),
},
})
self:SetData(raidassistant);
-- Setup Event Handlers and register for events
self:SetScript("OnEvent", function(self, e, ...) DelayedCallback(self.Update, 0.5, self, true); end);
self:RegisterEvent("PLAYER_LOOT_SPEC_UPDATED");
self:RegisterEvent("PLAYER_DIFFICULTY_CHANGED");
self:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED");
self:RegisterEvent("CHAT_MSG_SYSTEM");
self:RegisterEvent("SCENARIO_UPDATE");
self:RegisterEvent("ZONE_CHANGED_NEW_AREA");
self:RegisterEvent("GROUP_ROSTER_UPDATE");
end
-- Update the window and all of its row data
app.LegacyRaidDifficulty = GetLegacyRaidDifficultyID() or 1;
app.DungeonDifficulty = GetDungeonDifficultyID() or 1;
app.RaidDifficulty = GetRaidDifficultyID() or 14;
self.Spec = GetLootSpecialization();
if not self.Spec or self.Spec == 0 then
local spec = GetSpecialization();
if spec then self.Spec = GetSpecializationInfo(spec); end
end
-- 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, self); end
end
-- Update the groups without forcing Debug Mode.
local visibleState = app.Modules.Filter.Get.Visible();
app.Modules.Filter.Set.Visible()
self:BuildData();
self:DefaultUpdate(true);
app.Modules.Filter.Set.Visible(visibleState)
end
end)
app.AddCustomWindowOnUpdate("Random", function(self)
if self:IsVisible() then
if not self.initialized then
self.initialized = true;
local searchCache = {}
local function ClearCache()
wipe(searchCache)
end
-- when changing settings, we need the random cache to be cleared since it's determined based on search
-- results with specific settings
self:AddEventHandler("OnRecalculate_NewSettings", ClearCache)
local function SearchRecursively(group, results, func, field)
if group.visible and not (group.saved or group.collected) then
if group.g then
for i, subgroup in ipairs(group.g) do
SearchRecursively(subgroup, field, results, func);
end
end
if group[field] and (not func or func(group)) then
results[#results + 1] = group
end
end
end
local function SearchRecursivelyForValue(group, results, func, field, value)
if group.visible and not (group.saved or group.collected) then
if group.g then
for i, subgroup in ipairs(group.g) do
SearchRecursivelyForValue(subgroup, field, value, results, func);
end
end
if group[field] and group[field] == value and (not func or func(group)) then
results[#results + 1] = group
end
end
end
local function SearchRecursivelyForEverything(group, results)
if group.visible and not (group.saved or group.collected) then
if group.g then
for i, subgroup in ipairs(group.g) do
SearchRecursivelyForEverything(subgroup, results);
end
end
if group.collectible then
results[#results + 1] = group
end
end
end
local excludedZones = setmetatable({}, {
__index = function(t, mapID)
local info = C_Map_GetMapInfo(mapID);
t[mapID] = not info or info.mapType < 3;
return t[mapID];
end
});
-- Represents how to search for a given named-Thing
local SelectionMethods = setmetatable({
AllTheThings = SearchRecursivelyForEverything,
}, { __index = function() return SearchRecursively end})
-- Named-TypeIDs for the field to Select for a given named-Thing
local TypeIDLookups = {
Achievement = "achievementID",
Campsites = "campsiteID",
Decor = "decorID",
Dungeon = "instanceID",
Factions = "factionID",
Flight_Paths = "flightpathID",
Followers = "followerID",
Item = "itemID",
Instance = "instanceID",
Mount = "mountID",
Pet = "speciesID",
Quest = "questID",
Raid = "instanceID",
Titles = "titleID",
Toy = "toyID",
Zone = "mapID",
}
-- Named-Values for the value of a field in the Select
local TypeIDValueLookups = {
}
local DefaultSelectionFilter = function(o) return o.collectible and not o.collected end
-- Named-Functions (if not ignored) for whether to select data pertaining to a specific named-Thing
local SelectionFilters = setmetatable({
Achievement = function(o)
return o.collectible and not o.collected and not o.mapID and not o.criteriaID;
end,
-- Campsites - default
-- Decor - default
Dungeon = function(o)
return not o.isRaid and (((o.total or 0) - (o.progress or 0)) > 0);
end,
-- Factions - default
-- Flight Paths - default
-- Followers - default
-- Item - default
Instance = function(o)
return ((o.total or 0) - (o.progress or 0)) > 0;
end,
-- Mount - default
-- Pet - default
-- Quest - default
Raid = function(o)
return o.isRaid and (((o.total or 0) - (o.progress or 0)) > 0);
end,
-- Titles - default
-- Toy - default
Zone = function(o)
return (((o.total or 0) - (o.progress or 0)) > 0) and not o.instanceID and not excludedZones[o.mapID];
end,
}, { __index = function() return DefaultSelectionFilter end})
local function GetSearchResults(rootData, name)
if searchCache[name] then return searchCache[name] end
local searchResults = {}
SelectionMethods[name](rootData, searchResults, SelectionFilters[name], TypeIDLookups[name], TypeIDValueLookups[name])
if #searchResults > 0 then
searchCache[name] = searchResults
return searchResults
end
end
local mainHeader
local function AddRandomCategoryButton(text, icon, desc, name)
return app.CreateRawText(text, {
icon = icon,
description = desc,
visible = true,
OnUpdate = app.AlwaysShowUpdate,
OnClick = function(row, button)
self.RandomSearchFilter = name
self:SetData(mainHeader)
self:Reroll()
return true
end,
})
end
local rerollOption = app.CreateRawText(L.REROLL, {
['icon'] = app.asset("Button_Reroll"),
['description'] = L.REROLL_DESC,
['visible'] = true,
['OnClick'] = function(row, button)
self:Reroll();
return true;
end,
['OnUpdate'] = app.AlwaysShowUpdate,
})
local filterHeader = app.CreateRawText(L.APPLY_SEARCH_FILTER, {
['icon'] = app.asset("Button_Search"),
["description"] = L.APPLY_SEARCH_FILTER_DESC,
['visible'] = true,
['OnUpdate'] = app.AlwaysShowUpdate,
["indent"] = 0,
['back'] = 1,
['g'] = {
app.CreateRawText(L.TITLE, {
icon = app.asset("logo_32x32"),
preview = app.asset("Discord_2_128"),
description = L.SEARCH_EVERYTHING_BUTTON_OF_DOOM,
visible = true,
OnClick = function(row, button)
self.RandomSearchFilter = appName;
self:SetData(mainHeader);
self:Reroll();
return true;
end,
OnUpdate = app.AlwaysShowUpdate,
}),
AddRandomCategoryButton(L.ACHIEVEMENT, app.asset("Category_Achievements"), L.ACHIEVEMENT_DESC, "Achievement"),
AddRandomCategoryButton(L.CAMPSITES, app.asset("Category_Campsites"), L.CAMPSITE_DESC, "Campsites"),
AddRandomCategoryButton(L.DECOR, app.asset("Category_Housing"), L.DECOR_DESC, "Decor"),
AddRandomCategoryButton(L.DUNGEON, app.asset("Difficulty_Normal"), L.DUNGEON_DESC, "Dungeon"),
AddRandomCategoryButton(L.FACTIONS, app.asset("Category_Factions"), L.FACTION_DESC, "Factions"),
AddRandomCategoryButton(L.FLIGHT_PATHS, app.asset("Category_FlightPaths"), L.FLIGHT_PATH_DESC, "Flight_Paths"),
AddRandomCategoryButton(L.FOLLOWERS, app.asset("Category_Followers"), L.FOLLOWER_DESC, "Followers"),
AddRandomCategoryButton(L.INSTANCE, app.asset("Category_D&R"), L.INSTANCE_DESC, "Instance"),
AddRandomCategoryButton(L.ITEM, app.asset("Interface_Zone_drop"), L.ITEM_DESC, "Item"),
AddRandomCategoryButton(L.MOUNT, app.asset("Category_Mounts"), L.MOUNT_DESC, "Mount"),
AddRandomCategoryButton(L.PET, app.asset("Category_PetBattles"), L.PET_DESC, "Pet"),
AddRandomCategoryButton(L.QUEST, app.asset("Interface_Quest"), L.QUEST_DESC, "Quest"),
AddRandomCategoryButton(L.RAID, app.asset("Difficulty_Heroic"), L.RAID_DESC, "Raid"),
AddRandomCategoryButton(L.TITLES, app.asset("Category_Titles"), L.TITLES_RAND_DESC, "Titles"),
AddRandomCategoryButton(L.TOY, app.asset("Category_ToyBox"), L.TOY_DESC, "Toy"),
AddRandomCategoryButton(L.ZONE, app.asset("Category_Zones"), L.ZONE_DESC, "Zone"),
},
})
mainHeader = app.CreateRawText(L.GO_GO_RANDOM, {
['icon'] = app.asset("WindowIcon_Random"),
["description"] = L.GO_GO_RANDOM_DESC,
['visible'] = true,
['OnUpdate'] = app.AlwaysShowUpdate,
['back'] = 1,
["indent"] = 0,
['options'] = {
app.CreateRawText(L.CHANGE_SEARCH_FILTER, {
['icon'] = app.asset("Button_Search"),
["description"] = L.CHANGE_SEARCH_FILTER_DESC,
['visible'] = true,
['OnClick'] = function(row, button)
self:SetData(filterHeader);
self:Update(true);
return true;
end,
['OnUpdate'] = app.AlwaysShowUpdate,
}),
rerollOption,
},
['g'] = { },
})
self:SetData(mainHeader);
self.Rebuild = function(self, no)
-- Rebuild all the datas
wipe(self.data.g);
local primeWindow = app:GetWindow("Prime")
local primePending = primeWindow.HasPendingUpdate
-- Call to our method and build a list to draw from if Prime has been opened
if not primePending then
local method = self.RandomSearchFilter or appName;
rerollOption.text = L.REROLL_2 .. (method ~= appName and L[method:upper()] or method);
local temp = GetSearchResults(primeWindow.data, method) or app.EmptyTable;
local totalWeight = 0;
for i,o in ipairs(temp) do
totalWeight = totalWeight + ((o.total or 1) - (o.progress or 0));
end
-- app.PrintDebug("#random",temp and #temp,totalWeight)
if totalWeight > 0 and #temp > 0 then
local weight, selected = math.random(totalWeight), nil;
totalWeight = 0;
for i,o in ipairs(temp) do
totalWeight = totalWeight + ((o.total or 1) - (o.progress or 0));
if weight <= totalWeight then
selected = o;
break;
end
end
-- app.PrintDebug("select",weight,selected and (selected.text or selected.hash))
if not selected then selected = temp[#temp - 1]; end
if selected then
NestObject(self.data, selected, true);
else
app.print(L.NOTHING_TO_SELECT_FROM);
end
else
app.print(L.NOTHING_TO_SELECT_FROM);
end
else
rerollOption.text = "Please open /att"
app.print(L.NOTHING_TO_SELECT_FROM);
end
for i=#self.data.options,1,-1 do
tinsert(self.data.g, 1, self.data.options[i]);
end
self:AssignChildren();
if not no then self:Update(); end
end
self.Reroll = function(self)
Push(self, "Rebuild", self.Rebuild);
end
for i,o in ipairs(self.data.options) do
tinsert(self.data.g, o);
end
local method = self.RandomSearchFilter or appName;
rerollOption.text = L.REROLL_2 .. (method ~= appName and L[method:upper()] or method);
end
-- Update the window and all of its row data
self.data.progress = 0;
self.data.total = 0;
self.data.indent = 0;
self:AssignChildren();
self:DefaultUpdate(true);
end
end)
app.AddCustomWindowOnUpdate("RWP", function(self, force)
if not self.initialized then
if not app:GetDataCache() then -- This module requires a valid data cache to function correctly.
return;
end
self.initialized = true;
self:SetData(app.CreateRawText(L.FUTURE_UNOBTAINABLE, {
["icon"] = app.asset("Interface_Future_Unobtainable"),
["description"] = L.FUTURE_UNOBTAINABLE_TOOLTIP,
["visible"] = true,
["back"] = 1,
["g"] = app:BuildSearchResponse("rwp"),
}))
self:BuildData();
self.ExpandInfo = { Expand = true, Manual = true };
end
if self:IsVisible() then
self:DefaultUpdate(force);
end
end)
app.AddCustomWindowOnUpdate("Import", function(self, force)
if not self:IsVisible() then return end
if not self.initialized then
self.initialized = true
local SearchForObject = app.SearchForObject
function self:Rebuild()
self:BuildData()
self:Update(true)
end
function self:ClearResults()
local fixed = {}
for _, row in ipairs(self.data.g) do
if row.isButton then
fixed[#fixed + 1] = row
end
end
wipe(self.data.g)
ArrayAppend(self.data.g, fixed)
end
local function ParseIDs(str)
local ids = {}
for id in str:gmatch("%d+") do
id = tonumber(id)
if id then
ids[#ids + 1] = id
end
end
return ids
end
local function SearchTypeObject(typeKey, id)
local o = setmetatable({ OnUpdate = app.ForceShowUpdate }, {
__index = id and (SearchForObject(typeKey, id, "key")
or SearchForObject(typeKey, id, "field")
or CreateObject({[typeKey]=id}))
or setmetatable({name=EMPTY}, app.BaseClass)
})
-- app.PrintDebug("Created", typeKey, id, "->", o.name or "???")
-- app.PrintTable(o)
return o
end
function self:Import(typeKey, input)
local ids = ParseIDs(input)
self:ClearResults()
if not ids then return false end
-- app.PrintDebug("Importing", #ids, typeKey)
local objs = {}
for _, id in ipairs(ids) do
objs[#objs + 1] = SearchTypeObject(typeKey, id)
end
-- Merge all the search results into the list, ensure to clone
NestObjects(self.data, objs, true)
end
local initialButtons = {
{ id = "achievementID", name = ACHIEVEMENTS, icon = app.asset("Category_Achievements") },
{ id = "sourceID", name = WARDROBE, icon = 135276 },
{ id = "artifactID", name = ITEM_QUALITY6_DESC, icon = app.asset("Weapon_Type_Artifact") },
{ id = "azeriteessenceID", name = SPLASH_BATTLEFORAZEROTH_8_2_0_FEATURE2_TITLE, icon = app.asset("Category_AzeriteEssences") },
{ id = "speciesID", name = AUCTION_CATEGORY_BATTLE_PETS, icon = app.asset("Category_PetJournal") },
{ id = "campsiteID", name = WARBAND_SCENES, icon = app.asset("Category_Campsites") },
{ id = "currencyID", name = CURRENCY, icon = app.asset("Interface_Vendor") },
{ id = "decorID", name = CATALOG_SHOP_TYPE_DECOR, icon = app.asset("Category_Housing") },
{ id = "explorationID", name = "Exploration", icon = app.asset("Category_Exploration") },
{ id = "factionID", name = L.FACTIONS, icon = app.asset("Category_Factions") },
{ id = "flightpathID", name = L.FLIGHT_PATHS, icon = app.asset("Category_FlightPaths") },
{ id = "followerID", name = GARRISON_FOLLOWERS, icon = app.asset("Category_Followers") },
{ id = "illusionID", name = L.FILTER_ID_TYPES[103], icon = app.asset("Category_Illusions") },
{ id = "itemID", name = ITEMS, icon = 135276 },
{ id = "questID", name = TRACKER_HEADER_QUESTS, icon = app.asset("Interface_Quest_header") },
{ id = "titleID", name = PAPERDOLL_SIDEBAR_TITLES, icon = app.asset("Category_Titles") },
}
local function ImportButton(typeKey, label, icon)
return app.CreateRawText(label, {
icon = icon,
visible = true,
isButton = true,
OnUpdate = app.AlwaysShowUpdate,
OnClick = function()
app:ShowPopupDialogWithEditBox(
"Paste " .. label .. " IDs",
"",
function(input)
if not input or input:match("^%s*$") then
return
end
self:Import(typeKey, input)
self:ShowResetButton()
self:Rebuild()
end
)
return true
end,
})
end
function self:ResetToInitialButtons()
wipe(self.data.g)
for _, b in ipairs(initialButtons) do
tinsert(self.data.g, ImportButton(b.id, b.name, b.icon))
end
self:Rebuild()
end
function self:ShowResetButton()
local importedRows = {}
for _, row in ipairs(self.data.g) do
if not row.isButton then
tinsert(importedRows, row)
end
end
wipe(self.data.g)
local resetButton = app.CreateRawText("Reset Import", {
icon = app.asset("unknown"),
visible = true,
isButton = true,
OnUpdate = app.AlwaysShowUpdate,
OnClick = function()
self:ResetToInitialButtons()
return true
end,
})
tinsert(self.data.g, resetButton)
ArrayAppend(self.data.g, importedRows)
self:Rebuild()
end
self:SetData(app.CreateRawText("Import", {
icon = app.asset("logo_32x32"),
description = "Import objects using their IDs, separated by commas.",
visible = true,
back = 1,
g = {}
}))
self:ResetToInitialButtons()
end
self:DefaultUpdate(force)
end)
app.AddCustomWindowOnUpdate("Sync", function(self)
if self:IsVisible() then
if not self.initialized then
self.initialized = true;
local function OnRightButtonDeleteCharacter(row, button)
if button == "RightButton" then
app:ShowPopupDialog("CHARACTER DATA: " .. (row.ref.text or RETRIEVING_DATA) .. L.CONFIRM_DELETE,
function()
ATTCharacterData[row.ref.datalink] = nil;
app:RecalculateAccountWideData();
self:Reset();
end);
end
return true;
end
local function OnRightButtonDeleteLinkedAccount(row, button)
if button == "RightButton" then
app:ShowPopupDialog("LINKED ACCOUNT: " .. (row.ref.text or RETRIEVING_DATA) .. L.CONFIRM_DELETE,
function()
AllTheThingsAD.LinkedAccounts[row.ref.datalink] = nil;
app:SynchronizeWithPlayer(row.ref.datalink);
self:Reset();
end);
end
return true;
end
local function OnTooltipForCharacter(t, tooltipInfo)
local character = ATTCharacterData[t.unit];
if character then
-- last login info
local login = character.lastPlayed;
if login then
local d = C_DateAndTime.GetCalendarTimeFromEpoch(login * 1e6);
tinsert(tooltipInfo, {
left = PLAYED,
right = ("%d-%02d-%02d %02d:%02d"):format(d.year, d.month, d.monthDay, d.hour, d.minute),
r = 0.8, g = 0.8, b = 0.8
});
else
tinsert(tooltipInfo, {
left = PLAYED,
right = NEVER,
r = 0.8, g = 0.8, b = 0.8
});
end
local total = 0;
for i,field in ipairs(app.CharacterSyncTables) do
local values = character[field];
if values then
local subtotal = 0;
for key,value in pairs(values) do
if value then
subtotal = subtotal + 1;
end
end
total = total + subtotal;
tinsert(tooltipInfo, {
left = field,
right = tostring(subtotal),
r = 1, g = 1, b = 1
});
end
end
tinsert(tooltipInfo, { left = " " });
tinsert(tooltipInfo, {
left = "Total",
right = tostring(total),
r = 0.8, g = 0.8, b = 1
});
tinsert(tooltipInfo, {
left = L.DELETE_CHARACTER,
r = 1, g = 0.8, b = 0.8
});
end
end
local function OnTooltipForLinkedAccount(t, tooltipInfo)
if t.unit then
tinsert(tooltipInfo, {
left = L.LINKED_ACCOUNT_TOOLTIP,
r = 0.8, g = 0.8, b = 1, wrap = true,
});
tinsert(tooltipInfo, {
left = L.DELETE_LINKED_CHARACTER,
r = 1, g = 0.8, b = 0.8
});
else
tinsert(tooltipInfo, {
left = L.DELETE_LINKED_ACCOUNT,
r = 1, g = 0.8, b = 0.8
});
end
end
local syncHeader = app.CreateRawText(L.ACCOUNT_MANAGEMENT, {
icon = app.asset("WindowIcon_AccountManagement"),
description = L.ACCOUNT_MANAGEMENT_TOOLTIP,
visible = true,
back = 1,
OnUpdate = app.AlwaysShowUpdate,
OnClick = app.UI.OnClick.IgnoreRightClick,
g = {
app.CreateRawText(L.ADD_LINKED_CHARACTER_ACCOUNT, {
icon = app.asset("Button_Add"),
description = L.ADD_LINKED_CHARACTER_ACCOUNT_TOOLTIP,
visible = true,
OnUpdate = app.AlwaysShowUpdate,
OnClick = function(row, button)
app:ShowPopupDialogWithEditBox(L.ADD_LINKED_POPUP, "", function(cmd)
if cmd and cmd ~= "" then
AllTheThingsAD.LinkedAccounts[cmd] = true;
self:Reset();
end
end);
return true;
end,
}),
-- Characters Section
app.CreateRawText(L.CHARACTERS, {
icon = 526421,
description = L.SYNC_CHARACTERS_TOOLTIP,
visible = true,
expanded = true,
['g'] = {},
OnClick = app.UI.OnClick.IgnoreRightClick,
OnUpdate = function(data)
local g = {};
for guid,character in pairs(ATTCharacterData) do
if character then
tinsert(g, app.CreateUnit(guid, {
datalink = guid,
OnClick = OnRightButtonDeleteCharacter,
OnTooltip = OnTooltipForCharacter,
OnUpdate = app.AlwaysShowUpdate,
name = character.name,
lvl = character.lvl,
visible = true,
}));
end
end
if #g < 1 then
tinsert(g, app.CreateRawText(L.NO_CHARACTERS_FOUND, {
icon = 526421,
visible = true,
OnClick = app.UI.OnClick.IgnoreRightClick,
OnUpdate = app.AlwaysShowUpdate,
}));
else
data.SortType = "textAndLvl";
end
data.g = g;
AssignChildren(data);
return true;
end,
}),
-- Linked Accounts Section
app.CreateRawText(L.LINKED_ACCOUNTS, {
icon = 526421,
description = L.LINKED_ACCOUNTS_TOOLTIP,
visible = true,
['g'] = {},
OnClick = app.UI.OnClick.IgnoreRightClick,
OnUpdate = function(data)
data.g = {};
local charactersByName = {};
for guid,character in pairs(ATTCharacterData) do
if character.name then
charactersByName[character.name] = character;
end
end
for playerName,allowed in pairs(AllTheThingsAD.LinkedAccounts) do
local character = charactersByName[playerName];
if character then
tinsert(data.g, app.CreateUnit(playerName, {
datalink = playerName,
OnClick = OnRightButtonDeleteLinkedAccount,
OnTooltip = OnTooltipForLinkedAccount,
OnUpdate = app.AlwaysShowUpdate,
visible = true,
}));
elseif playerName:find("#") then
-- Garbage click handler for unsync'd account data.
tinsert(data.g, app.CreateRawText(playerName, {
datalink = playerName,
icon = 526421,
OnClick = OnRightButtonDeleteLinkedAccount,
OnTooltip = OnTooltipForLinkedAccount,
OnUpdate = app.AlwaysShowUpdate,
visible = true,
}));
else
-- Garbage click handler for unsync'd character data.
tinsert(data.g, app.CreateRawText(playerName, {
datalink = playerName,
icon = 374212,
OnClick = OnRightButtonDeleteLinkedAccount,
OnTooltip = OnTooltipForLinkedAccount,
OnUpdate = app.AlwaysShowUpdate,
visible = true,
}));
end
end
if #data.g < 1 then
tinsert(data.g, app.CreateRawText(L.NO_LINKED_ACCOUNTS, {
icon = 526421,
visible = true,
OnClick = app.UI.OnClick.IgnoreRightClick,
OnUpdate = app.AlwaysShowUpdate,
}));
end
AssignChildren(data);
return true;
end,
}),
}
});
self.Reset = function()
self:SetData(syncHeader);
self:Update(true);
end
self:Reset();
end
-- Update the groups without forcing Debug Mode.
if self.data.OnUpdate then self.data.OnUpdate(self.data, self); end
self:BuildData();
for i,g in ipairs(self.data.g) do
if g.OnUpdate then g.OnUpdate(g, self); end
end
self:DefaultUpdate(true);
end
end)
-- Returns an Object based on a QuestID a lot of Quest information for displaying in a row
---@return table?
local function GetPopulatedQuestObject(questID)
-- cannot do anything on a missing object or questID
if not questID then return; end
-- either want to duplicate the existing data for this quest, or create new data for a missing quest
local questObject = CreateObject(SearchForObject("questID", questID, "field") or { questID = questID, _missing = true }, true);
-- if questID == 78663 then
-- local debug = app.Debugging
-- app.Debugging = true
-- app.PrintTable(questObject)
-- app.Debugging = debug
-- end
-- Try populating quest rewards
app.TryPopulateQuestRewards(questObject);
return questObject;
end
app.AddCustomWindowOnUpdate("list", function(self, force, got)
if not self.initialized then
self.VerifyGroupSourceID = function(data)
-- can only determine a sourceID if there is an itemID/sourceID on the group
if not data.itemID and not data.sourceID then return true end
if not data._VerifyGroupSourceID then data._VerifyGroupSourceID = 0 end
if data._VerifyGroupSourceID > 5 then
-- app.PrintDebug("Cannot Harvest: No Item Info",
-- app:SearchLink(SearchForObject("itemID",data.modItemID,"field") or SearchForObject("sourceID",data.sourceID,"field")),
-- data._VerifyGroupSourceID)
return true
end
data._VerifyGroupSourceID = data._VerifyGroupSourceID + 1
local link, source = data.link or data.silentLink, data.sourceID;
if not link then return; end
-- If it doesn't, the source ID will need to be harvested.
local sourceID = app.GetSourceID(link);
-- app.PrintDebug("SourceIDs",data.modItemID,source,app.GetSourceID(link),link)
if sourceID and sourceID > 0 then
-- only save the source if it is different than what we already have, or being forced
if not source or source < 1 or source ~= sourceID then
-- print(GetItemInfo(text))
if not source then
-- app.print("SourceID Update",link,data.modItemID,source,"=>",sourceID);
data.sourceID = sourceID
app.SaveHarvestSource(data)
else
app.print("SourceID Diff!",link,source,"=>",sourceID)
-- replace the item information of the root Item (this affects the Main list)
-- since the inherent item information does not match the SourceID any longer
local mt = getmetatable(data)
if mt then
local rootData = mt.__index
if rootData then
rootData.rawlink = nil
rootData.itemID = nil
rootData.modItemID = nil
end
end
end
end
end
return true
end
self.RemoveSelf = function(o)
local parent = rawget(o, "parent");
if not parent then
app.PrintDebug("no parent?",o.text)
return;
end
local og = parent.g;
if not og then
app.PrintDebug("no g?",parent.text)
return;
end
local i = indexOf(og, o) or (o.__dlo and indexOf(og, o.__dlo));
if i and i > 0 then
-- app.PrintDebug("RemoveSelf",#og,i,o.text)
tremove(og, i);
-- app.PrintDebug("RemoveSelf",#og)
end
return og;
end
self.AutoHarvestFirstPartitionCoroutine = function()
-- app.PrintDebug("AutoExpandingPartitions")
local i = 10;
-- yield a few frames to allow the list to fully generate
while i > 0 do
coroutine.yield();
i = i - 1;
end
local partitions = self.data.g;
if not partitions then return; end
local part;
-- app.PrintDebug("AutoExpandingPartitions",#partitions)
while #partitions > 0 do
part = partitions[1];
if not part.expanded then
part.expanded = true;
-- app.PrintDebug("AutoExpand",part.text)
app.DirectGroupRefresh(part);
end
coroutine.yield();
-- Make sure the coroutine stops running if we close the list window
if not self:IsVisible() then return; end
end
end
-- temporarily prevent a force refresh from exploding the game if this window is open
self.doesOwnUpdate = true;
self.initialized = true;
force = true;
local DGU, DGR = app.DirectGroupUpdate, app.DirectGroupRefresh;
-- custom params for initialization
local dataType = (app.GetCustomWindowParam("list", "type") or "quest");
local onlyMissing = app.GetCustomWindowParam("list", "missing");
local onlyCached = app.GetCustomWindowParam("list", "cached");
local onlyCollected = app.GetCustomWindowParam("list", "collected");
local harvesting = app.GetCustomWindowParam("list", "harvesting");
self.PartitionSize = tonumber(app.GetCustomWindowParam("list", "part")) or 1000;
self.Limit = tonumber(app.GetCustomWindowParam("list", "limit")) or 1000;
local min = tonumber(app.GetCustomWindowParam("list", "min")) or 0
-- print("Quests - onlyMissing",onlyMissing)
local CacheFields, ItemHarvester;
-- manual type adjustments to match internal use (due to lowercase keys with non-lowercase cache keys >_<)
if dataType == "s" or dataType == "source" then
dataType = "source";
elseif dataType == "achievementcategory" then
dataType = "achievementCategory";
elseif dataType == "azeriteessence" then
dataType = "azeriteEssence";
elseif dataType == "flightpath" then
dataType = "flightPath";
elseif dataType == "runeforgepower" then
dataType = "runeforgePower";
elseif dataType == "itemharvester" then
if not app.CreateItemHarvester then
app.print("'itemharvester' Requires 'Debugging' enabled when loading the game!")
return
end
ItemHarvester = app.CreateItemHarvester;
elseif dataType:find("cache") then
-- special data type to utilize an ATT cache instead of generating raw groups
-- "cache:item"
-- => itemID
-- fill all items from itemID cache into list, sorted by itemID
local added = {};
CacheFields = {};
local cacheID;
local _, cacheKey = (":"):split(dataType);
local cacheKeyID = cacheKey.."ID";
local imin, imax = 0, 999999
-- convert the list min/max into cache-based min/max for cache lists
if self.Limit ~= 1000 then
imax = self.Limit + 1;
self.Limit = 999999
end
if min ~= 0 then
imin = min;
min = 0;
end
dataType = cacheKey;
-- collect valid id values
for id,groups in pairs(app.GetRawFieldContainer(cacheKey) or app.GetRawFieldContainer(cacheKeyID) or app.EmptyTable) do
for index,o in ipairs(groups) do
cacheID = tonumber(o.modItemID or o[dataType] or o[cacheKeyID]) or 0;
if imin <= cacheID and cacheID <= imax then
added[cacheID] = true;
-- app.PrintDebug("CacheID",cacheID,"from cache",id,"@",index,#groups)
-- app.PrintDebug(o.modItemID,o[dataType],o[cacheKeyID])
-- else app.PrintDebug("Ignored Data for Harvest due to CacheID Bounds",cacheID,app:SearchLink(o))
end
end
end
for id,_ in pairs(added) do
CacheFields[#CacheFields + 1] = id
end
app.Sort(CacheFields, app.SortDefaults.Values);
app.PrintDebug(#CacheFields,"CacheFields:Sorted",CacheFields[1],"->",CacheFields[#CacheFields])
end
-- add the ID
dataType = dataType.."ID";
local ForceVisibleFields = {
visible = true,
total = 0,
progress = 0,
costTotal = 0,
upgradeTotal = 0,
};
local PartitionUpdateFields = {
total = true,
progress = true,
parent = true,
expanded = true,
window = true
};
local PartitionMeta = {
__index = ForceVisibleFields,
__newindex = function(t, key, val)
-- only allow changing existing table fields
if PartitionUpdateFields[key] then
rawset(t, key, val);
-- app.PrintDebug("__newindex:part",key,val)
end
end
};
local ObjectTypeFuncs = {
questID = GetPopulatedQuestObject,
};
if CacheFields then
-- app.PrintDebug("OTF:Define",dataType)
ObjectTypeFuncs[dataType] = function(id)
-- use the cached id in the slot of the requested id instead
-- app.PrintDebug("OTF",id)
id = CacheFields[id];
-- app.PrintDebug("OTF:CacheID",dataType,id)
return setmetatable({ visible = true }, {
__index = id and (SearchForObject(dataType, id, "key")
or SearchForObject(dataType, id, "field")
or CreateObject({[dataType]=id}))
or setmetatable({name=EMPTY}, app.BaseClass)
});
end
-- app.PrintDebug("SetLimit",#CacheFields)
self.Limit = #CacheFields;
end
if ItemHarvester then
ObjectTypeFuncs[dataType] = ItemHarvester;
end
local function CreateTypeObject(type, id)
-- app.PrintDebug("DLO-Obj:",type,id)
local func = ObjectTypeFuncs[type];
if func then return func(id); end
-- Simply a visible table whose Base will be the actual referenced object
return setmetatable({ visible = true }, {
__index = SearchForObject(dataType, id, "key")
or SearchForObject(type, id, "field")
or CreateObject({[type]=id})
});
end
-- info about the Window
local g = {};
self:SetData(setmetatable({
text = "Full Data List - "..(dataType or "None"),
icon = app.asset("Interface_Quest_header"),
description = "1 - "..self.Limit,
g = g,
}, PartitionMeta));
local overrides = {
visible = not harvesting and true or nil,
indent = 2,
collectibleAsCost = false,
costCollectibles = false,
g = false,
back = function(o, key)
return o._missing and 1 or 0;
end,
text = harvesting and function(o, key)
local text = o.text;
if not IsRetrieving(text) then
DGR(o);
if not self.VerifyGroupSourceID(o) then
return "Harvesting..."
end
local og = self.RemoveSelf(o);
-- app.PrintDebug(#og,"-",text)
if #og <= 0 then
self.RemoveSelf(o.parent);
else
o.visible = true;
end
return text;
end
end
or function(o, key)
local text = o.text;
if not IsRetrieving(text) then
-- if not self.VerifyGroupSourceID(o) then
-- DGR(o);
-- return "Harvesting..."
-- end
return "#"..(o[dataType] or o.keyval or "?")..": "..text;
end
end,
OnLoad = function(o)
-- app.PrintDebug("DGU-OnLoad:",o.hash)
DGU(o);
end,
};
if onlyMissing then
app.SetDGUDelay(0);
if onlyCached then
overrides.visible = function(o, key)
if o._missing then
local text = o.text;
-- app.PrintDebug("check",text)
return IsRetrieving(text) or
(not text:find("#") and text ~= UNKNOWN and not text:find("transmogappearance:"));
end
end
else
overrides.visible = function(o, key)
return o._missing;
end
end
end
if onlyCollected then
app.SetDGUDelay(0);
if onlyMissing then
overrides.visible = function(o, key)
if o._missing and o.collected then
return o.collected;
end
end
else
overrides.visible = function(o, key)
return o.collected;
end
end
end
if harvesting then
app.SetDGUDelay(0);
StartCoroutine("AutoHarvestFirstPartitionCoroutine", self.AutoHarvestFirstPartitionCoroutine);
end
-- add a bunch of raw, delay-loaded objects in order into the window
local groupCount = math_floor(self.Limit / self.PartitionSize);
local groupStart = math_floor(min / self.PartitionSize);
local partition, partitionStart, partitionGroups;
local dlo = app.DelayLoadedObject;
for j=groupStart,groupCount,1 do
partitionStart = j * self.PartitionSize;
partitionGroups = {};
-- define a sub-group for a range of things
partition = setmetatable({
text = tostring(partitionStart + 1).."+",
icon = app.asset("Interface_Quest_header"),
g = partitionGroups,
}, PartitionMeta);
for i=1,self.PartitionSize,1 do
tinsert(partitionGroups, dlo(CreateTypeObject, "text", overrides, dataType, partitionStart + i));
end
tinsert(g, partition);
end
self:BuildData();
end
if self:IsVisible() then
-- requires Visibility filter to check .visibile for display of the group
local filterVisible = app.Modules.Filter.Get.Visible();
app.Modules.Filter.Set.Visible(true);
self:DefaultUpdate(force);
app.Modules.Filter.Set.Visible(filterVisible);
end
end)
app.AddCustomWindowOnUpdate("Tradeskills", function(self, force, got)
if not app:GetDataCache() then -- This module requires a valid data cache to function correctly.
return;
end
if not self.initialized then
self.initialized = true;
self.SkillsInit = {};
self.force = true;
self:SetMovable(false);
self:SetUserPlaced(false);
self:SetClampedToScreen(false);
self:RegisterEvent("TRADE_SKILL_SHOW");
self:RegisterEvent("TRADE_SKILL_LIST_UPDATE");
self:RegisterEvent("TRADE_SKILL_CLOSE");
self:RegisterEvent("GARRISON_TRADESKILL_NPC_CLOSED");
self:SetData(app.CreateRawText(L.PROFESSION_LIST, {
icon = 134940,
description = L.PROFESSION_LIST_DESC,
visible = true,
indent = 0,
back = 1,
g = { },
}))
local MissingRecipes = {}
-- Adds the pertinent information about a given recipeID to the reagentcache
local function CacheRecipeSchematic(recipeID)
local schematic = C_TradeSkillUI_GetRecipeSchematic(recipeID, false);
local craftedItemID = schematic.outputItemID;
if not craftedItemID then return end
local cachedRecipe = SearchForObject("recipeID",recipeID,"key")
local recipeInfo = C_TradeSkillUI_GetRecipeInfo(recipeID)
if not cachedRecipe then
local tradeSkillID, skillLineName, parentTradeSkillID = C_TradeSkillUI_GetTradeSkillLineForRecipe(recipeID)
local missing = app.TableConcat({"Missing Recipe:",recipeID,skillLineName,tradeSkillID,"=>",parentTradeSkillID}, nil, nil, " ")
-- app.PrintDebug(missing)
MissingRecipes[#MissingRecipes + 1] = missing
elseif cachedRecipe.u == app.PhaseConstants.NEVER_IMPLEMENTED then
-- learned NYI recipe?
if recipeInfo and recipeInfo.learned then
-- known NYI recipes
app.PrintDebug("Learned NYI Recipe",app:SearchLink(cachedRecipe))
else
-- don't cache reagents for unknown NYI recipes
-- app.PrintDebug("Skip NYI Recipe",app:SearchLink(cachedRecipe))
return
end
end
local reagentCache = app.ReagentsDB
local itemRecipes, reagentCount, reagentItemID;
-- handle other types of recipes maybe
if recipeInfo then
if recipeInfo.craftable then
-- Salvage Recipe harvest
if recipeInfo.isSalvageRecipe then
-- craftedItemID from salvage...
-- in some cases this is the 'actual' ouput of the salvage (TWW Cooking)
-- but in many other cases this is a 'fake item' representing 'multiple possible item outputs'
-- theoretically we could list this 'fake item' under Profession > Crafted > with all possible outputs
-- to allow driving crafting chains
-- Not really a great way to utilize this output currently, since typically the input drives the output through
-- the same Recipe, and it can be variable depending on skill or reagent qualities
-- local salvageItems = C_TradeSkillUI_GetSalvagableItemIDs(recipeID)
-- for _,salvageItemID in ipairs(salvageItems) do
-- reagentItemID = salvageItemID
-- -- only requirement is Reagent -> Recipe -> Crafted | Reagent Count
-- -- Minimum Structure
-- -- reagentCache[reagentItemID][<recipeID>] = { craftedItemID, reagentCount }
-- if reagentItemID then
-- itemRecipes = reagentCache[reagentItemID];
-- if not itemRecipes then
-- itemRecipes = { };
-- reagentCache[reagentItemID] = itemRecipes;
-- end
-- -- app.PrintDebug("Reagent",reagentItemID,"x 5 =>",craftedItemID,"via",app:SearchLink(cachedRecipe))
-- -- Salvage recipes are always '5' per
-- itemRecipes[recipeID] = { craftedItemID, 5 };
-- end
-- end
return
end
end
end
-- app.PrintDebug("Recipe",recipeID,"==>",craftedItemID)
-- Recipes now have Slots for available Regeants...
if #schematic.reagentSlotSchematics == 0 and schematic.hasCraftingOperationInfo then
-- Milling Recipes...
app.PrintDebug("EMPTY SCHEMATICS",app:SearchLink(cachedRecipe or CreateObject({recipeID=recipeID})))
return;
end
-- Typical Recipe harvest
for _,reagentSlot in ipairs(schematic.reagentSlotSchematics) do
-- reagentType: 0 = sparks?, 1 = required, 2 = optional
if reagentSlot.required then
reagentCount = reagentSlot.quantityRequired;
-- Each available Reagent for the Slot can be associated to the Recipe/Output Item
for _,reagentSlotSchematic in ipairs(reagentSlot.reagents) do
reagentItemID = reagentSlotSchematic.itemID;
-- only requirement is Reagent -> Recipe -> Crafted | Reagent Count
-- Minimum Structure
-- reagentCache[reagentItemID][<recipeID>] = { craftedItemID, reagentCount }
if reagentItemID then
itemRecipes = reagentCache[reagentItemID];
if not itemRecipes then
itemRecipes = { };
reagentCache[reagentItemID] = itemRecipes;
end
-- app.PrintDebug("Reagent",reagentItemID,"x",reagentCount,"=>",craftedItemID,"via",recipeID)
itemRecipes[recipeID] = { craftedItemID, reagentCount };
end
end
end
end
end
app.HarvestRecipes = function()
local reagentsDB = LocalizeGlobal("AllTheThingsHarvestItems", true)
reagentsDB.ReagentsDB = app.ReagentsDB
local Runner = self:GetRunner()
Runner.SetPerFrame(100);
local Run = Runner.Run;
for spellID,data in pairs(SearchForFieldContainer("spellID")) do
Run(CacheRecipeSchematic, spellID);
end
Runner.OnEnd(function()
app.print("Harvested all Sourced Recipes & Reagents => [Reagents]")
end);
end
local function UpdateLocalizedCategories(self, updates)
if not updates.Categories then
-- app.PrintDebug("UpdateLocalizedCategories",self.lastTradeSkillID)
local categories = AllTheThingsAD.LocalizedCategoryNames;
updates.Categories = true;
local currentCategoryID;
local categoryData = {};
local categoryIDs = { C_TradeSkillUI_GetCategories() };
for i = 1,#categoryIDs do
currentCategoryID = categoryIDs[i];
if not categories[currentCategoryID] then
C_TradeSkillUI_GetCategoryInfo(currentCategoryID, categoryData);
if categoryData.name then
categories[currentCategoryID] = categoryData.name;
end
end
end
end
end
local function UpdateLearnedRecipes(self, updates)
-- Cache learned recipes
if not updates.Recipes then
-- app.PrintDebug("UpdateLearnedRecipes",self.lastTradeSkillID)
if app.Debugging then
local reagentsDB = LocalizeGlobal("AllTheThingsHarvestItems", true)
reagentsDB.ReagentsDB = app.ReagentsDB
end
updates.Recipes = true;
wipe(MissingRecipes)
local categoryData = {};
local learned, recipeID = {}, nil;
local recipeIDs = C_TradeSkillUI.GetAllRecipeIDs();
local acctSpells, charSpells = ATTAccountWideData.Spells, app.CurrentCharacter.Spells;
local spellRecipeInfo, currentCategoryID;
local categories = AllTheThingsAD.LocalizedCategoryNames;
-- app.PrintDebug("Scanning recipes",#recipeIDs)
for i = 1,#recipeIDs do
spellRecipeInfo = C_TradeSkillUI_GetRecipeInfo(recipeIDs[i]);
-- app.PrintDebug("Recipe",recipeIDs[i])
if spellRecipeInfo then
recipeID = spellRecipeInfo.recipeID;
local cachedRecipe = SearchForObject("recipeID",recipeID,"key")
currentCategoryID = spellRecipeInfo.categoryID;
if not categories[currentCategoryID] then
C_TradeSkillUI_GetCategoryInfo(currentCategoryID, categoryData);
if categoryData.name then
categories[currentCategoryID] = categoryData.name;
end
end
-- recipe is learned, so cache that it's learned regardless of being craftable
if spellRecipeInfo.learned then
-- Shadowlands recipes are weird...
local rank = spellRecipeInfo.unlockedRecipeLevel or 0;
if rank > 0 then
-- when the recipeID specifically is available, it will show as available for ALL possible ranks
-- so we can check if the next known rank is also considered available for this recipeID
spellRecipeInfo = C_TradeSkillUI_GetRecipeInfo(recipeID, rank + 1);
-- app.PrintDebug("NextRankCheck",recipeID,rank + 1, spellRecipeInfo.learned)
end
end
-- recipe is learned, so cache that it's learned regardless of being craftable
if spellRecipeInfo and spellRecipeInfo.learned then
-- only disabled & enable-type recipes should be un-cached when considered learned
if spellRecipeInfo.disabled and cachedRecipe and cachedRecipe.isEnableTypeRecipe then
-- disabled learned enable-type recipes shouldn't be marked as known by the character (they require an 'unlock' typically to become usable)
if charSpells[recipeID] then
charSpells[recipeID] = nil;
-- local link = app:Linkify(recipeID, app.Colors.ChatLink, "search:recipeID:"..recipeID);
-- app.PrintDebug("Unlearned Disabled Recipe", link);
end
else
charSpells[recipeID] = 1;
if not acctSpells[recipeID] then
acctSpells[recipeID] = 1;
tinsert(learned, recipeID);
end
end
else
if spellRecipeInfo.disabled then
-- disabled & unlearned recipes shouldn't be marked as known by the character
if charSpells[recipeID] then
charSpells[recipeID] = nil;
-- local link = app:Linkify(recipeID, app.Colors.ChatLink, "search:spellID:"..recipeID);
-- app.PrintDebug("Unlearned Disabled Recipe", link);
end
else
-- ignore removal of enable-type recipes when considered unlearned and not disabled
if cachedRecipe and cachedRecipe.isEnableTypeRecipe then
-- local link = app:Linkify(recipeID, app.Colors.ChatLink, "search:recipeID:"..recipeID);
-- app.PrintDebug("Unlearned Enable-Type Recipe", link);
else
-- non-disabled, unlearned recipes shouldn't be marked as known by the character
if charSpells[recipeID] then
charSpells[recipeID] = nil;
-- local link = app:Linkify(recipeID, app.Colors.ChatLink, "search:spellID:"..recipeID);
-- app.PrintDebug("Unlearned Recipe", link);
end
end
end
end
-- moved to stand-alone on-demand function across all known professions, or called if DEBUG_PRINT is enabled to harvest un-sourced recipes
if app.Debugging then
CacheRecipeSchematic(recipeID);
end
end
end
-- If something new was "learned", then refresh the data.
-- app.PrintDebug("Done. learned",#learned)
app.UpdateRawIDs("spellID", learned);
if #learned > 0 then
app.HandleEvent("OnThingCollected", "Recipes")
self.force = true;
end
-- In Debugging, pop a dialog of all found missing recipes
if app.Debugging then
if #MissingRecipes > 0 then
app:ShowPopupDialogWithMultiLineEditBox(app.TableConcat(MissingRecipes, nil, nil, "\n"), nil, "Missing Recipes")
else
app.PrintDebug("No Missing Recipes!")
end
end
end
end
-- Custom SearchValueCriteria for requireSkill searches
local criteria = {
SearchValueCriteria = {
-- Include if the field of the group matches the desired value (or via translated requireSkill value matches)
-- and if it filters for the current character
function(o, field, value)
local v = o[field]
return v and (v == value or app.SkillDB.SpellToSkill[app.SkillDB.SpecializationSpells[v] or 0] == value)
and app.CurrentCharacterFilters(o)
end
}
}
local function UpdateData(self, updates)
-- Open the Tradeskill list for this Profession
local data = updates.Data;
if not data then
-- app.PrintDebug("UpdateData",self.lastTradeSkillID)
data = app.CreateProfession(self.lastTradeSkillID);
app.BuildSearchResponse_IgnoreUnavailableRecipes = true;
NestObjects(data, app:BuildSearchResponse("requireSkill", data.requireSkill, nil, criteria));
-- Profession headers use 'professionID' and don't actually convey a requirement on knowing the skill
-- but in a Profession window for that skill it's nice to see what that skill can craft...
NestObjects(data, app:BuildSearchResponse("professionID", data.requireSkill));
app.BuildSearchResponse_IgnoreUnavailableRecipes = nil;
data.indent = 0;
data.visible = true;
AssignChildren(data);
updates.Data = data;
-- only expand the list if this is the first time it is being generated
self.ExpandInfo = { Expand = true };
self.force = true;
end
self:SetData(data);
self:Update(self.force);
end
-- Can trigger multiple times quickly, but will only run once per profession in a row
self.RefreshRecipes = function(self, doUpdate)
-- If it's not yours, don't take credit for it.
if C_TradeSkillUI.IsTradeSkillLinked() or C_TradeSkillUI.IsTradeSkillGuild() then return; end
if app.Settings.Collectibles.Recipes then
-- app.PrintDebug("RefreshRecipes")
-- Cache Learned Spells
local skillCache = app.GetRawFieldContainer("spellID");
if not skillCache then return; end
local tradeSkillID = app.GetTradeSkillLine();
self.lastTradeSkillID = tradeSkillID;
local updates = self.SkillsInit[tradeSkillID] or {};
self.SkillsInit[tradeSkillID] = updates;
if doUpdate then
-- allow re-scanning learned Recipes
-- app.PrintDebug("Allow Rescan of Recipes")
updates.Recipes = nil;
end
local Runner = self:GetRunner()
Runner.Run(UpdateLocalizedCategories, self, updates);
Runner.Run(UpdateLearnedRecipes, self, updates);
Runner.Run(UpdateData, self, updates);
end
end
-- 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:SetMovable(true);
self:ClearAllPoints();
if visible and self.cachedTSMFrame then
---@diagnostic disable-next-line: undefined-field
local queue = self.cachedTSMFrame.queue;
if queue and queue:IsShown() then
self:SetPoint("TOPLEFT", queue, "TOPRIGHT", 0, 0);
self:SetPoint("BOTTOMLEFT", 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);
-- Skillet compatibility
elseif SkilletFrame then
self:SetPoint("TOPLEFT", SkilletFrame, "TOPRIGHT", 0, 0);
self:SetPoint("BOTTOMLEFT", SkilletFrame, "BOTTOMRIGHT", 0, 0);
self:SetMovable(true);
elseif TradeSkillFrame then
-- Default Alignment on the WoW UI.
self:SetPoint("TOPLEFT", TradeSkillFrame, "TOPRIGHT", 0, 0);
self:SetPoint("BOTTOMLEFT", TradeSkillFrame, "BOTTOMRIGHT", 0, 0);
self:SetMovable(false);
elseif ProfessionsFrame then
-- Default Alignment on the 10.0 WoW UI
self:SetPoint("TOPLEFT", ProfessionsFrame, "TOPRIGHT", 0, 0);
self:SetPoint("BOTTOMLEFT", ProfessionsFrame, "BOTTOMRIGHT", 0, 0);
self:SetMovable(false);
else
self:SetMovable(false);
StartCoroutine("TSMWHY", function()
while InCombatLockdown() or not TradeSkillFrame do coroutine.yield(); end
StartCoroutine("TSMWHYPT2", function()
local thing = self.TSMCraftingVisible;
self.TSMCraftingVisible = nil;
self:SetTSMCraftingVisible(thing);
end);
end);
return;
end
AfterCombatCallback(self.Update, self);
end
-- Setup Event Handlers and register for events
local EventHandlers = {
TRADE_SKILL_SHOW = function(self)
-- If it's not yours, don't take credit for it.
if C_TradeSkillUI.IsTradeSkillLinked() or C_TradeSkillUI.IsTradeSkillGuild() then
self:SetVisible(false)
return false
end
-- Check to see if ATT has information about this profession.
local tradeSkillID = app.GetTradeSkillLine()
if not tradeSkillID or #SearchForField("professionID", tradeSkillID) < 1 then
self:SetVisible(false)
return false
end
if self.TSMCraftingVisible == nil then
self:SetTSMCraftingVisible(false)
end
if app.Settings:GetTooltipSetting("Auto:ProfessionList") then
self:SetVisible(true)
end
self:RefreshRecipes(true)
end,
TRADE_SKILL_CLOSE = function(self)
self:SetVisible(false)
end,
}
EventHandlers.GARRISON_TRADESKILL_NPC_CLOSED = EventHandlers.TRADE_SKILL_CLOSE
self:SetScript("OnEvent", function(self, e, ...)
-- app.PrintDebug("Tradeskills.event",e,...)
local handler = EventHandlers[e]
if not handler then return end
-- app.PrintDebug("Tradeskills.event.handle",e)
handler(self, e, ...)
-- app.PrintDebugPrior("Tradeskills.event.done")
end)
return
end
if self:IsVisible() then
if TSM_API and TSMAPI_FOUR then
if not self.cachedTSMFrame then
for i,child in ipairs({UIParent:GetChildren()}) do
---@class ATTChildFrameTemplate: Frame
---@field headerBgCenter any
local f = child;
if f.headerBgCenter then
self.cachedTSMFrame = f;
local oldSetVisible = f.SetVisible;
local oldShow = f.Show;
local oldHide = f.Hide;
f.SetVisible = function(frame, visible)
oldSetVisible(frame, visible);
self:SetTSMCraftingVisible(visible);
end
f.Hide = function(frame)
oldHide(frame);
self:SetTSMCraftingVisible(false);
end
f.Show = function(frame)
oldShow(frame);
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;
---@diagnostic disable-next-line: duplicate-set-field
TSMAPI_FOUR.UI.NewElement = function(...)
AfterCombatCallback(self.Update, self);
return self.OldNewElement(...);
end
end
end
elseif TSMCraftingTradeSkillFrame then
-- print("TSMCraftingTradeSkillFrame")
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(frame, visible)
oldSetVisible(frame, visible);
self:SetTSMCraftingVisible(visible);
end
f.Hide = function(frame)
oldHide(frame);
self:SetTSMCraftingVisible(false);
end
f.Show = function(frame)
oldShow(frame);
self:SetTSMCraftingVisible(true);
end
if f.queueBtn then
local setScript = f.queueBtn.SetScript;
f.queueBtn.SetScript = function(frame, e, callback)
if e == "OnClick" then
setScript(frame, e, function(...)
if callback then callback(...); end
local thing = self.TSMCraftingVisible;
self.TSMCraftingVisible = nil;
self:SetTSMCraftingVisible(thing);
end);
else
setScript(frame, e, callback);
end
end
f.queueBtn:SetScript("OnClick", f.queueBtn:GetScript("OnClick"));
end
self:SetTSMCraftingVisible(f:IsShown());
return;
end
end
-- Update the window and all of its row data
self:DefaultUpdate(force or self.force, got);
self.force = nil;
end
end)
app.AddCustomWindowOnUpdate("WorldQuests", function(self, force, got)
-- localize some APIs
local C_TaskQuest_GetQuestsForPlayerByMapID = C_TaskQuest.GetQuestsOnMap;
local C_AreaPoiInfo_GetAreaPOIForMap,C_AreaPoiInfo_GetAreaPOIInfo,C_AreaPoiInfo_GetEventsForMap,C_AreaPoiInfo_GetAreaPOISecondsLeft
= C_AreaPoiInfo.GetAreaPOIForMap,C_AreaPoiInfo.GetAreaPOIInfo,C_AreaPoiInfo.GetEventsForMap,C_AreaPoiInfo.GetAreaPOISecondsLeft
local C_QuestLine_RequestQuestLinesForMap = C_QuestLine.RequestQuestLinesForMap;
local C_QuestLine_GetAvailableQuestLines = C_QuestLine.GetAvailableQuestLines;
local C_Map_GetMapChildrenInfo = C_Map.GetMapChildrenInfo;
local C_QuestLog_GetBountiesForMapID = C_QuestLog.GetBountiesForMapID;
local GetNumRandomDungeons, GetLFGDungeonInfo, GetLFGRandomDungeonInfo, GetLFGDungeonRewards, GetLFGDungeonRewardInfo =
GetNumRandomDungeons, GetLFGDungeonInfo, GetLFGRandomDungeonInfo, GetLFGDungeonRewards, GetLFGDungeonRewardInfo;
if self:IsVisible() then
if not self.initialized then
self.initialized = true;
force = true;
local UpdateButton = app.CreateRawText(L.UPDATE_WORLD_QUESTS, {
["icon"] = 134269,
["description"] = L.UPDATE_WORLD_QUESTS_DESC,
["hash"] = "funUpdateWorldQuests",
["OnClick"] = function(data, button)
Push(self, "WorldQuests-Rebuild", self.Rebuild);
return true;
end,
["OnUpdate"] = app.AlwaysShowUpdate,
})
local data = app.CreateRawText(L.WORLD_QUESTS, {
["icon"] = 237387,
["description"] = L.WORLD_QUESTS_DESC,
["indent"] = 0,
["back"] = 1,
["g"] = {
UpdateButton,
},
})
self:SetData(data);
-- Build the initial heirarchy
self:BuildData();
local emissaryMapIDs = {
{ 619, 650 }, -- Broken Isles, Highmountain
{ app.FactionID == Enum.FlightPathFaction.Horde and 875 or 876, 895 }, -- Kul'Tiras or Zandalar, Stormsong Valley
};
local worldMapIDs = {
-- The War Within Continents
{
2274, -- Khaz Algar
},
-- Dragon Isles Continents
{
1978, -- Dragon Isles
{
2085, -- Primalist Tomorrow
}
},
-- Shadowlands Continents
{
1550, -- Shadowlands
-- {}
},
-- BFA Continents
{
875, -- Zandalar
},
{
876, -- Kul'Tiras
},
{ 1355 }, -- Nazjatar
-- Legion Continents
{
619, -- Broken Isles
{
627, -- Dalaran
}
},
{ 905 }, -- Argus
-- WoD Continents
{ 572 }, -- Draenor
-- MoP Continents
{
424, -- Pandaria
},
-- Cataclysm Continents
{ 948 }, -- The Maelstrom
-- WotLK Continents
{ 113 }, -- Northrend
-- BC Continents
{ 101 }, -- Outland
-- Vanilla Continents
{
12, -- Kalimdor
{
62, -- Darkshore
},
},
{ 13, -- Eastern Kingdoms
{
14, -- Arathi Highlands
},
},
}
local RepeatablesPerMapID = {
[2200] = { -- Emerald Dream
78319, -- The Superbloom
},
[2024] = { -- The Azure Span
79226, -- The Big Dig: Traitor's Rest
},
}
-- Blizz likes to list the same quest on multiple maps
local AddedQuestIDs = {}
self.Clear = function(self)
self:GetRunner().Reset()
local g = self.data.g
-- wipe parent references from current top-level groups so any delayed
-- updates on sub-groups no longer chain to the window
for _,o in ipairs(g) do
o.parent = nil
end
wipe(g);
tinsert(g, UpdateButton);
self:BuildData();
self:Update(true);
end
-- World Quests (Tasks)
self.MergeTasks = function(self, mapObject)
local mapID = mapObject.mapID;
if not mapID then return; end
local pois = C_TaskQuest_GetQuestsForPlayerByMapID(mapID);
-- app.PrintDebug(#pois,"WQ in",mapID);
if pois then
for i,poi in ipairs(pois) do
-- only include Tasks on this actual mapID since each Zone mapID is checked individually
if poi.mapID == mapID and not AddedQuestIDs[poi.questID] then
-- app.PrintTable(poi)
AddedQuestIDs[poi.questID] = true
local questObject = GetPopulatedQuestObject(poi.questID);
if questObject then
if self.includeAll or
-- include the quest in the list if holding shift and tracking quests
(self.includePermanent and self.includeQuests) or
-- or if it is repeatable (i.e. one attempt per day/week/year)
questObject.repeatable or
-- or if it has time remaining
(questObject.timeRemaining or 0 > 0)
then
-- if poi.questID == 78663 then
-- app.print("WQ",questObject.questID,questObject.g and #questObject.g);
-- end
-- add the map POI coords to our new quest object
if poi.x and poi.y then
questObject.coords = {{ 100 * poi.x, 100 * poi.y, mapID }}
end
NestObject(mapObject, questObject);
-- see if need to retry based on missing data
-- if not self.retry and questObject.missingData then self.retry = true; end
end
end
-- else app.PrintDebug("Skipped WQ",mapID,poi.mapID,poi.questID)
end
end
end
end
local MapPOIs = {}
-- Area POIs (Points of Interest)
self.MergeAreaPOIs = function(self, mapObject)
local mapID = mapObject.mapID
if not mapID then return end
local pois = app.ArrayAppend(C_AreaPoiInfo_GetAreaPOIForMap(mapID), C_AreaPoiInfo_GetEventsForMap(mapID))
if not pois or #pois == 0 then return end
-- replace the POI IDs with their respective infos
for i=1,#pois do
pois[i] = C_AreaPoiInfo_GetAreaPOIInfo(mapID, pois[i])
end
local poi, poiID, x, y
for i=1,#pois do
poi = pois[i]
poiID = poi.areaPoiID
local poiMapID = poi.linkedUiMapID
-- one poiID may by linked to multiple Things or copies of a Thing so make sure to merge them together
local o = app.SearchForObject("poiID", poiID, nil, true)
if #o > 0 then
local clone = CreateObject(o[1])
if not poiMapID and not poi.isPrimaryMapForPOI then
poiMapID = GetRelativeValue(o[1], "mapID")
end
if #o > 1 then
for i=2,#o do
MergeProperties(clone, o[i])
end
end
o = clone
end
if o and o.__type then
o.timeRemaining = C_AreaPoiInfo_GetAreaPOISecondsLeft(poiID)
if self.includeAll or
-- if it has time remaining
(not o.timeRemaining or (o.timeRemaining > 0))
then
-- add the map POI coords to our new object
if poi.position then
x, y = poi.position.x, poi.position.y
else
x, y = nil, nil
end
if x and y then
o.coords = {{ 100 * x, 100 * y, mapID }}
end
if not poiMapID or poiMapID == mapID or poi.isPrimaryMapForPOI then
NestObject(mapObject, o)
else
local mapPOIs = MapPOIs[poiMapID]
if mapPOIs then mapPOIs[#mapPOIs + 1] = o
else
mapPOIs = {o}
MapPOIs[poiMapID] = mapPOIs
end
end
end
end
end
-- add any POIs which show only on 'other' maps but intended for this one
local myPOIs = MapPOIs[mapID]
if myPOIs then
for i=1,#myPOIs do
NestObject(mapObject, myPOIs[i])
end
end
end
-- Storylines/Map Quest Icons
self.MergeStorylines = function(self, mapObject)
local mapID = mapObject.mapID;
if not mapID then return; end
C_QuestLine_RequestQuestLinesForMap(mapID);
local questLines = C_QuestLine_GetAvailableQuestLines(mapID)
if questLines then
for id,questLine in pairs(questLines) do
-- dont show 'hidden' quest lines... not sure what this is exactly
if not questLine.isHidden and not AddedQuestIDs[questLine.questID] then
AddedQuestIDs[questLine.questID] = true
local questObject = GetPopulatedQuestObject(questLine.questID);
if questObject then
if self.includeAll or
-- include the quest in the list if holding shift and tracking quests
(self.includePermanent and self.includeQuests) or
-- or if it is repeatable (i.e. one attempt per day/week/year)
questObject.repeatable or
-- or if it has time remaining
(questObject.timeRemaining or 0 > 0)
then
NestObject(mapObject, questObject);
-- see if need to retry based on missing data
-- if not self.retry and questObject.missingData then self.retry = true; end
end
end
end
end
else
-- print("No questline data yet for mapID:",mapID);
self.retry = true;
end
end
-- Static Repeatables
self.MergeRepeatables = function(self, mapObject)
local mapID = mapObject.mapID;
if not mapID then return; end
local repeatables = RepeatablesPerMapID[mapID]
if not repeatables then return end
local questObject
for _,questID in ipairs(repeatables) do
questObject = GetPopulatedQuestObject(questID)
if questObject then
if self.includeAll or
-- Account/Debug or not saved
(app.MODE_DEBUG_OR_ACCOUNT or not questObject.saved)
then
NestObject(mapObject, questObject);
-- see if need to retry based on missing data
-- if not self.retry and questObject.missingData then self.retry = true; end
end
end
end
end
self.BuildMapAndChildren = function(self, mapObject)
if not mapObject.mapID then return; end
-- print("Build Map",mapObject.mapID,mapObject.text);
-- Merge Tasks for Zone
self:MergeTasks(mapObject)
-- Merge Storylines for Zone
self:MergeStorylines(mapObject)
-- Merge Repeatables for Zone
self:MergeRepeatables(mapObject)
-- Merge Area POIs for Zone
self:MergeAreaPOIs(mapObject)
-- look for quests on map child maps as well
local mapChildInfos = C_Map_GetMapChildrenInfo(mapObject.mapID, 3);
if mapChildInfos then
for _,mapInfo in ipairs(mapChildInfos) do
-- start fetching the data while other stuff is setup
C_QuestLine_RequestQuestLinesForMap(mapInfo.mapID);
local subMapObject = app.CreateMapWithStyle(mapInfo.mapID);
-- Build the children maps
self:BuildMapAndChildren(subMapObject);
NestObject(mapObject, subMapObject);
end
end
end
self.Rebuild = function(self, no)
-- Already filled with data and nothing needing to retry, just give it a forced update pass since data for quests should now populate dynamically
if not self.retry and #self.data.g > 1 then
-- app.PrintDebug("Already WQ data, just update again")
-- Force Update Callback
Callback(self.Update, self, true);
return;
end
-- Reset the world quests Runner before building new data
self:GetRunner().Reset()
wipe(self.data.g);
-- Rebuild all World Quest data
wipe(AddedQuestIDs)
wipe(MapPOIs)
-- app.PrintDebug("Rebuild WQ Data")
self.retry = nil;
-- Put a 'Clear World Quests' click first in the list
local temp = {app.CreateRawText(L.CLEAR_WORLD_QUESTS, {
['icon'] = 2447782,
['description'] = L.CLEAR_WORLD_QUESTS_DESC,
['hash'] = "funClearWorldQuests",
['OnClick'] = function(data, button)
Push(self, "WorldQuests-Clear", self.Clear);
return true;
end,
['OnUpdate'] = app.AlwaysShowUpdate,
})}
-- options when refreshing the list
self.includeAll = app.MODE_DEBUG;
self.includeQuests = app.Settings.Collectibles.Quests or app.Settings.Collectibles.QuestsLocked;
self.includePermanent = IsAltKeyDown() or self.includeAll;
-- Acquire all of the world mapIDs
for _,pair in ipairs(worldMapIDs) do
local mapID = pair[1];
-- app.PrintDebug("WQ.WorldMapIDs.", mapID)
-- start fetching the data while other stuff is setup
C_QuestLine_RequestQuestLinesForMap(mapID);
local mapObject = app.CreateMapWithStyle(mapID);
-- Build top-level maps all the way down
self:BuildMapAndChildren(mapObject);
-- Additional Maps
local additionalMapIDs = pair[2];
if additionalMapIDs then
for i=1,#additionalMapIDs do
-- Basic Sub-map
local subMap = app.CreateMapWithStyle(additionalMapIDs[i])
-- Build top-level maps all the way down for the sub-map
self:BuildMapAndChildren(subMap);
NestObject(mapObject, subMap);
end
end
-- Merge everything for this map into the list
app.Sort(mapObject.g, true)
MergeObject(temp, mapObject)
end
-- Acquire all of the emissary quests
for _,pair in ipairs(emissaryMapIDs) do
local mapID = pair[1];
-- print("WQ.EmissaryMapIDs." .. tostring(mapID))
local mapObject = app.CreateMapWithStyle(mapID);
local bounties = C_QuestLog_GetBountiesForMapID(pair[2]);
if bounties and #bounties > 0 then
for _,bounty in ipairs(bounties) do
local questObject = GetPopulatedQuestObject(bounty.questID);
NestObject(mapObject, questObject);
end
end
app.Sort(mapObject.g, true)
MergeObject(temp, mapObject);
end
-- Heroic Deeds
if self.includePermanent and not (IsQuestFlaggedCompleted(32900) or IsQuestFlaggedCompleted(32901)) then
local mapObject = app.CreateMapWithStyle(424);
NestObject(mapObject, GetPopulatedQuestObject(app.FactionID == Enum.FlightPathFaction.Alliance and 32900 or 32901));
MergeObject(temp, mapObject);
end
local OnUpdateForLFGHeader = function(group)
local meetLevelrange = app.Modules.Filter.Filters.Level(group);
if meetLevelrange or app.MODE_DEBUG_OR_ACCOUNT then
-- default logic for available LFG category/Debug/Account
return false;
else
group.visible = nil;
return true;
end
end
-- Get the LFG Rewards Available at this level
local numRandomDungeons = GetNumRandomDungeons();
-- print(numRandomDungeons,"numRandomDungeons");
if numRandomDungeons > 0 then
local gfg = {}
local groupFinder = app.CreateRawText(DUNGEONS_BUTTON, { icon = app.asset("Category_GroupFinder"), g = gfg })
for index=1,numRandomDungeons,1 do
local dungeonID = GetLFGRandomDungeonInfo(index);
-- app.PrintDebug("RandInfo",index,GetLFGRandomDungeonInfo(index));
-- app.PrintDebug("NormInfo",dungeonID,GetLFGDungeonInfo(dungeonID))
-- app.PrintDebug("DungeonAppearsInRandomLFD(dungeonID)",DungeonAppearsInRandomLFD(dungeonID)); -- useless
local name, typeID, subtypeID, minLevel, maxLevel, recLevel, minRecLevel, maxRecLevel, expansionLevel, groupID, textureFilename, difficulty, maxPlayers, description, isHoliday, bonusRepAmount, minPlayers, isTimeWalker, name2, minGearLevel = GetLFGDungeonInfo(dungeonID);
-- print(dungeonID,name, typeID, subtypeID, minLevel, maxLevel, recLevel, minRecLevel, maxRecLevel, expansionLevel, groupID, textureFilename, difficulty, maxPlayers, description, isHoliday, bonusRepAmount, minPlayers, isTimeWalker, name2, minGearLevel);
local _, gold, unknown, xp, unknown2, numRewards, unknown = GetLFGDungeonRewards(dungeonID);
-- print("GetLFGDungeonRewards",dungeonID,GetLFGDungeonRewards(dungeonID));
local hg = {}
local header = app.CreateRawText(name, { g = hg, dungeonID = dungeonID, description = description, lvl = { minRecLevel or 1, maxRecLevel }, OnUpdate = OnUpdateForLFGHeader})
if expansionLevel and not isHoliday then
header.icon = app.CreateExpansion(expansionLevel + 1).icon;
elseif isTimeWalker then
header.icon = app.asset("Difficulty_Timewalking");
end
for rewardIndex=1,numRewards,1 do
local itemName, icon, count, claimed, rewardType, itemID, quality = GetLFGDungeonRewardInfo(dungeonID, rewardIndex);
-- common logic
local idType = (rewardType or "item").."ID";
local thing = { [idType] = itemID };
local _cache = SearchForField(idType, itemID);
for _,data in ipairs(_cache) do
-- copy any sourced data for the dungeon reward into the list
if GroupMatchesParams(data, idType, itemID, true) then
MergeProperties(thing, data);
end
local lvl;
if isTimeWalker then
lvl = (data.lvl and type(data.lvl) == "table" and data.lvl[1]) or
data.lvl or
(data.parent and data.parent.lvl and type(data.parent.lvl) == "table" and data.parent.lvl[1]) or
data.parent.lvl or 0;
else
lvl = 0;
end
-- Should the rewards be listed in the window based on the level of the rewards
if lvl <= minRecLevel then
NestObjects(thing, data.g); -- no need to clone, everything is re-created at the end
end
end
hg[#hg + 1] = thing
end
gfg[#gfg + 1] = header
end
MergeObject(temp, groupFinder)
end
-- put all the things into the window data, turning them into objects as well
NestObjects(self.data, temp);
-- Build the heirarchy
self:BuildData();
-- Force Update
self:Update(true);
end
end
self:DefaultUpdate(force);
end
end)
end)();
do -- Setup and Startup Functionality
-- Creates the data structures and initial 'Default' profiles for ATT
app.SetupProfiles = function()
-- base profiles containers
local ATTProfiles = {
Profiles = {},
Assignments = {},
};
AllTheThingsProfiles = ATTProfiles;
local default = app.Settings:NewProfile(DEFAULT);
-- copy various existing settings that are now Profiled
if AllTheThingsSettings then
-- General Settings
if AllTheThingsSettings.General then
for k,v in pairs(AllTheThingsSettings.General) do
default.General[k] = v;
end
end
-- Tooltip Settings
if AllTheThingsSettings.Tooltips then
for k,v in pairs(AllTheThingsSettings.Tooltips) do
default.Tooltips[k] = v;
end
end
-- Seasonal Filters
if AllTheThingsSettings.Seasonal then
for k,v in pairs(AllTheThingsSettings.Seasonal) do
default.Seasonal[k] = v;
end
end
-- Unobtainable Filters
if AllTheThingsSettings.Unobtainable then
for k,v in pairs(AllTheThingsSettings.Unobtainable) do
default.Unobtainable[k] = v and true or nil;
end
end
end
-- pull in window data for the default profile
for _,window in pairs(app.Windows) do
window:StorePosition();
end
app.print("Initialized ATT Profiles!");
-- delete old variables
AllTheThingsSettings = nil;
AllTheThingsAD.UnobtainableItemFilters = nil;
AllTheThingsAD.SeasonalFilters = nil;
-- initialize settings again due to profiles existing now
app.Settings:Initialize();
end
-- Called when the Addon is loaded to process initial startup information
app.Startup = function()
-- app.PrintMemoryUsage("Startup")
AllTheThingsAD = LocalizeGlobalIfAllowed("AllTheThingsAD", true); -- For account-wide data.
-- Cache the Localized Category Data
AllTheThingsAD.LocalizedCategoryNames = setmetatable(AllTheThingsAD.LocalizedCategoryNames or {}, { __index = app.CategoryNames });
app.CategoryNames = nil;
-- Clear some keys which got added and shouldn't have been
AllTheThingsAD.ExplorationDB = nil
AllTheThingsAD.ExplorationAreaPositionDB = nil
-- Character Data Storage
local characterData = LocalizeGlobalIfAllowed("ATTCharacterData", true);
local currentCharacter = characterData[app.GUID];
if not currentCharacter then
currentCharacter = {};
characterData[app.GUID] = currentCharacter;
end
currentCharacter.build = app.GameBuildVersion;
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 app.Race then currentCharacter.race = app.Race; end
if not currentCharacter.ActiveSkills then currentCharacter.ActiveSkills = {}; end
if not currentCharacter.CustomCollects then currentCharacter.CustomCollects = {}; end
if not currentCharacter.Deaths then currentCharacter.Deaths = 0; end
if not currentCharacter.Lockouts then currentCharacter.Lockouts = {}; end
if not currentCharacter.Professions then currentCharacter.Professions = {}; end
app.CurrentCharacter = currentCharacter;
app.AddEventHandler("OnPlayerLevelUp", function()
currentCharacter.lvl = app.Level;
end);
-- Account Wide Data Storage
ATTAccountWideData = LocalizeGlobalIfAllowed("ATTAccountWideData", true);
local accountWideData = ATTAccountWideData;
if not accountWideData.HeirloomRanks then accountWideData.HeirloomRanks = {}; end
-- Old unused data
currentCharacter.CommonItems = nil
ATTAccountWideData.CommonItems = nil
-- Notify Event Handlers that Saved Variable Data is available.
app.HandleEvent("OnSavedVariablesAvailable", currentCharacter, ATTAccountWideData);
-- Event handlers which need Saved Variable data which is added by OnSavedVariablesAvailable handlers into saved variables
app.HandleEvent("OnAfterSavedVariablesAvailable", currentCharacter, ATTAccountWideData);
-- Update the total account wide death counter.
local deaths = 0;
for guid,character in pairs(characterData) do
if character and character.Deaths and character.Deaths > 0 then
deaths = deaths + character.Deaths;
end
end
ATTAccountWideData.Deaths = deaths;
-- CRIEVE NOTE: Once the Sync Window is moved over from Classic, this can be removed.
if not AllTheThingsAD.LinkedAccounts then
AllTheThingsAD.LinkedAccounts = {};
end
-- app.PrintMemoryUsage("Startup:Done")
end
-- This needs to be the first OnStartup event processed
app.AddEventHandler("OnStartup", app.Startup, true)
local function PrePopulateAchievementSymlinks()
local achCache = app.SearchForFieldContainer("achievementID")
-- app.PrintDebug("FillAchSym")
if achCache then
local FillSym = app.FillAchievementCriteriaAsync
app.FillRunner.SetPerFrame(500)
local Run = app.FillRunner.Run
local group
for achID,groups in pairs(achCache) do
for i=1,#groups do
group = groups[i]
if group.__type == "Achievement" and not GetRelativeValue(group, "sourceIgnored") then
-- app.PrintDebug("FillAchSym",group.hash)
Run(FillSym, group)
end
end
end
app.FillRunner.SetPerFrame(25)
end
Callback(app.RemoveEventHandler, PrePopulateAchievementSymlinks)
-- app.PrintDebug("Done:FillAchSym")
end
app.AddEventHandler("OnRefreshCollectionsDone", PrePopulateAchievementSymlinks)
-- Function which is triggered after Startup
local function InitDataCoroutine()
local yield = coroutine.yield
-- app.PrintMemoryUsage("InitDataCoroutine")
-- if IsInInstance() then
-- app.print("cannot fully load while in an Instance due to Blizzard restrictions. Please Zone out to finish loading ATT.")
-- end
-- Wait for the Data Cache to return something.
while not app:GetDataCache() do yield(); end
-- Wait for the app to finish OnStartup event, somehow this can trigger out of order on some clients
while app.Wait_OnStartupDone do yield(); end
local accountWideData = LocalizeGlobalIfAllowed("ATTAccountWideData", true);
local characterData = LocalizeGlobalIfAllowed("ATTCharacterData", true);
local currentCharacter = characterData[app.GUID];
-- Clean up other matching Characters with identical Name-Realm but differing GUID
Callback(function()
local myGUID = app.GUID;
local myName, myRealm = currentCharacter.name, currentCharacter.realm;
local myRegex = "%|cff[A-z0-9][A-z0-9][A-z0-9][A-z0-9][A-z0-9][A-z0-9]"..myName.."%-"..myRealm.."%|r";
local otherName, otherRealm, otherText;
local toClean;
for guid,character in pairs(characterData) do
-- simple check on name/realm first
otherName = character.name;
otherRealm = character.realm;
otherText = character.text;
if guid ~= myGUID then
if otherName == myName and otherRealm == myRealm then
if toClean then tinsert(toClean, guid)
else toClean = { guid }; end
elseif otherText and otherText:match(myRegex) then
if toClean then tinsert(toClean, guid)
else toClean = { guid }; end
end
end
end
if toClean then
local copyTables = { "Buildings","GarrisonBuildings","Factions","FlightPaths" };
local cleanCharacterFunc = function(guid)
-- copy the set of QuestIDs from the duplicate character (to persist repeatable Quests collection)
local character = characterData[guid];
for _,tableName in ipairs(copyTables) do
local copyTable = character[tableName];
if copyTable then
-- app.PrintDebug("Copying Dupe",tableName)
local currentTable = currentCharacter[tableName];
if not currentTable then
-- old/restored character missing copied data
currentTable = {}
currentCharacter[tableName] = currentTable
end
for ID,complete in pairs(copyTable) do
-- app.PrintDebug("Check",ID,complete,"?",currentTable[ID])
if complete and not currentTable[ID] then
-- app.PrintDebug("Copied Completed",ID)
currentTable[ID] = complete;
end
end
end
end
-- Remove the actual dupe data afterwards
-- move to a backup table temporarily in case anyone reports weird issues, we could potentially resolve them?
local backups = accountWideData._CharacterBackups;
if not backups then
backups = {};
accountWideData._CharacterBackups = backups;
end
backups[guid] = character;
characterData[guid] = nil;
local count = 0
for guid,char in pairs(backups) do
count = count + 1
end
app.print("Removed & Backed up Duplicate Data of Current Character:",character.text,guid,"[You have",count,"total character backups]")
app.print("Use '/att remove-deleted-character-backups help' for more info")
end
for _,guid in ipairs(toClean) do
app.FunctionRunner.Run(cleanCharacterFunc, guid);
end
end
-- Allows removing the character backups that ATT automatically creates for duplicated characters which are replaced by new ones
app.ChatCommands.Add("remove-deleted-character-backups", function(args)
local backups = 0
for guid,char in pairs(accountWideData._CharacterBackups or app.EmptyTable) do
backups = backups + 1
end
accountWideData._CharacterBackups = nil
app.print("Cleaned up",backups,"character backups!")
return true
end, {
"Usage : /att remove-deleted-character-backups",
"Allows permanently removing all deleted character backup data",
"-- ATT removes and cleans out character-specific cached data which is stored by a character with the same Name-Realm as the logged-in character but a different character GUID. If you find yourself creating and deleting a lot of repeated characters, this will clean up those characters' data backups",
})
end);
app.HandleEvent("OnInit")
-- Current character collections shouldn't use '2' ever... so clear any 'inaccurate' data
local currentQuestsCache = currentCharacter.Quests;
for questID,completion in pairs(currentQuestsCache) do
if completion == 2 then currentQuestsCache[questID] = nil; end
end
-- Setup the use of profiles after a short delay to ensure that the layout window positions are collected
if not AllTheThingsProfiles then DelayedCallback(app.SetupProfiles, 5); end
-- do a settings apply to ensure ATT windows which have now been created, are moved according to the current Profile
app.Settings:ApplyProfile();
-- clear harvest data on load in case someone forgets
AllTheThingsHarvestItems = {};
-- warning about debug logging in case it sneaks in we can realize quicker
app.PrintDebug("NOTE: ATT debug prints enabled!")
-- Execute the OnReady handlers.
app.HandleEvent("OnReady")
-- finally can say the app is ready
app.IsReady = true;
-- app.PrintDebug("ATT is Ready!");
-- app.PrintMemoryUsage("InitDataCoroutine:Done")
end
app:RegisterFuncEvent("PLAYER_ENTERING_WORLD", function(...)
-- app.PrintDebug("PLAYER_ENTERING_WORLD",...)
app.InWorld = true;
app:UnregisterEventClean("PLAYER_ENTERING_WORLD")
StartCoroutine("InitDataCoroutine", InitDataCoroutine);
end);
end -- Setup and Startup Functionality
-- Define Event Behaviours
app.AddonLoadedTriggers = {
[appName] = function()
-- OnLoad events (saved variables are now available)
app.HandleEvent("OnLoad")
end,
};
-- Register Event for startup
app:RegisterFuncEvent("ADDON_LOADED", function(addonName)
local addonTrigger = app.AddonLoadedTriggers[addonName];
if addonTrigger then addonTrigger(); end
end)
app.Wait_OnStartupDone = true
app.AddEventHandler("OnStartupDone", function() app.Wait_OnStartupDone = nil end)
-- Clean up unused saved variables if they become deprecated after being pushed to Git
do
local function CleanupDeprecatedSavedVariables()
ATTAccountWideData.Campsite = nil
ATTAccountWideData.WarbandScene = nil
ATTAccountWideData.TEMP_TWWSources = nil
Callback(app.RemoveEventHandler, CleanupDeprecatedSavedVariables)
end
app.AddEventHandler("OnAfterSavedVariablesAvailable", CleanupDeprecatedSavedVariables)
end
-- app.AddEventHandler("Addon.Memory", function(info)
-- app.PrintMemoryUsage(info)
-- end)
-- app.LinkEventSequence("OnLoad", "Addon.Memory")
-- app.LinkEventSequence("OnInit", "Addon.Memory")
-- app.LinkEventSequence("OnReady", "Addon.Memory")
-- app.LinkEventSequence("OnStartupDone", "Addon.Memory")
-- app.LinkEventSequence("OnWindowFillComplete", "Addon.Memory")
-- app.HandleEvent("Addon.Memory", "AllTheThings.EOF")