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
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")
|
|
|