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.

7258 lines
264 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
= app.AssignChildren, app.GetRelativeValue, app.IsQuestFlaggedCompleted
-- 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
= print, rawget, rawset, tostring, ipairs, pairs, tonumber, wipe, select, setmetatable, getmetatable, tinsert, tremove, type, math.floor
-- 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 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 containsValue = app.containsValue;
local indexOf = app.indexOf;
local CloneArray = app.CloneArray
-- OnLoad assignments, probably temporary as code gets migrated
local ExpandGroupsRecursively
app.AddEventHandler("OnLoad", function()
ExpandGroupsRecursively = app.ExpandGroupsRecursively
end)
-- Data Lib
local AllTheThingsAD = {}; -- For account-wide data.
local function formatNumericWithCommas(amount)
local k
while true do
amount, k = tostring(amount):gsub("^(-?%d+)(%d%d%d)", '%1,%2')
if k == 0 then
break
end
end
return amount
end
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 ResolveSymbolicLink;
local GetSpecsString, GetGroupItemIDWithModID, GetItemIDAndModID, GroupMatchesParams, GetClassesString
= app.GetSpecsString, app.GetGroupItemIDWithModID, app.GetItemIDAndModID, app.GroupMatchesParams, app.GetClassesString
local function CleanInheritingGroups(groups, ...)
-- Cleans any groups which are nested under any group with any specified fields
local arrs = select("#", ...);
if groups and arrs > 0 then
local refined, f, match = {}, nil, nil;
-- app.PrintDebug("CIG:Start",#groups,...)
for _,j in ipairs(groups) do
match = nil;
for n=1,arrs do
f = select(n, ...);
if GetRelativeValue(j, f) then
match = true;
-- app.PrintDebug("CIG:Skip",j.hash,f)
break;
end
end
if not match then
tinsert(refined, j);
end
end
-- app.PrintDebug("CIG:End",#refined)
return refined;
end
end
-- Symlink Lib
do
local select, tremove, unpack =
select, tremove, unpack;
local FinalizeModID, PruneFinalized, FillFinalized, SelectMod
-- Checks if any of the provided arguments can be found within the first array object
local function ContainsAnyValue(arr, ...)
local value;
local vals = select("#", ...);
for i=1,vals do
value = select(i, ...);
for _,v in ipairs(arr) do
if v == value then return true; end
end
end
end
local function Resolve_Extract(results, group, field)
if group[field] then
results[#results + 1] = group
elseif group.g then
for _,o in ipairs(group.g) do
Resolve_Extract(results, o, field);
end
end
end
local function Resolve_Find(results, groups, field, val)
if groups then
for _,o in ipairs(groups) do
if o[field] == val then
results[#results + 1] = o
else
Resolve_Find(results, o.g, field, val)
end
end
end
end
local GetAchievementNumCriteria = GetAchievementNumCriteria
local GetItemInfoInstant = app.WOWAPI.GetItemInfoInstant;
-- Defines a known set of functions which can be run via symlink resolution. The inputs to each function will be identical in order when called.
-- searchResults - the current set of searchResults when reaching the current sym command
-- o - the specific group object which contains the symlink commands
-- (various expected components of the respective sym command)
local ResolveFunctions = {
-- Instruction to search the full database for multiple of a given type
["select"] = function(finalized, searchResults, o, cmd, field, ...)
local cache, val;
local vals = select("#", ...);
local Search = SearchForObject
for i=1,vals do
val = select(i, ...) + (SelectMod or 0)
if field == "modItemID" then
-- this is really dumb but direct raw values don't 'always' properly match generated values...
-- but splitting the value apart and putting it back together searches accurately
val = GetGroupItemIDWithModID(nil, GetItemIDAndModID(val))
end
cache = Search(field, val, "field", true);
if cache and #cache > 0 then
ArrayAppend(searchResults, cache)
else
-- TODO: re-enable after all catalystID's are re-structured
-- app.print("Failed to select ", field, val);
end
end
SelectMod = nil
end,
-- Instruction to select the parent object of the group that owns the symbolic link
["selectparent"] = function(finalized, searchResults, o, cmd, level)
level = level or 1;
-- an search for the specific 'o' to retrieve the source parent since the parent is not always actually attached to the reference resolving the symlink
local parent
local searchedObject = app.SearchForObject(o.key, o.keyval, "key");
if searchedObject then
parent = searchedObject.parent;
while level > 1 do
parent = parent and parent.parent;
level = level - 1;
end
if parent then
-- app.PrintDebug("selectparent-searched",level,parent.hash,parent.text)
tinsert(searchResults, parent);
return;
end
end
app.print("'selectparent' failed for",o.hash);
end,
-- Instruction to find all content marked with the specified 'requireSkill'
["selectprofession"] = function(finalized, searchResults, o, cmd, requireSkill)
local search = app:BuildSearchResponse("requireSkill", requireSkill);
ArrayAppend(searchResults, search);
end,
-- Instruction to fill with identical content Sourced elsewhere for this group (no symlinks)
["fill"] = function(finalized, searchResults, o)
local okey = o.key;
if okey then
local okeyval = o[okey];
if okeyval then
for _,result in ipairs(SearchForObject(okey, okeyval, "field", true)) do
ArrayAppend(searchResults, result.g);
end
end
end
end,
-- Instruction to finalize the current search results and prevent additional queries from affecting this selection
["finalize"] = function(finalized, searchResults)
ArrayAppend(finalized, searchResults);
wipe(searchResults);
end,
-- Instruction to take all of the finalized and non-finalized search results and merge them back in to the processing queue
["merge"] = function(finalized, searchResults)
local orig;
if #searchResults > 0 then
orig = CloneArray(searchResults);
end
wipe(searchResults);
-- finalized first
ArrayAppend(searchResults, finalized);
wipe(finalized);
-- then any existing searchResults
ArrayAppend(searchResults, orig);
end,
-- Instruction to "push" all of the group values into an object as specified
["push"] = function(finalized, searchResults, o, cmd, field, value)
local orig;
if #searchResults > 0 then
orig = CloneArray(searchResults);
end
wipe(searchResults);
local group = CreateObject({[field] = value });
NestObjects(group, orig);
searchResults[1] = group;
end,
-- Instruction to "pop" all of the group values up one level
["pop"] = function(finalized, searchResults)
local orig;
if #searchResults > 0 then
orig = CloneArray(searchResults);
end
wipe(searchResults);
if orig then
for _,obj in ipairs(orig) do
-- insert raw & symlinked Things from this group
ArrayAppend(searchResults, obj.g, ResolveSymbolicLink(obj));
end
end
end,
-- Instruction to include only search results where a key value is a value
["where"] = function(finalized, searchResults, o, cmd, field, value)
for k=#searchResults,1,-1 do
local result = searchResults[k];
if not result[field] or result[field] ~= value then
tremove(searchResults, k);
end
end
end,
-- Instruction to include only search results where a key value is a value
["whereany"] = function(finalized, searchResults, o, cmd, field, ...)
local hash = {};
for k,value in ipairs({...}) do
hash[value] = true;
end
for k=#searchResults,1,-1 do
local result = searchResults[k];
if not result[field] or not hash[result[field]] then
tremove(searchResults, k);
end
end
end,
-- Instruction to extract all nested results which contain a given field
["extract"] = function(finalized, searchResults, o, cmd, field)
local orig;
if #searchResults > 0 then
orig = CloneArray(searchResults);
end
wipe(searchResults);
if orig then
for _,o in ipairs(orig) do
Resolve_Extract(searchResults, o, field);
end
end
end,
-- Instruction to find all nested results which contain a given field/value
["find"] = function(finalized, searchResults, o, cmd, field, val)
if #searchResults > 0 then
local resolved = {}
Resolve_Find(resolved, searchResults, field, val)
wipe(searchResults)
ArrayAppend(searchResults, resolved)
end
end,
-- Instruction to include the search result with a given index within each of the selection's groups
["index"] = function(finalized, searchResults, o, cmd, index)
local orig;
if #searchResults > 0 then
orig = CloneArray(searchResults);
end
wipe(searchResults);
if orig then
local result, g;
for k=#orig,1,-1 do
result = orig[k];
g = result.g;
if g and index <= #g then
tinsert(searchResults, g[index]);
end
end
end
end,
-- Instruction to include only search results where a key value is not a value
["not"] = function(finalized, searchResults, o, cmd, field, ...)
local values = {...};
if #values > 1 then
local matches = {};
for i,o in ipairs(values) do
matches[o] = true;
end
for k=#searchResults,1,-1 do
local result = searchResults[k];
local value = result[field];
if value and matches[value] then
tremove(searchResults, k);
end
end
elseif #values == 1 then
local value = values[1];
for k=#searchResults,1,-1 do
local result = searchResults[k];
if result[field] and result[field] == value then
tremove(searchResults, k);
end
end
else
app.print("'",cmd,"' had empty value set")
end
end,
-- Instruction to include only search results where a key exists
["is"] = function(finalized, searchResults, o, cmd, field)
for k=#searchResults,1,-1 do
if not searchResults[k][field] then tremove(searchResults, k); end
end
end,
-- Instruction to include only search results where a key doesn't exist
["isnt"] = function(finalized, searchResults, o, cmd, field)
for k=#searchResults,1,-1 do
if searchResults[k][field] then tremove(searchResults, k); end
end
end,
-- Instruction to include only search results where a key value/table contains a value
["contains"] = function(finalized, searchResults, o, cmd, field, ...)
local vals = select("#", ...);
if vals < 1 then
app.print("'",cmd,"' had empty value set")
return;
end
local result, kval;
for k=#searchResults,1,-1 do
result = searchResults[k];
kval = result[field];
-- key doesn't exist at all on the result
if not kval then
tremove(searchResults, k);
-- none of the values match the contains values
elseif type(kval) == "table" then
if not ContainsAnyValue(kval, ...) then
tremove(searchResults, k);
end
-- key exists with single value on the result
else
local match;
for i=1,vals do
if kval == select(i, ...) then
match = true;
break;
end
end
if not match then
tremove(searchResults, k);
end
end
end
end,
-- Instruction to exclude search results where a key value contains a value
["exclude"] = function(finalized, searchResults, o, cmd, field, ...)
local vals = select("#", ...);
if vals < 1 then
app.print("'",cmd,"' had empty value set")
return;
end
local result, kval;
for k=#searchResults,1,-1 do
result = searchResults[k];
kval = result[field];
-- key exists
if kval then
local match;
for i=1,vals do
if kval == select(i, ...) then
match = true;
break;
end
end
if match then
-- TEMP logic to allow Ensembles to continue working until they get fixed again
if field == "itemID" and result.g and kval == o[field] then
ArrayAppend(searchResults, result.g);
end
tremove(searchResults, k);
end
end
end
end,
-- Instruction to include only search results where an item is of a specific inventory type
["invtype"] = function(finalized, searchResults, o, cmd, ...)
local vals = select("#", ...);
if vals < 1 then
app.print("'",cmd,"' had empty value set")
return;
end
local result, invtype, itemID;
for k=#searchResults,1,-1 do
result = searchResults[k];
itemID = result.itemID;
if itemID then
invtype = select(4, GetItemInfoInstant(itemID));
local match;
for i=1,vals do
if invtype == select(i, ...) then
match = true;
break;
end
end
if not match then
tremove(searchResults, k);
end
end
end
end,
-- Instruction to search the full database for multiple achievementID's and persist only actual achievements
["meta_achievement"] = function(finalized, searchResults, o, cmd, ...)
local vals = select("#", ...);
if vals < 1 then
app.print("'",cmd,"' had empty value set")
return;
end
local Search = SearchForObject
local cache, value;
for i=1,vals do
value = select(i, ...);
cache = CleanInheritingGroups(Search("achievementID", value, "key", true), "sourceIgnored")
local mergeAch = cache[1]
-- multiple achievements match the selection, make sure to merge them together so we don't lose fields
-- that only exist in the original Source (Achievements source prunes some data)
local count = #cache
if count > 1 then
for j=2,count do
-- app.PrintDebug("Merge Ach",app:SearchLink(cache[j]))
MergeProperties(mergeAch, cache[j])
end
end
if mergeAch then
searchResults[#searchResults + 1] = mergeAch
else
app.print("Failed to select achievementID",value);
end
end
PruneFinalized = { "g" };
end,
-- Instruction to search the full database for an achievementID and persist the associated Criteria
["partial_achievement"] = function(finalized, searchResults, o, cmd, achID)
local cache = app.SearchForField("achievementID", achID)
local crit
for i=1,#cache do
crit = cache[i]
if crit.criteriaID then
searchResults[#searchResults + 1] = crit
end
end
end,
-- Instruction to simply 'prune' sub-groups from the finalized selection, or specified fields
["prune"] = function(finalized, searchResults, o, cmd, ...)
local vals = select("#", ...);
if vals < 1 then
PruneFinalized = { "g" }
return;
end
local value;
for i=1,vals do
value = select(i, ...);
if PruneFinalized then PruneFinalized[#PruneFinalized + 1] = value
else PruneFinalized = { value } end
end
end,
-- Instruction to apply a specific modID to any Items within the finalized search results
["modID"] = function(finalized, searchResults, o, cmd, modID)
FinalizeModID = modID
end,
-- Instruction to apply the modID from the Source object to any Items within the finalized search results
["myModID"] = function(finalized, searchResults, o)
FinalizeModID = o.modID
end,
-- Instruction to apply a specific modID to any Items within the finalized search results
["usemodID"] = function(finalized, searchResults, o, cmd, modID)
SelectMod = GetGroupItemIDWithModID(nil, nil, modID)
end,
-- Instruction to apply the modID from the Source object to any Items within the finalized search results
["usemyModID"] = function(finalized, searchResults, o)
SelectMod = GetGroupItemIDWithModID(nil, nil, o.modID)
end,
-- Instruction to use the modID from the Source object to filter matching modID on any Items within the finalized search results
["whereMyModID"] = function(finalized, searchResults, o)
local modID = o.modID
for k=#searchResults,1,-1 do
local result = searchResults[k];
if not result.modID or result.modID ~= modID then
tremove(searchResults, k);
end
end
end,
-- Instruction to perform an immediate 'FillGroups' against the objects in the finalized set prior to returning the results
-- or to fill the groups currently within the searchResults at this step
["groupfill"] = function(finalized, searchResults, o, cmd, onCurrent)
if onCurrent then
if #searchResults == 0 then return end
local orig = CloneArray(searchResults);
wipe(searchResults);
local Fill = app.FillGroups
local result
for k=1,#orig do
result = CreateObject(orig[k])
Fill(result)
searchResults[#searchResults + 1] = result
end
else
FillFinalized = true
end
end,
};
-- Replace achievementy_criteria function if criteria API doesn't exist
if GetAchievementNumCriteria then
local GetAchievementCriteriaInfo = _G.GetAchievementCriteriaInfo;
-- Instruction to query all criteria of an Achievement via the in-game APIs and generate Criteria data into the most-accurate Sources
ResolveFunctions.achievement_criteria = function(finalized, searchResults, o)
-- Instruction to select the criteria provided by the achievement this is attached to. (maybe build this into achievements?)
local achievementID = o.achievementID;
if not achievementID then
app.PrintDebug("'achievement_criteria' used on a non-Achievement group")
return;
end
local _, criteriaType, _, _, reqQuantity, _, _, assetID, _, _, criteriaObject, uniqueID
---@diagnostic disable-next-line: redundant-parameter
for criteriaID=1,GetAchievementNumCriteria(achievementID, true),1 do
---@diagnostic disable-next-line: redundant-parameter
_, criteriaType, _, _, reqQuantity, _, _, assetID, _, uniqueID = GetAchievementCriteriaInfo(achievementID, criteriaID, true);
if not uniqueID or uniqueID <= 0 then uniqueID = criteriaID; end
criteriaObject = app.CreateAchievementCriteria(uniqueID, {["achievementID"] = achievementID}, true);
-- criteriaType ref: https://warcraft.wiki.gg/wiki/API_GetAchievementCriteriaInfo
-- Quest source
if criteriaType == 27 -- Completing a quest
then
local quests = SearchForField("questID", assetID)
if #quests > 0 then
for _,c in ipairs(quests) do
-- criteria inherit their achievement data ONLY when the achievement data is actually referenced... this is required for proper caching
NestObject(c, criteriaObject);
AssignChildren(c);
CacheFields(criteriaObject);
app.DirectGroupUpdate(c);
criteriaObject = app.CreateAchievementCriteria(uniqueID, {["achievementID"] = achievementID}, true);
-- app.PrintDebug("Add-Crit",achievementID,uniqueID,"=>",c.hash)
end
-- added to the quest(s) groups, not added to achievement
criteriaObject = nil;
else
app.print("'achievement_criteria' Quest type missing Quest Source group!","Quest",assetID,app:Linkify("Achievement #"..achievementID,app.Colors.ChatLink,"search:achievementID:"..achievementID))
end
-- NPC source
elseif criteriaType == 0 -- Monster kill
then
-- app.PrintDebug("NPC Kill Criteria",assetID)
local c = SearchForObject("npcID", assetID, "field")
if c then
-- criteria inherit their achievement data ONLY when the achievement data is actually referenced... this is required for proper caching
NestObject(c, criteriaObject);
AssignChildren(c);
CacheFields(criteriaObject);
app.DirectGroupUpdate(c);
-- app.PrintDebug("Add-Crit",achievementID,uniqueID,"=>",c.hash)
-- added to the npc group, not added to achievement
criteriaObject = nil;
elseif assetID and assetID > 0 then
app.print("'achievement_criteria' NPC type missing NPC Source group!","NPC",assetID,app:Linkify("Achievement #"..achievementID,app.Colors.ChatLink,"search:achievementID:"..achievementID))
criteriaObject.crs = { assetID };
end
-- Items
elseif criteriaType == 36 -- Acquiring items (soulbound)
or criteriaType == 41 -- Eating or drinking a specific item
or criteriaType == 42 -- Fishing things up
or criteriaType == 57 -- Having items (tabards and legendaries)
then
criteriaObject.providers = {{ "i", assetID }};
-- Currency
elseif criteriaType == 12 -- Collecting currency
then
criteriaObject.cost = {{ "c", assetID, reqQuantity }};
-- Ignored
elseif criteriaType == 29 -- Casting a spell (often crafting)
or criteriaType == 43 -- Exploration
or criteriaType == 52 -- Killing specific classes of player
or criteriaType == 53 -- Kill-a-given-race (TODO?)
or criteriaType == 54 -- Using emotes on targets
or criteriaType == 69 -- Buff Gained
or criteriaType == 110 -- Casting spells on specific target
then
-- nothing to do here
else
--app.print("Unhandled Criteria Type", criteriaType, assetID, achievementID);
-- app.PrintDebug("Collecting currency",criteriaString, criteriaType, completed, quantity, reqQuantity, charName, flags, assetID, quantityString, uniqueID)
end
-- Criteria was not Sourced, so return it in search results
if criteriaObject then
CacheFields(criteriaObject);
-- this criteria object may have been turned into a cost via costs/providers assignment, so make sure we update those respective costs via the Cost Runner
-- if settings are changed while this is running, it's ok because it refreshes costs from the cache
app.HandleEvent("OnSearchResultUpdate", criteriaObject)
tinsert(searchResults, criteriaObject);
end
end
end
end
-- Subroutine Logic Cache
local SubroutineCache = {
["pvp_gear_base"] = function(finalized, searchResults, o, cmd, _, headerID1, headerID2)
local select, find = ResolveFunctions.select, ResolveFunctions.find
select(finalized, searchResults, o, "select", "headerID", headerID1); -- Select the Season header
if headerID2 then
find(finalized, searchResults, o, "find", "headerID", headerID2); -- Find the Set header
end
end,
["pvp_gear_faction_base"] = function(finalized, searchResults, o, cmd, _, headerID1, headerID2, headerID3)
local select, find = ResolveFunctions.select, ResolveFunctions.find
select(finalized, searchResults, o, "select", "headerID", headerID1); -- Select the Season header
find(finalized, searchResults, o, "find", "headerID", headerID2); -- Select the Faction header
find(finalized, searchResults, o, "find", "headerID", headerID3); -- Select the Set header
end,
-- Set Gear
["pvp_set_ensemble"] = function(finalized, searchResults, o, cmd, _, headerID1, headerID2, classID)
local select, find, extract = ResolveFunctions.select, ResolveFunctions.find, ResolveFunctions.extract
select(finalized, searchResults, o, "select", "headerID", headerID1); -- Select the Season header
find(finalized, searchResults, o, "find", "headerID", headerID2); -- Select the Set header
find(finalized, searchResults, o, "find", "classID", classID); -- Select the class header
extract(finalized, searchResults, o, "extract", "sourceID"); -- Extract all Items with a SourceID
end,
["pvp_set_faction_ensemble"] = function(finalized, searchResults, o, cmd, _, headerID1, headerID2, headerID3, classID)
local select, find, extract = ResolveFunctions.select, ResolveFunctions.find, ResolveFunctions.extract
select(finalized, searchResults, o, "select", "headerID", headerID1); -- Select the Season header
find(finalized, searchResults, o, "find", "headerID", headerID2); -- Select the Faction header
find(finalized, searchResults, o, "find", "headerID", headerID3); -- Select the Set header
find(finalized, searchResults, o, "find", "classID", classID); -- Select the class header
extract(finalized, searchResults, o, "extract", "sourceID"); -- Extract all Items with a SourceID
end,
-- Weapons
["pvp_weapons_ensemble"] = function(finalized, searchResults, o, cmd, _, headerID1, headerID2)
local select, find, extract = ResolveFunctions.select, ResolveFunctions.find, ResolveFunctions.extract
select(finalized, searchResults, o, "select", "headerID", headerID1); -- Select the Season header
find(finalized, searchResults, o, "find", "headerID", headerID2); -- Select the Set header
find(finalized, searchResults, o, "find", "headerID", app.HeaderConstants.WEAPONS); -- Select the "Weapons" header.
extract(finalized, searchResults, o, "extract", "sourceID"); -- Extract all Items with a SourceID
end,
["pvp_weapons_faction_ensemble"] = function(finalized, searchResults, o, cmd, _, headerID1, headerID2, headerID3)
local select, find, extract = ResolveFunctions.select, ResolveFunctions.find, ResolveFunctions.extract
select(finalized, searchResults, o, "select", "headerID", headerID1); -- Select the Season header
find(finalized, searchResults, o, "find", "headerID", headerID2); -- Select the Faction header
find(finalized, searchResults, o, "find", "headerID", headerID3); -- Select the Set header
find(finalized, searchResults, o, "find", "headerID", app.HeaderConstants.WEAPONS); -- Select the "Weapons" header.
extract(finalized, searchResults, o, "extract", "sourceID"); -- Extract all Items with a SourceID
end,
-- Common Northrend/Cataclysm Recipes Vendor
["common_recipes_vendor"] = function(finalized, searchResults, o, cmd, npcID)
local select, pop, is, exclude = ResolveFunctions.select, ResolveFunctions.pop, ResolveFunctions.is, ResolveFunctions.exclude;
select(finalized, searchResults, o, "select", "npcID", npcID); -- Main Vendor
pop(finalized, searchResults); -- Remove Main Vendor and push his children into the processing queue.
is(finalized, searchResults, o, "is", "itemID"); -- Only Items
-- Exclude items specific to certain vendors
exclude(finalized, searchResults, o, "exclude", "itemID",
-- Borya <Tailoring Supplies> Cataclysm Tailoring
6270, -- Pattern: Blue Linen Vest
6274, -- Pattern: Blue Overalls
10314, -- Pattern: Lavender Mageweave Shirt
10317, -- Pattern: Pink Mageweave Shirt
5772, -- Pattern: Red Woolen Bag
-- Sumi <Blacksmithing Supplies> Cataclysm Blacksmithing
12162, -- Plans: Hardened Iron Shortsword
-- Tamar <Leatherworking Supplies> Cataclysm Leatherworking
18731, -- Pattern: Heavy Leather Ball
-- Kithas <Enchanting Supplies> Cataclysm Enchanting
6349, -- Formula: Enchant 2H Weapon - Lesser Intellect
20753, -- Formula: Lesser Wizard Oil
20752, -- Formula: Minor Mana Oil
20758, -- Formula: Minor Wizard Oil
22307, -- Pattern: Enchanted Mageweave Pouch
-- Marith Lazuria <Jewelcrafting Supplies> Cataclysm Jewelcrafting
-- Shazdar <Sous Chef> Cataclysm Cooking
-- Tiffany Cartier <Jewelcrafting Supplies> Northrend Jewelcrafting
-- Timothy Jones <Jewelcrafting Trainer> Northrend Jewelcrafting
0); -- 0 allows the trailing comma on previous itemIDs for cleanliness
end,
["common_vendor"] = function(finalized, searchResults, o, cmd, npcID)
local select, pop, is = ResolveFunctions.select, ResolveFunctions.pop, ResolveFunctions.is;
select(finalized, searchResults, o, "select", "npcID", npcID); -- Main Vendor
pop(finalized, searchResults); -- Remove Main Vendor and push his children into the processing queue.
end,
-- TW Instance
["tw_instance"] = function(finalized, searchResults, o, cmd, instanceID)
local select, pop, whereany, push, finalize = ResolveFunctions.select, ResolveFunctions.pop, ResolveFunctions.whereany, ResolveFunctions.push, ResolveFunctions.finalize;
select(finalized, searchResults, o, "select", "itemID", 133543); -- Infinite Timereaver
push(finalized, searchResults, o, "push", "headerID", app.HeaderConstants.COMMON_BOSS_DROPS); -- Push into 'Common Boss Drops' header
finalize(finalized, searchResults); -- capture current results
select(finalized, searchResults, o, "select", "instanceID", instanceID); -- select this instance
whereany(finalized, searchResults, o, "whereany", "e", unpack(app.TW_EventIDs or app.EmptyTable) ); -- Select any TIMEWALKING eventID
if #searchResults > 0 then o.e = searchResults[1].e; end
pop(finalized, searchResults); -- pop the instance header
end,
["instance_tier"] = function(finalized, searchResults, o, cmd, instanceID, difficultyID, classID)
local select, pop, where, extract, invtype =
ResolveFunctions.select,
ResolveFunctions.pop,
ResolveFunctions.where,
ResolveFunctions.extract,
ResolveFunctions.invtype;
-- Select the Instance & pop out all results
select(finalized, searchResults, o, "select", "instanceID", instanceID);
pop(finalized, searchResults);
-- If there's a Difficulty, filter by Difficulty
if difficultyID then
where(finalized, searchResults, o, "where", "difficultyID", difficultyID);
pop(finalized, searchResults);
end
-- Extract the Items that have a Class restriction
extract(finalized, searchResults, o, "extract", "c");
local orig;
-- Pop out any actual Tier Tokens
if #searchResults > 0 then
orig = CloneArray(searchResults);
end
wipe(searchResults);
if orig then
for _,o in ipairs(orig) do
if not o.f then
if o.g then
-- no filter Item with sub-groups
ArrayAppend(searchResults, o.g)
else
-- no filter Item without sub-groups, keep it directly in case it is a cost for the actual Tier pieces
tinsert(searchResults, o);
end
end
end
end
-- Exclude anything that isn't a Tier slot
invtype(finalized, searchResults, o, "invtype",
"INVTYPE_HEAD",
"INVTYPE_SHOULDER",
"INVTYPE_CHEST", "INVTYPE_ROBE",
"INVTYPE_LEGS",
"INVTYPE_HAND"
);
-- If there's a Class, filter by Class
if classID then
if #searchResults > 0 then
orig = CloneArray(searchResults);
end
wipe(searchResults);
local c;
if orig then
for _,o in ipairs(orig) do
c = o.c;
if c and ContainsAnyValue(c, classID) then
tinsert(searchResults, o);
end
end
end
end
end,
};
app.RegisterSymlinkResolveFunction = function(name, method)
ResolveFunctions[name] = method;
end
app.RegisterSymlinkSubroutine = function(name, method)
-- NOTE: This passes a function to call immediately and cache used resolve functions.
SubroutineCache[name] = method(ResolveFunctions);
end
-- TODO: when symlink becomes a stand-alone Module, it should work like this
-- Don't expect every caller to know what event is proper for registering a symlink
-- Plus we need to ensure RegisterSymlinkResolveFunction handles additions prior to all RegisterSymlinkSubroutine
-- Since we won't know the order of the callers assigning the handlers
-- local RegisteredSymlinkSubroutines, RegisteredResolveFunctions = {}
-- app.RegisterSymlinkResolveFunction = function(name, method)
-- RegisteredResolveFunctions[name] = method
-- end
-- app.RegisterSymlinkSubroutine = function(name, method)
-- -- NOTE: This stores a function to call immediately OnLoad and cache used resolve functions.
-- RegisteredSymlinkSubroutines[name] = method
-- end
-- app.AddEventHandler("OnLoad", function()
-- for name,method in pairs(RegisteredResolveFunctions) do
-- ResolveFunctions[name] = method
-- end
-- for name,method in pairs(RegisteredSymlinkSubroutines) do
-- SubroutineCache[name] = method(ResolveFunctions)
-- end
-- end);
-- Instruction to perform a specific subroutine using provided input values
ResolveFunctions.sub = function(finalized, searchResults, o, cmd, sub, ...)
local subroutine = SubroutineCache[sub];
-- new logic: no metatable cloning, no table creation for sub-commands
if subroutine then
-- app.PrintDebug("sub",o.hash,sub,...)
subroutine(finalized, searchResults, o, cmd, ...);
-- each subroutine result is finalized after being processed
ResolveFunctions.finalize(finalized, searchResults);
return;
end
app.print("Could not find subroutine", sub);
end;
local NonSelectCommands = {
finalize = true,
achievement_criteria = true,
sub = true,
myModID = true,
modID = true,
usemyModID = true,
usemodID = true,
}
local HandleCommands = app.Debugging and function(finalized, searchResults, o, oSym)
local cmd, cmdFunc
local debug = true
for _,sym in ipairs(oSym) do
cmd = sym[1];
cmdFunc = ResolveFunctions[cmd];
-- app.PrintDebug("sym: '",cmd,"' for",o.hash,"with:",unpack(sym))
if cmdFunc then
cmdFunc(finalized, searchResults, o, unpack(sym));
if debug and #searchResults == 0 and not NonSelectCommands[cmd] then
app.PrintDebug(Colorize("Symlink command with no results for: "..app:SearchLink(o), app.Colors.ChatLinkError),"@",_,unpack(sym))
app.PrintTable(oSym)
debug = false
end
else
app.print("Unknown symlink command",cmd);
end
-- app.PrintDebug("Finalized",#finalized,"Results",#searchResults,"from",o.hash,"with:",unpack(sym))
end
end or function(finalized, searchResults, o, oSym)
local cmd, cmdFunc
for _,sym in ipairs(oSym) do
cmd = sym[1];
cmdFunc = ResolveFunctions[cmd];
if cmdFunc then
cmdFunc(finalized, searchResults, o, unpack(sym));
else
app.print("Unknown symlink command",cmd);
end
end
end
local ResolveCache = {};
ResolveSymbolicLink = function(o, refonly)
local oSym = o.sym
if not oSym then return end
local oHash, oKey = o.hash, o.key;
if o.resolved or (oKey and app.ThingKeys[oKey] and ResolveCache[oHash]) then
if refonly then
return o.resolved or ResolveCache[oHash]
end
-- app.PrintDebug(o.resolved and "Object Resolve" or "Cache Resolve",oHash,#(o.resolved or ResolveCache[oHash]))
local cloned = {};
MergeObjects(cloned, o.resolved or ResolveCache[oHash], true);
return cloned;
end
FinalizeModID = nil;
PruneFinalized = nil;
FillFinalized = nil
-- app.PrintDebug("Fresh Resolve:",oHash)
local searchResults, finalized = {}, {};
HandleCommands(finalized, searchResults, o, oSym)
-- Verify the final result is finalized
ResolveFunctions.finalize(finalized, searchResults);
-- app.PrintDebug("Forced Finalize",oKey,oKey and o[oKey],#finalized)
-- If we had any finalized search results, then clone all the records, store the results, and return them
if #finalized == 0 then
-- app.PrintDebug("Symbolic Link for ", oKey, " ",oKey and o[oKey], " contained no values after filtering.")
return
end
local cloned = {};
-- app.PrintDebug("Symbolic Link for", oKey,oKey and o[oKey], "contains", #cloned, "values after filtering.")
local sHash, clone
local Fill = app.FillGroups
for i=1,#finalized do
clone = finalized[i]
-- if somehow the symlink pulls in the same item as used as the source of the symlink, notify in chat and clear any symlink on it
sHash = clone.hash;
if clone == o or (sHash and sHash == oHash) then
app.print("Symlink group pulled itself into finalized results!",oHash,o.key,o.modItemID,o.link or o.text,i,FinalizeModID)
else
clone = CreateObject(clone)
cloned[#cloned + 1] = clone
-- Apply any modID if necessary
if FinalizeModID and clone.itemID and clone.modID ~= FinalizeModID then
clone.modID = FinalizeModID;
-- refresh the item group since certain metadata may be different now
app.RefreshItemGroup(clone)
end
if PruneFinalized then
for _,field in ipairs(PruneFinalized) do
clone[field] = nil
end
end
if FillFinalized then
-- app.PrintDebug("Fill",clone.hash)
Fill(clone)
clone.skipFill = 2
end
-- in symlinking a Thing to another Source, we are effectively declaring that it is Sourced within this Source, for the specific scope
clone.symParent = clone.parent
clone.sourceParent = nil;
clone.parent = nil;
end
end
if oKey and app.ThingKeys[oKey] then
-- global resolve cache if it's a 'Thing'
-- app.PrintDebug("Thing Results",oHash,#cloned)
ResolveCache[oHash] = cloned;
elseif oKey ~= false then
-- otherwise can store it in the object itself (like a header from the Main list with symlink), if it's not specifically a pseudo-symlink resolve group
o.resolved = cloned;
-- app.PrintDebug("Object Results",oHash,#cloned)
end
return cloned;
end
app.ResolveSymbolicLink = ResolveSymbolicLink
local function ResolveSymlinkGroupAsync(group)
-- app.PrintDebug("RSGa",group.hash)
local groups = ResolveSymbolicLink(group);
group.sym = nil;
if groups then
PriorityNestObjects(group, groups, nil, app.RecursiveCharacterRequirementsFilter, app.RecursiveGroupRequirementsFilter);
-- app.PrintDebug("RSGa",group.g and #group.g,group.hash)
-- newly added group data needs to be checked again for further content to fill, since it will not have been recursively checked
-- on the initial pass due to the async nature
app.FillGroups(group);
AssignChildren(group);
-- auto-expand the symlink group
ExpandGroupsRecursively(group, true);
app.DirectGroupUpdate(group);
end
end
-- Fills the symlinks within a group by using an 'async' process to spread the filler function over multiple game frames to reduce stutter or apparent lag
-- NOTE: ONLY performs the symlink for 'achievement_criteria'
app.FillAchievementCriteriaAsync = function(o)
local sym = o.sym
if not sym then return end
local sym = sym[1][1]
if sym ~= "achievement_criteria" then return end
-- app.PrintDebug("resolve achievement_criteria",o.hash)
app.FillRunner.Run(ResolveSymlinkGroupAsync, o);
end
end -- Symlink Lib
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) };
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 SummarizeShowForActiveRowKeys
local function AddContainsData(group, tooltipInfo)
local key = group.key
-- only show Contains on Things
if not app.ThingKeys[key] or (app.ActiveRowReference and not SummarizeShowForActiveRowKeys[key]) then return end
local id = group[key]
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
if app.Settings:GetTooltipSetting("Currencies") then
local currencyCount = app.CalculateTotalCosts(group, id)
if currencyCount > 0 then
tinsert(tooltipInfo, { left = L.CURRENCY_NEEDED_TO_BUY, right = formatNumericWithCommas(currencyCount) });
end
end
end
return working
end
app.AddEventHandler("OnLoad", function()
SummarizeShowForActiveRowKeys = app.CloneDictionary(app.ThingKeys, {
-- Specific keys which we don't want to list Contains data on row reference tooltips but are considered Things
npcID = false,
creatureID = false,
encounterID = false,
explorationID = false,
})
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
-- 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);
group.isBaseSearchResult = true;
return group, group.working
end
app.GetCachedSearchResults = function(method, paramA, paramB, options)
if options and options.IgnoreCache then
return GetSearchResults(method, paramA, paramB, options)
end
return app.GetCachedData(paramB and paramA..":"..paramB or paramA, GetSearchResults, method, paramA, paramB, options);
end
local IsComplete = app.IsComplete
local function CalculateGroupsCostAmount(g, costID, includedHashes)
local o, subg, subcost, c
local cost = 0
for i=1,#g do
o = g[i]
subcost = o.visible and not IsComplete(o) and o.cost or nil
if not includedHashes[o.hash] and subcost and type(subcost) == "table" then
for j=1,#subcost do
c = subcost[j]
if c[2] == costID then
includedHashes[o.hash] = true
cost = cost + c[3];
break
end
end
end
subg = o.g
if subg then
cost = cost + CalculateGroupsCostAmount(subg, costID, includedHashes)
end
end
return cost
end
-- Returns the total amount of 'costID' for all non-collected Things within the group (not including the group itself)
app.CalculateTotalCosts = function(group, costID)
-- app.PrintDebug("CalculateTotalCosts",group.hash,costID)
local g = group and group.g
local cost = g and CalculateGroupsCostAmount(g, costID, {}) or 0
-- app.PrintDebug("CalculateTotalCosts",group.hash,costID,"=>",cost)
return cost
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,
npcID = true,
conduitID = true,
creatureID = 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,
encounterID = true,
artifactID = true,
azeriteessenceID = true,
followerID = true,
factionID = true,
explorationID = true,
titleID = true,
campsiteID = true,
achievementID = true, -- special handling
criteriaID = true, -- special handling
};
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
-- 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
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
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 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 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 = {},
OnClick = app.UI.OnClick.IgnoreRightClick,
})
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.CreateRawText(TRANSMOG_SOURCE_4);
db.g = app.Categories.WorldDrops;
db.isWorldDropCategory = true;
db.icon = app.asset("Category_WorldDrops");
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.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
-----------------------------------------
-- 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
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 = CHARACTER.." "..UNLOCK.."s",
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")
}),
-- 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
do -- Search Response Logic
end -- Search Response Logic
-- 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
customWindowUpdates.AchievementHarvester = function(self, ...)
-- /run AllTheThings:GetWindow("AchievementHarvester"):Toggle();
if self:IsVisible() then
if not self.initialized then
self.doesOwnUpdate = true;
self.initialized = true;
self.Limit = 45000; -- MissingAchievements:11.0.0.54774 (maximum achievementID)
self.PartitionSize = 5000;
local CleanUpHarvests = function()
local g, partition, pg, pgcount, refresh = self.data.g, nil, nil, nil, nil;
local count = g and #g or 0;
if count > 0 then
for p=count,1,-1 do
partition = g[p];
if partition.g and partition.expanded then
refresh = true;
pg = partition.g;
pgcount = #pg;
-- print("UpdateDone.Partition",partition.text,pgcount)
if pgcount > 0 then
for i=pgcount,1,-1 do
if pg[i].collected then
-- item harvested, so remove it
-- print("remove",pg[i].text)
tremove(pg, i);
end
end
else
-- empty partition, so remove it
tremove(g, p);
end
end
end
if refresh then
-- refresh the window again
self:BaseUpdate();
else
-- otherwise stop until a group is expanded again
self.UpdateDone = nil;
end
end
end;
-- add a bunch of raw, delay-loaded items in order into the window
local groupCount = math_floor(self.Limit / self.PartitionSize);
local g, overrides = {}, {visible=true};
local partition, partitionStart, partitionGroups;
local dlo, obj = app.DelayLoadedObject, app.CreateAchievementHarvester;
for j=0,groupCount,1 do
partitionStart = j * self.PartitionSize;
partitionGroups = {};
-- define a sub-group for a range of quests
partition = app.CreateRawText(tostring(partitionStart + 1).."+", {
["icon"] = app.asset("Interface_Quest_header"),
["visible"] = true,
["OnClick"] = function(row, button)
-- assign the clean up method now that the group was clicked
self.UpdateDone = CleanUpHarvests;
-- no return so that it acts like a normal row
end,
["g"] = partitionGroups,
})
for i=1,self.PartitionSize,1 do
tinsert(partitionGroups, dlo(obj, "text", overrides, partitionStart + i));
end
tinsert(g, partition);
end
local db = app.CreateRawText("Achievement Harvester", {
g = g,
icon = app.asset("WindowIcon_RaidAssistant"),
description = "This is a contribution debug tool. NOT intended to be used by the majority of the player base.\n\nExpand a group to harvest the 1,000 Achievements within that range.",
visible = true,
back = 1,
})
self:SetData(db);
end
self:BaseUpdate(true);
end
end;
local function RoundNumber(number, decimalPlaces)
local ret;
if number < 60 then
ret = number .. " second(s)";
else
ret = (("%%.%df"):format(decimalPlaces)):format(number/60) .. " minute(s)";
end
return ret;
end
customWindowUpdates.AuctionData = function(self)
if not self.initialized then
local C_AuctionHouse_ReplicateItems = C_AuctionHouse.ReplicateItems;
self.initialized = true;
self:SetData(app.CreateRawText("Auction Module", {
visible = true,
back = 1,
icon = 133784,
description = "This is a debug window for all of the auction data that was returned. Turn on 'Account Mode' to show items usable on any character on your account!",
options = {
app.CreateRawText("Wipe Scan Data", {
["icon"] = 2065582,
["description"] = "Click this button to wipe out all of the previous scan data.",
["visible"] = true,
["priority"] = -4,
["OnClick"] = function()
if AllTheThingsAuctionData then
local window = app:GetWindow("AuctionData");
wipe(AllTheThingsAuctionData);
wipe(window.data.g);
for i,option in ipairs(window.data.options) do
tinsert(window.data.g, option);
end
window:Update();
end
end,
['OnUpdate'] = function(data)
local window = app:GetWindow("AuctionData");
data.visible = #window.data.g > #window.data.options;
return true;
end,
}),
app.CreateRawText("Scan or Load Last Save", {
["icon"] = 1100023,
["description"] = "Click this button to perform a full scan of the auction house or load the last scan conducted within 15 minutes. The game may or may not freeze depending on the size of your auction house.\n\nData should populate automatically.",
["visible"] = true,
["priority"] = -3,
["OnClick"] = function()
if AucAdvanced and AucAdvanced.API then AucAdvanced.API.CompatibilityMode(1, ""); end
-- Only allow a scan once every 15 minutes.
local cooldown = self.AuctionScanCooldownTime or 0;
local now = time();
if cooldown - now < 0 then
self.AuctionScanCooldownTime = now + 900;
app.AuctionFrame:RegisterEvent("REPLICATE_ITEM_LIST_UPDATE");
C_AuctionHouse_ReplicateItems();
else
app.print(": Throttled scan! Please wait " .. RoundNumber(cooldown - now, 0) .. " before running another. Loading last save instead...");
StartCoroutine("ProcessAuctionData", app.ProcessAuctionData, 1);
end
end,
['OnUpdate'] = app.AlwaysShowUpdate,
}),
app.CreateRawText("Toggle Debug Mode", {
["icon"] = 134521,
["description"] = "Click this button to toggle debug mode to show everything regardless of filters!",
["visible"] = true,
["priority"] = -2,
["OnClick"] = function()
app.Settings:ToggleDebugMode();
end,
['OnUpdate'] = function(data)
data.visible = true;
if app.MODE_DEBUG then
-- Novaplane made me do it
data.trackable = true;
data.saved = true;
else
data.trackable = nil;
data.saved = nil;
end
return true;
end,
}),
app.CreateRawText("Toggle Account Mode", {
["icon"] = 413583,
["description"] = "Turn this setting on if you want to track all of the Things for all of your characters regardless of class and race filters.\n\nUnobtainable filters still apply.",
["visible"] = true,
["priority"] = -1,
["OnClick"] = function()
app.Settings:ToggleAccountMode();
end,
['OnUpdate'] = function(data)
data.visible = true;
if app.MODE_ACCOUNT then
data.trackable = true;
data.saved = true;
else
data.trackable = nil;
data.saved = nil;
end
return true;
end,
}),
app.CreateRawText("Toggle Faction Mode", {
["icon"] = 134932,
["description"] = "Click this button to toggle faction mode to show everything for your faction!",
["visible"] = true,
["OnClick"] = function()
app.Settings:ToggleFactionMode();
end,
['OnUpdate'] = function(data)
if app.MODE_DEBUG or not app.MODE_ACCOUNT then
data.visible = false;
else
data.visible = true;
if app.Settings:Get("FactionMode") then
data.trackable = true;
data.saved = true;
else
data.trackable = nil;
data.saved = nil;
end
end
return true;
end,
}),
app.CreateRawText("Toggle Unobtainable Items", {
["icon"] = 135767,
["description"] = "Click this button to see currently unobtainable items in the auction data.",
["visible"] = true,
["priority"] = 0,
["OnClick"] = function()
local show = not app.Settings:GetValue("Unobtainable", 7);
app.Settings:SetValue("Unobtainable", 7, show);
for k,v in pairs(L.PHASES) do
if v.state < 4 then
if k ~= 7 then
app.Settings:SetValue("Unobtainable", k, show);
end
end
end
app.Settings:Refresh();
-- TODO: use events
-- app:RefreshData();
end,
['OnUpdate'] = function(data)
data.visible = true;
if app.Settings:GetValue("Unobtainable", 7) then
data.trackable = true;
data.saved = true;
else
data.trackable = nil;
data.saved = nil;
end
return true;
end,
}),
},
["g"] = {}
}))
for i,option in ipairs(self.data.options) do
tinsert(self.data.g, option);
end
end
-- Update the window and all of its row data
self.data.progress = 0;
self.data.total = 0;
self.data.indent = 0;
self.data.back = 1;
AssignChildren(self.data);
app.TopLevelUpdateGroup(self.data);
self.data.visible = true;
self:BaseUpdate(true);
end;
customWindowUpdates.Bounty = function(self, force, got)
if not self.initialized then
self.initialized = true;
local autoOpen = app.CreateToggle("openAuto", {
["text"] = L.OPEN_AUTOMATICALLY,
["icon"] = 134327,
["description"] = L.OPEN_AUTOMATICALLY_DESC,
["visible"] = true,
["OnUpdate"] = app.AlwaysShowUpdate,
["OnClickHandler"] = function(toggle)
app.Settings:SetTooltipSetting("Auto:BountyList", toggle);
self:BaseUpdate(true, got);
end,
});
local header = app.CreateCustomHeader(app.HeaderConstants.UI_BOUNTY_WINDOW, {
['visible'] = true,
["g"] = {
autoOpen,
},
});
-- add bounty content
-- TODO: This window pulls its data manually, there should be a key for bounty.
-- Update this when we merge over Classic's extended window logic.
-- NOTE: Everything we want is current marked with a u value of 45, so why not just pull that in? :)
NestObjects(header, {
app.CreateCustomHeader(app.HeaderConstants.RARES, {
app.CreateNPC(87622, { -- Ogom the Mangler
['g'] = {
app.CreateItemSource(67041, 119366),
},
}),
}),
app.CreateCustomHeader(app.HeaderConstants.ZONE_DROPS, {
["description"] = "These items were likely not readded with 10.1.7 or their source is currently unknown.",
["g"] = {
app.CreateItemSource(85, 778), -- Kobold Excavation Pick
app.CreateItemSource(1932, 4951), -- Squeeler's Belt
app.CreateItem(1462), -- Ring of the Shadow
app.CreateItem(1404), -- Tidal Charm
},
}),
});
self:SetData(header);
AssignChildren(self.data);
self.rawData = {};
local function RefreshBounties()
if #self.data.g > 1 and app.Settings:GetTooltipSetting("Auto:BountyList") then
autoOpen.saved = true;
self:SetVisible(true);
end
end
self:SetScript("OnEvent", function(self, e, ...)
if select(1, ...) == appName then
self:UnregisterEvent("ADDON_LOADED");
Callback(RefreshBounties);
end
end);
self:RegisterEvent("ADDON_LOADED");
end
if self:IsVisible() then
-- Update the window and all of its row data
self.data.back = 1;
self:BaseUpdate(true, got);
end
end;
customWindowUpdates.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:BaseUpdate(force);
end
end;
customWindowUpdates.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
header = CreateWrapVisualHeader(header, {group})
header.SortType = "name"
return header
else
return { g = { group }, ["collectible"] = false, SortType = "name" };
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, difficultyID, nextParent, headerID, isInInstance
local rootGroups, mapGroups = {}, {};
local function TryExpandCurrentDifficulty()
if not app.Settings:GetTooltipSetting("Expand:Difficulty") then return end
local difficultyID = app.GetCurrentDifficultyID()
if difficultyID == 0 or not header.g then return end
local expanded
for _,row in ipairs(header.g) do
if row.difficultyID or row.difficulties then
if (row.difficultyID or -1) == difficultyID or (row.difficulties and containsValue(row.difficulties, difficultyID)) then
if not row.expanded then
ExpandGroupsRecursively(row, true, true);
expanded = true;
end
elseif row.expanded then
ExpandGroupsRecursively(row, false, true);
end
-- Zone Drops/Common Boss Drops should also be expanded within instances
-- elseif row.headerID == app.HeaderConstants.ZONE_DROPS or row.headerID == app.HeaderConstants.COMMON_BOSS_DROPS then
-- if not row.expanded then ExpandGroupsRecursively(row, true); expanded = true; end
end
end
-- No difficulty found to expand, so just expand everything in the list once it is built
if not expanded then
self.ExpandInfo = { Expand = true };
expanded = true;
end
return expanded
end
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)
TryExpandCurrentDifficulty()
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, without any results that have been cloned into Source Ignored groups or are under Unsorted
results = CleanInheritingGroups(SearchForField("mapID", mapID), "sourceIgnored");
-- 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 = CleanInheritingGroups(SearchForField("mapID", mapInfo.mapID), "sourceIgnored")
-- 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
for _,result in ipairs(results) do
if result.key == "mapID" and result.mapID == mapID then
rootMap = result
break;
end
end
if rootMap and rootMap.maps then
local subresults
for _,subMapID in ipairs(rootMap.maps) do
if subMapID ~= mapID then
subresults = CleanInheritingGroups(SearchForField("mapID", subMapID), "sourceIgnored")
-- app.PrintDebug("Adding Sub-Map Results:",subMapID,#subresults)
results = ArrayAppend(results, subresults)
end
end
end
-- Simplify the returned groups
groups = {};
wipe(rootGroups);
wipe(mapGroups);
header = { mapID = mapID, g = groups }
currentMaps[mapID] = true;
isInInstance = IsInInstance();
headerKeys = isInInstance and subGroupInstanceKeys or subGroupKeys;
-- split search results by whether they represent the 'root' of the minilist or some other mapped content
for _,group in ipairs(results) do
-- do not use any raw Source groups in the final list
group = CreateObject(group);
-- Instance/Map/Class/Header(of current map) groups are allowed as root of minilist
if (group.instanceID or (group.mapID and (group.key == "mapID" or (group.key == "headerID" and group.mapID == mapID))) or group.key == "classID")
-- and actually match this minilist...
-- only if this group mapID matches the minilist mapID directly or by maps
and (group.mapID == mapID or (group.maps and contains(group.maps, mapID))) then
rootGroups[#rootGroups + 1] = group
else
mapGroups[#mapGroups + 1] = group
end
end
-- first merge all root groups into the list
for _,group in ipairs(rootGroups) do
if group.maps then
for _,m in ipairs(group.maps) do
currentMaps[m] = true;
end
end
-- app.PrintDebug("Merge as Root",group.hash)
MergeProperties(header, group, true);
NestObjects(header, group.g);
end
-- then merge all mapped groups into the list
for _,group in ipairs(mapGroups) do
-- app.PrintDebug("Mapping:",app:SearchLink(group))
nested = nil;
-- Get the header chain for the group
nextParent = group.parent;
-- Cache the difficultyID, if there is one and we are in an actual instance where the group is being mapped
difficultyID = isInInstance and GetRelativeValue(nextParent, "difficultyID");
-- 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 _,hkey in ipairs(headerKeys) do
if nextParent[hkey] 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 difficultyID, then merge it into one.
if difficultyID then group = app.CreateDifficulty(difficultyID, { g = { group } }); end
-- app.PrintDebug("Merge as Mapped:",app:SearchLink(group))
MergeObject(groups, group);
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
header.g = nil;
MergeProperties(header, headerGroups[1], true);
NestObjects(header, headerGroups[1].g);
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);
-- if enabled, minimize rows based on difficulty
local expanded = TryExpandCurrentDifficulty()
self:BuildData();
-- check to expand groups after they have been built and updated
-- dont re-expand if the user has previously full-collapsed the minilist
-- need to force expand if so since the groups haven't been updated yet
if not expanded and not self.fullCollapsed then
self.ExpandInfo = { Expand = true };
end
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:BaseUpdate(force, got);
end
end;
customWindowUpdates.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:BaseUpdate(force);
end
end;
customWindowUpdates.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 = "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 = {
text = L.NEW_WITH_PATCH,
icon = app.asset("WindowIcon_RWP"),
description = L.NEW_WITH_PATCH_TOOLTIP,
visible = true,
back = 1,
g = CreateNWPWindow(),
};
self:SetData(NWPwindow);
self:BuildData();
end
if self:IsVisible() then
self:BaseUpdate(force);
end
end;
customWindowUpdates.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}
-- 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}
}
-- 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 = "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 = {
text = L.ADDED_WITH_PATCH,
icon = 135769,
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:BaseUpdate(force);
end
end;
customWindowUpdates.Prime = function(self, ...)
self:BaseUpdate(...);
-- 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
customWindowUpdates.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.BaseUpdate, 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.BaseUpdate, 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:BaseUpdate(true);
app.Modules.Filter.Set.Visible(visibleState)
end
end;
customWindowUpdates.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 = {
[12] = 1, -- Kalimdor
[13] = 1, -- Eastern Kingdoms
[101] = 1, -- Outland
[113] = 1, -- Northrend
[424] = 1, -- Pandaria
[948] = 1, -- The Maelstrom
[572] = 1, -- Draenor
[619] = 1, -- The Broken Isles
[905] = 1, -- Argus
[876] = 1, -- Kul'Tiras
[875] = 1, -- Zandalar
[1550] = 1, -- The Shadowlands
[1978] = 1, -- Dragon Isles
[2274] = 1, -- Khaz Algar
};
-- 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",
Dungeon = "instanceID",
Factions = "factionID",
-- Follower = "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,
Dungeon = function(o)
return not o.isRaid and (((o.total or 0) - (o.progress or 0)) > 0);
end,
-- Factions - default
-- Follower - 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.DUNGEON, app.asset("Difficulty_Normal"), L.DUNGEON_DESC, "Dungeon"),
AddRandomCategoryButton(L.FACTIONS, app.asset("Category_Factions"), L.FACTION_DESC, "Factions"),
-- missing locale values
-- AddRandomCategoryButton(L.HEADER_NAMES[app.HeaderConstants.FOLLOWERS], L.HEADER_ICONS[app.HeaderConstants.FOLLOWERS], L.FOLLOWER_DESC, "Follower"),
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
AssignChildren(self.data);
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;
AssignChildren(self.data);
self:BaseUpdate(true);
end
end;
customWindowUpdates.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("WindowIcon_RWP"),
["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:BaseUpdate(force);
end
end;
customWindowUpdates.Sync = function(self)
if self:IsVisible() then
if not self.initialized then
self.initialized = true;
local function OnRightButtonDeleteCharacter(row, button)
if button == "RightButton" then
app:ShowPopupDialog("CHARACTER DATA: " .. (row.ref.text or RETRIEVING_DATA) .. 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:BaseUpdate(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
customWindowUpdates.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:BaseUpdate(force);
app.Modules.Filter.Set.Visible(filterVisible);
end
end
customWindowUpdates.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:BaseUpdate(force or self.force, got);
self.force = nil;
end
end;
customWindowUpdates.WorldQuests = function(self, force, got)
-- localize some APIs
local C_TaskQuest_GetQuestsForPlayerByMapID = C_TaskQuest.GetQuestsOnMap;
local C_QuestLine_RequestQuestLinesForMap = C_QuestLine.RequestQuestLinesForMap;
local C_QuestLine_GetAvailableQuestLines = C_QuestLine.GetAvailableQuestLines;
local C_Map_GetMapChildrenInfo = C_Map.GetMapChildrenInfo;
local C_AreaPoiInfo_GetAreaPOISecondsLeft = C_AreaPoiInfo.GetAreaPOISecondsLeft;
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
-- any un-attached sub-zones
}
},
-- Shadowlands Continents
{
1550, -- Shadowlands
-- {}
},
-- BFA Continents
{
875, -- Zandalar
{
{ 863, 5969, { 54135, 54136 }}, -- Nazmir (Romp in the Swamp [H] / March on the Marsh [A])
{ 864, 5970, { 53885, 54134 }}, -- Voldun (Isolated Victory [H] / Many Fine Heroes [A])
{ 862, 5973, { 53883, 54138 }}, -- Zuldazar (Shores of Zuldazar [H] / Ritual Rampage [A])
}
},
{
876, -- Kul'Tiras
{
{ 896, 5964, { 54137, 53701 }}, -- Drustvar (In Every Dark Corner [H] / A Drust Cause [A])
{ 942, 5966, { 54132, 51982 }}, -- Stormsong Valley (A Horde of Heroes [H] / Storm's Rage [A])
{ 895, 5896, { 53939, 53711 }}, -- Tiragarde Sound (Breaching Boralus [H] / A Sound Defense [A])
}
},
{ 1355 }, -- Nazjatar
-- Legion Continents
{
619, -- Broken Isles
{
{ 627 }, -- Dalaran (not a Zone, so doesn't list automatically)
{ 630, 5175, { 47063 }}, -- Azsuna
{ 650, 5177, { 47063 }}, -- Highmountain
{ 634, 5178, { 47063 }}, -- Stormheim
{ 641, 5210, { 47063 }}, -- Val'Sharah
}
},
{ 905 }, -- Argus
-- WoD Continents
{ 572 }, -- Draenor
-- MoP Continents
{
424, -- Pandaria
{
{ 1530, 6489, { 56064 }}, -- Assault: The Black Empire
{ 1530, 6491, { 57728 }}, -- Assault: The Endless Swarm
{ 1530, 6490, { 57008 }}, -- Assault: The Warring Clans
},
},
-- Cataclysm Continents
{ 948 }, -- The Maelstrom
-- WotLK Continents
{ 113 }, -- Northrend
-- BC Continents
{ 101 }, -- Outland
-- Vanilla Continents
{
12, -- Kalimdor
{
{ 1527, 6486, { 57157 }}, -- Assault: The Black Empire
{ 1527, 6488, { 56308 }}, -- Assault: Aqir Unearthed
{ 1527, 6487, { 55350 }}, -- Assault: Amathet Advance
{ 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
-- 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);
-- 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)
-- 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);
-- Invasions
local mapIDPOIPairs = pair[2];
if mapIDPOIPairs then
for i,arr in ipairs(mapIDPOIPairs) do
-- Sub-Map with Quest information to track
if #arr >= 3 then
for j,questID in ipairs(arr[3]) do
if not IsQuestFlaggedCompleted(questID) then
local timeLeft = C_AreaPoiInfo_GetAreaPOISecondsLeft(arr[2]);
if timeLeft and timeLeft > 0 then
local questObject = GetPopulatedQuestObject(questID);
-- Custom time remaining based on the map POI since the quest itself does not indicate time remaining
questObject.timeRemaining = timeLeft;
local subMapObject = app.CreateMapWithStyle(arr[1]);
NestObject(subMapObject, questObject);
NestObject(mapObject, subMapObject);
end
end
end
else
-- Basic Sub-map
local subMap = app.CreateMapWithStyle(arr[1]);
-- Build top-level maps all the way down for the sub-map
self:BuildMapAndChildren(subMap);
NestObject(mapObject, subMap);
end
end
end
-- Merge everything for this map into the list
app.Sort(mapObject.g);
if mapObject.g then
-- Sort the sub-groups as well
for i,mapGrp in ipairs(mapObject.g) do
if mapGrp.mapID then
app.Sort(mapGrp.g);
end
end
end
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);
if mapObject.g then
-- Sort the sub-groups as well
for i,mapGrp in ipairs(mapObject.g) do
if mapGrp.mapID then
app.Sort(mapGrp.g);
end
end
end
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 groupFinder = { text = DUNGEONS_BUTTON, icon = app.asset("Category_GroupFinder") };
local gfg = {}
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 header = { dungeonID = dungeonID, text = name, description = description, lvl = { minRecLevel or 1, maxRecLevel }, OnUpdate = OnUpdateForLFGHeader}
local hg = {}
header.g = hg
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
tinsert(temp, CreateObject(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:BaseUpdate(force);
end
end;
end)();
-- Auction House Lib
(function()
local auctionFrame = CreateFrame("Frame");
app.AuctionFrame = auctionFrame;
app.ProcessAuctionData = function()
-- If we have no auction data, then simply return now.
if not AllTheThingsAuctionData then return end;
local count = 0;
for _ in pairs(AllTheThingsAuctionData) do count = count+1 end
if count < 1 then return end;
-- Search the ATT Database for information related to the auction links (items, species, etc)
local filterID;
local searchResultsByKey, searchResult, searchResults, key, keys, value, data = {}, nil, nil, nil, nil, nil, nil;
for k,v in pairs(AllTheThingsAuctionData) do
searchResults = app.SearchForLink(v.itemLink);
if searchResults then
if #searchResults > 0 then
searchResult = searchResults[1];
key = searchResult.key;
if key == "npcID" then
if searchResult.itemID then
key = "itemID";
end
elseif key == "spellID" then
local AuctionDataItemKeyOverrides = {
[92426] = "itemID", -- Sealed Tome of the Lost Legion
};
if AuctionDataItemKeyOverrides[searchResult.itemID] then
key = AuctionDataItemKeyOverrides[searchResult.itemID]
end
end
value = searchResult[key];
keys = searchResultsByKey[key];
-- Make sure that the key type is represented.
if not keys then
keys = {};
searchResultsByKey[key] = keys;
end
-- First time this key value was used.
data = keys[value];
if not data then
data = CreateObject(searchResult);
for i=2,#searchResults,1 do
MergeObject(data, CreateObject(searchResults[i]));
end
if data.key == "npcID" then app.CreateItem(data.itemID, data); end
data.auctions = {};
keys[value] = data;
end
tinsert(data.auctions, v.itemLink);
end
end
end
-- Move all achievementID-based items into criteriaID
if searchResultsByKey.achievementID then
local criteria = searchResultsByKey.criteriaID;
if criteria then
for key,entry in pairs(searchResultsByKey.achievementID) do
criteria[key] = entry;
end
else
searchResultsByKey.criteriaID = searchResultsByKey.achievementID;
end
searchResultsByKey.achievementID = nil;
end
-- Apply a sub-filter to items with spellID-based identifiers.
if searchResultsByKey.spellID then
local filteredItems = {};
for key,entry in pairs(searchResultsByKey.spellID) do
filterID = entry.filterID or entry.f;
if filterID then
local filterData = filteredItems[filterID];
if not filterData then
filterData = {};
filteredItems[filterID] = filterData;
end
filterData[key] = entry;
else
print("Spell " .. entry.spellID .. " (Item ID #" .. (entry.itemID or "???") .. ") is missing a filterID?");
end
end
if filteredItems[100] then searchResultsByKey.mountID = filteredItems[100]; end -- Mounts
if filteredItems[200] then searchResultsByKey.recipeID = filteredItems[200]; end -- Recipes
searchResultsByKey.spellID = nil;
end
if searchResultsByKey.sourceID then
local filteredItems = {};
local cachedSourceIDs = searchResultsByKey.sourceID;
searchResultsByKey.sourceID = {};
for sourceID,entry in pairs(cachedSourceIDs) do
filterID = entry.filterID or entry.f;
if filterID then
local filterData = filteredItems[entry.f];
if not filterData then
filterData = app.CreateFilter(filterID);
filterData.g = {};
filteredItems[filterID] = filterData;
tinsert(searchResultsByKey.sourceID, filterData);
end
tinsert(filterData.g, entry);
end
end
for f,entry in pairs(filteredItems) do
app.Sort(entry.g, function(a,b)
return a.u and not b.u;
end);
end
end
-- Process the Non-Collectible Items for Reagents
local reagentCache = app.ReagentsDB;
if reagentCache and searchResultsByKey.itemID then
local cachedItems = searchResultsByKey.itemID;
searchResultsByKey.itemID = {};
searchResultsByKey.reagentID = {};
for itemID,entry in pairs(cachedItems) do
if reagentCache[itemID] then
searchResultsByKey.reagentID[itemID] = entry;
if not entry.g then entry.g = {}; end
for itemID2,count in pairs(reagentCache[itemID][2]) do
local searchResults = SearchForField("itemID", itemID2);
if #searchResults > 0 then
tinsert(entry.g, CreateObject(searchResults[1]));
end
end
else
-- Push it back into the itemID table
searchResultsByKey.itemID[itemID] = entry;
end
end
end
-- Insert Buttons into the groups.
-- not sure what this was but unreferenced globals currently
-- wipe(window.data.g);
-- for i,option in ipairs(window.data.options) do
-- tinsert(window.data.g, option);
-- end
local ObjectTypeMetas = {
["criteriaID"] = app.CreateFilter(105, { -- Achievements
["icon"] = 341221,
["description"] = L.ITEMS_FOR_ACHIEVEMENTS_DESC,
["priority"] = 1,
}),
["sourceID"] = app.CreateRawText("Appearances", { -- Appearances
["icon"] = 135276,
["description"] = L.ALL_APPEARANCES_DESC,
["priority"] = 2,
}),
["mountID"] = app.CreateFilter(100, { -- Mounts
["description"] = L.ALL_THE_MOUNTS_DESC,
["priority"] = 3,
}),
["speciesID"] = app.CreateFilter(101, { -- Battle Pets
["description"] = L.ALL_THE_BATTLEPETS_DESC,
["priority"] = 4,
}),
["questID"] = app.CreateCustomHeader(app.HeaderConstants.QUESTS, {
["icon"] = 464068,
["description"] = L.ALL_THE_QUESTS_DESC,
["priority"] = 5,
}),
["recipeID"] = app.CreateFilter(200, { -- Recipes
["icon"] = 134942,
["description"] = L.ALL_THE_RECIPES_DESC,
["priority"] = 6,
}),
["itemID"] = app.CreateRawText(L.GENERAL_PAGE, {
["icon"] = 334365,
["description"] = L.ALL_THE_ILLUSIONS_DESC,
["priority"] = 7,
}),
["reagentID"] = app.CreateFilter(56, { -- Reagent
["icon"] = 135851,
["description"] = L.ALL_THE_REAGENTS_DESC,
["priority"] = 8,
}),
};
-- Display Test for Raw Data + Filtering
for key, searchResults in pairs(searchResultsByKey) do
local subdata = {};
subdata.visible = true;
if ObjectTypeMetas[key] then
setmetatable(subdata, { __index = ObjectTypeMetas[key] });
else
subdata.description = "Container for '" .. key .. "' object types.";
subdata.text = key;
end
subdata.g = {};
for i,j in pairs(searchResults) do
tinsert(subdata.g, j);
end
-- not sure what this was but unreferenced globals currently
-- tinsert(window.data.g, subdata);
end
-- not sure what this was but unreferenced globals currently
-- app.Sort(window.data.g, function(a, b)
-- return (b.priority or 0) > (a.priority or 0);
-- end);
-- AssignChildren(window.data);
-- app.TopLevelUpdateGroup(window.data);
-- window:Show();
-- window:Update();
end
app.OpenAuctionModule = function(self)
-- TODO: someday someone might fix this AH functionality...
if true then return; end
if C_AddOns.IsAddOnLoaded("TradeSkillMaster") then -- Why, TradeSkillMaster, why are you like this?
C_Timer.After(2, app.EmptyFunction);
end
if app.Blizzard_AuctionHouseUILoaded then
-- Localize some global APIs
local C_AuctionHouse_GetNumReplicateItems = C_AuctionHouse.GetNumReplicateItems;
local C_AuctionHouse_GetReplicateItemInfo = C_AuctionHouse.GetReplicateItemInfo;
local C_AuctionHouse_GetReplicateItemLink = C_AuctionHouse.GetReplicateItemLink;
-- Create the Auction Tab for ATT.
local tabID = AuctionHouseFrame.numTabs+1;
local button = CreateFrame("Button", "AuctionHouseFrameTab"..tabID, AuctionHouseFrame, "AuctionHouseFrameDisplayModeTabTemplate");
button:SetID(tabID);
button:SetText(L.SHORTTITLE);
button:SetNormalFontObject(GameFontHighlightSmall);
button:SetPoint("LEFT", AuctionHouseFrame.Tabs[tabID-1], "RIGHT", -15, 0);
tinsert(AuctionHouseFrame.Tabs, button);
PanelTemplates_SetNumTabs (AuctionHouseFrame, tabID);
PanelTemplates_EnableTab (AuctionHouseFrame, tabID);
-- Garbage collect the function after this is executed.
app.OpenAuctionModule = app.EmptyFunction;
app.AuctionModuleTabID = tabID;
-- Create the movable Auction Data window.
local window = app:GetWindow("AuctionData", AuctionHouseFrame);
auctionFrame:SetScript("OnEvent", function(self, e, ...)
if e == "REPLICATE_ITEM_LIST_UPDATE" then
self:UnregisterEvent("REPLICATE_ITEM_LIST_UPDATE");
AllTheThingsAuctionData = {};
local items = {};
local auctionItems = C_AuctionHouse_GetNumReplicateItems();
for i=0,auctionItems-1 do
local itemLink;
local count, _, _, _, _, _, _, price, _, _, _, _, _, _, itemID, status = select(3, C_AuctionHouse_GetReplicateItemInfo(i));
if itemID then
if price and status then
itemLink = C_AuctionHouse_GetReplicateItemLink(i);
if itemLink then
AllTheThingsAuctionData[itemID] = { itemLink = itemLink, count = count, price = (price/count) };
end
else
local item = Item:CreateFromItemID(itemID);
items[item] = true;
item:ContinueOnItemLoad(function()
count, _, _, _, _, _, _, price, _, _, _, _, _, _, itemID, status = select(3, C_AuctionHouse_GetReplicateItemInfo(i));
items[item] = nil;
if itemID and status then
itemLink = C_AuctionHouse_GetReplicateItemLink(i);
if itemLink then
AllTheThingsAuctionData[itemID] = { itemLink = itemLink, count = count, price = (price/count) };
end
end
if not next(items) then
items = {};
end
end);
end
end
end
if not next(items) then
items = {};
end
print(L.TITLE .. L.AH_SCAN_SUCCESSFUL_1 .. auctionItems .. L.AH_SCAN_SUCCESSFUL_2);
StartCoroutine("ProcessAuctionData", app.ProcessAuctionData, 1);
end
end);
window:SetPoint("TOPLEFT", AuctionHouseFrame, "TOPRIGHT", 0, -10);
window:SetPoint("BOTTOMLEFT", AuctionHouseFrame, "BOTTOMRIGHT", 0, 10);
window:Hide();
-- Cache some functions to make them faster
local origSideDressUpFrameHide, origSideDressUpFrameShow = SideDressUpFrame.Hide, SideDressUpFrame.Show;
---@diagnostic disable-next-line: duplicate-set-field
SideDressUpFrame.Hide = function(...)
origSideDressUpFrameHide(...);
window:ClearAllPoints();
window:SetPoint("TOPLEFT", AuctionHouseFrame, "TOPRIGHT", 0, -10);
window:SetPoint("BOTTOMLEFT", AuctionHouseFrame, "BOTTOMRIGHT", 0, 10);
end
---@diagnostic disable-next-line: duplicate-set-field
SideDressUpFrame.Show = function(...)
origSideDressUpFrameShow(...);
window:ClearAllPoints();
window:SetPoint("LEFT", SideDressUpFrame, "RIGHT", 0, 0);
window:SetPoint("TOP", AuctionHouseFrame, "TOP", 0, -10);
window:SetPoint("BOTTOM", AuctionHouseFrame, "BOTTOM", 0, 10);
end
button:SetScript("OnClick", function(self) -- This is the "ATT" button at the bottom of the auction house frame
if self:GetID() == tabID then
window:Show();
end
end);
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
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.FactionBonus then accountWideData.FactionBonus = {}; end
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) 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,
["Blizzard_AuctionHouseUI"] = function()
app.Blizzard_AuctionHouseUILoaded = true;
if app.Settings:GetTooltipSetting("Auto:AH") then
app:OpenAuctionModule();
end
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)
-- app.PrintMemoryUsage("AllTheThings.EOF");
-- app.AddEventHandler("Addon.Memory", function(info)
-- app.PrintMemoryUsage(info)
-- end)
-- app.LinkEventSequence("OnStartupDone", "Addon.Memory")
-- app.LinkEventSequence("OnWindowFillComplete", "Addon.Memory")