q3fuba

SavedInstances 7.0.3 - Fixed

Aug 12th, 2016
2,390
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 171.45 KB | None | 0 0
  1. local addonName, vars = ...
  2. SavedInstances = vars
  3. local addon = vars
  4. local addonAbbrev = "SI"
  5. vars.core = LibStub("AceAddon-3.0"):NewAddon(addonName, "AceEvent-3.0", "AceTimer-3.0", "AceBucket-3.0")
  6. local core = vars.core
  7. local L = vars.L
  8. vars.LDB = LibStub("LibDataBroker-1.1", true)
  9. vars.icon = vars.LDB and LibStub("LibDBIcon-1.0", true)
  10.  
  11. local QTip = LibStub("LibQTip-1.0")
  12. local dataobject, db, config
  13. local maxdiff = 23 -- max number of instance difficulties
  14. local maxcol = 4 -- max columns per player+instance
  15.  
  16. addon.svnrev = {}
  17. addon.svnrev["SavedInstances.lua"] = tonumber(("$Revision: 518 $"):match("%d+"))
  18.  
  19. -- local (optimal) references to provided functions
  20. local table, math, bit, string, pairs, ipairs, unpack, strsplit, time, type, wipe, tonumber, select, strsub =
  21.   table, math, bit, string, pairs, ipairs, unpack, strsplit, time, type, wipe, tonumber, select, strsub
  22. local GetSavedInstanceInfo, GetNumSavedInstances, GetSavedInstanceChatLink, GetLFGDungeonNumEncounters, GetLFGDungeonEncounterInfo, GetNumRandomDungeons, GetLFGRandomDungeonInfo, GetLFGDungeonInfo, LFGGetDungeonInfoByID, GetLFGDungeonRewards, GetTime, UnitIsUnit, GetInstanceInfo, IsInInstance, SecondsToTime, GetQuestResetTime, GetGameTime, GetCurrencyInfo, GetNumGroupMembers =
  23.   GetSavedInstanceInfo, GetNumSavedInstances, GetSavedInstanceChatLink, GetLFGDungeonNumEncounters, GetLFGDungeonEncounterInfo, GetNumRandomDungeons, GetLFGRandomDungeonInfo, GetLFGDungeonInfo, LFGGetDungeonInfoByID, GetLFGDungeonRewards, GetTime, UnitIsUnit, GetInstanceInfo, IsInInstance, SecondsToTime, GetQuestResetTime, GetGameTime, GetCurrencyInfo, GetNumGroupMembers
  24.  
  25. -- local (optimal) references to Blizzard's strings
  26. local RAID_CLASS_COLORS = RAID_CLASS_COLORS
  27. local RAID_FINDER = PLAYER_DIFFICULTY3
  28. local FONTEND = FONT_COLOR_CODE_CLOSE
  29. local GOLDFONT = NORMAL_FONT_COLOR_CODE
  30. local YELLOWFONT = LIGHTYELLOW_FONT_COLOR_CODE
  31. local REDFONT = RED_FONT_COLOR_CODE
  32. local GREENFONT = GREEN_FONT_COLOR_CODE
  33. local WHITEFONT = HIGHLIGHT_FONT_COLOR_CODE
  34. local GRAYFONT = GRAY_FONT_COLOR_CODE
  35. local GRAY_COLOR = { 0.5, 0.5, 0.5, 1 }
  36. local LFD_RANDOM_REWARD_EXPLANATION2 = LFD_RANDOM_REWARD_EXPLANATION2
  37. local INSTANCE_SAVED, TRANSFER_ABORT_TOO_MANY_INSTANCES, NO_RAID_INSTANCES_SAVED =
  38.   INSTANCE_SAVED, TRANSFER_ABORT_TOO_MANY_INSTANCES, NO_RAID_INSTANCES_SAVED
  39.  
  40. vars.Indicators = {
  41.   ICON_STAR = ICON_LIST[1] .. "16:16:0:0|t",
  42.   ICON_CIRCLE = ICON_LIST[2] .. "16:16:0:0|t",
  43.   ICON_DIAMOND = ICON_LIST[3] .. "16:16:0:0|t",
  44.   ICON_TRIANGLE = ICON_LIST[4] .. "16:16:0:0|t",
  45.   ICON_MOON = ICON_LIST[5] .. "16:16:0:0|t",
  46.   ICON_SQUARE = ICON_LIST[6] .. "16:16:0:0|t",
  47.   ICON_CROSS = ICON_LIST[7] .. "16:16:0:0|t",
  48.   ICON_SKULL = ICON_LIST[8] .. "16:16:0:0|t",
  49.   BLANK = "None",
  50. }
  51.  
  52. vars.Categories = { }
  53. local maxExpansion
  54. for i = 0,10 do
  55.   local ename = _G["EXPANSION_NAME"..i]
  56.   if ename then
  57.     maxExpansion = i
  58.     vars.Categories["D"..i] = ename .. ": " .. LFG_TYPE_DUNGEON
  59.     vars.Categories["R"..i] = ename .. ": " .. LFG_TYPE_RAID
  60.   else
  61.     break
  62.   end
  63. end
  64.  
  65. local tooltip, indicatortip
  66. local thisToon = UnitName("player") .. " - " .. GetRealmName()
  67. local maxlvl = MAX_PLAYER_LEVEL_TABLE[#MAX_PLAYER_LEVEL_TABLE]
  68.  
  69. local scantt = CreateFrame("GameTooltip", "SavedInstancesScanTooltip", UIParent, "GameTooltipTemplate")
  70.  
  71. local currency = {
  72.   --395, -- Justice Points
  73.   1191, -- Valor Points
  74.   --392, -- Honor Points
  75.   --390, -- Conquest Points
  76.   738, -- Lesser Charm of Good Fortune
  77.   697, -- Elder Charm of Good Fortune
  78.   752, -- Mogu Rune of Fate
  79.   776, -- Warforged Seal
  80.   777, -- Timeless Coin
  81.   402, -- Ironpaw Token
  82.   81, -- Epicurean Award
  83.   515, -- Darkmoon Prize Ticket
  84.   241, -- Champion's Seal
  85.   391, -- Tol Barad Commendation
  86.   416, -- Mark of the World Tree
  87.   789, -- Bloody Coin
  88.   823, -- Apexis Crystal
  89.   824, -- Garrison Resources
  90.   1101,-- Oil
  91.   1155,-- Ancient Mana
  92.   1220,-- Order Resources
  93.   994, -- Seal of Tempered Fate
  94.   1129,-- Seal of Inevitable Fate
  95.   1166,-- Timewarped Badge
  96.   1226,-- Nethershards
  97. }
  98. addon.currency = currency
  99.  
  100. addon.LFRInstances = {
  101.   -- total is boss count, base is boss offset,
  102.   -- parent is instance name to use
  103.   -- altid is for alternate LFRID for higher level toons
  104.  
  105.   [416] = { total=4, base=1,  parent=448, altid=843 }, -- DS1: The Siege of Wyrmrest Temple
  106.   [417] = { total=4, base=5,  parent=448, altid=844 }, -- DS2: Fall of Deathwing
  107.  
  108.   [527] = { total=3, base=1,  parent=532, altid=830 }, -- MSV1: Guardians of Mogu'shan
  109.   [528] = { total=3, base=4,  parent=532, altid=831 }, -- MSV2: The Vault of Mysteries
  110.   [529] = { total=3, base=1,  parent=534, altid=832 }, -- HoF1: The Dread Approach
  111.   [530] = { total=3, base=4,  parent=534, altid=833 }, -- HoF2: Nightmare of Shek'zeer
  112.   [526] = { total=4, base=1,  parent=536, altid=834 }, -- TeS1: Terrace of Endless Spring
  113.   [610] = { total=3, base=1,  parent=634, altid=835 }, -- ToT1: Last Stand of the Zandalari
  114.   [611] = { total=3, base=4,  parent=634, altid=836 }, -- ToT2: Forgotten Depths
  115.   [612] = { total=3, base=7,  parent=634, altid=837 }, -- ToT3: Halls of Flesh-Shaping
  116.   [613] = { total=3, base=10, parent=634, altid=838 }, -- ToT4: Pinnacle of Storms
  117.   [716] = { total=4, base=1,  parent=766, altid=839 }, -- SoO1: Vale of Eternal Sorrows
  118.   [717] = { total=4, base=5,  parent=766, altid=840 }, -- SoO2: Gates of Retribution
  119.   [724] = { total=3, base=9,  parent=766, altid=841 }, -- SoO3: The Underhold
  120.   [725] = { total=3, base=12, parent=766, altid=842 }, -- SoO4: Downfall
  121.  
  122.   [849] = { total=3, base=1,  parent=897, altid=nil }, -- Highmaul1: Walled City
  123.   [850] = { total=3, base=4,  parent=897, altid=nil }, -- Highmaul2: Arcane Sanctum
  124.   [851] = { total=1, base=7,  parent=897, altid=nil }, -- Highmaul3: Imperator's Rise
  125.   [847] = { total=3, base=1,  parent=900, altid=nil, remap={ 1, 2, 7 } }, -- BRF1: Slagworks
  126.   [846] = { total=3, base=4,  parent=900, altid=nil, remap={ 3, 5, 8 } }, -- BRF2: The Black Forge
  127.   [848] = { total=3, base=7,  parent=900, altid=nil, remap={ 4, 6, 9 } }, -- BRF3: Iron Assembly
  128.   [823] = { total=1, base=10, parent=900, altid=nil }, -- BRF4: Blackhand's Crucible
  129.  
  130.   [982] = { total=3, base=1,  parent=989, altid=nil }, -- Hellfire1: Hellbreach
  131.   [983] = { total=3, base=4,  parent=989, altid=nil }, -- Hellfire2: Halls of Blood
  132.   [984] = { total=3, base=7,  parent=989, altid=nil, remap={ 7, 8,  11 } }, -- Hellfire3: Bastion of Shadows
  133.   [985] = { total=3, base=10, parent=989, altid=nil, remap={ 9, 10, 12 } }, -- Hellfire4: Destructor's Rise
  134.   [986] = { total=1, base=13, parent=989, altid=nil }, -- Hellfire5: Black Gate
  135.  
  136.   [1287] ={ total=3, base=1,  parent=1350,altid=nil }, -- EN1: Darkbough
  137.   [1288] ={ total=3, base=4,  parent=1350,altid=nil }, -- EN2: Tormented Guardians
  138.   [1289] ={ total=1, base=7,  parent=1350,altid=nil }, -- EN3: Rift of Aln
  139.  
  140.   [1290] ={ total=3, base=1,  parent=1353,altid=nil }, -- NH1: Arcing Aqueducts
  141.   [1291] ={ total=3, base=4,  parent=1353,altid=nil }, -- NH2: Royal Athenaeum
  142.   [1292] ={ total=3, base=7,  parent=1353,altid=nil }, -- NH3: Nightspire
  143.   [1293] ={ total=1, base=10, parent=1353,altid=nil }, -- NH4: Betrayer's Rise
  144. }
  145. local tmp = {}
  146. for id, info in pairs(addon.LFRInstances) do
  147.   tmp[id] = info
  148.   if info.altid then
  149.     tmp[info.altid] = info
  150.   end
  151. end
  152. addon.LFRInstances = tmp
  153.  
  154. addon.WorldBosses = {
  155.   -- encounter index is embedded in the Hjournal hyperlink
  156.   [691] = { quest=32099, expansion=4, level=90 }, -- Sha of Anger
  157.   [725] = { quest=32098, expansion=4, level=90 }, -- Galleon
  158.   [814] = { quest=32518, expansion=4, level=90 }, -- Nalak
  159.   [826] = { quest=32519, expansion=4, level=90 }, -- Oondasta
  160.   [857] = { quest=33117,   expansion=4, level=90, name=L["The Four Celestials"]  }, -- Chi-Ji
  161.   --[858] = { quest=nil, expansion=4, level=90 }, -- Yu'lon
  162.   --[859] = { quest=nil, expansion=4, level=90 }, -- Niuzao
  163.   --[860] = { quest=nil, expansion=4, level=90 }, -- Xuen
  164.   [861] = { quest=nil,   expansion=4, level=90 }, -- Ordos
  165.  
  166.   --[[
  167.   [1291] = { quest=37460,  expansion=5, level=100 }, -- Drov the Ruiner
  168.   [1211] = { quest=37462,  expansion=5, level=100 }, -- Tarlna the Ageless
  169.   --]]
  170.   [1211] = { quest=37462,  expansion=5, level=100, -- Drov/Tarlna share a loot and quest atm
  171.     name=select(2,EJ_GetCreatureInfo(1,1291)):match("^[^ ]+").." / "..
  172.     select(2,EJ_GetCreatureInfo(1,1211)):match("^[^ ]+")},
  173.   [1291] = { remove=true }, -- Drov cleanup
  174.  
  175.   [1262] = { quest=37464, expansion=5, level=100 }, -- Rukhmar
  176.   [1452] = { quest=94015, expansion=5, level=100 }, -- Kazzak
  177.  
  178.   -- bosses with no EJ entry (eid is a placeholder)
  179.   [9001] = { quest=38276, name=GARRISON_LOCATION_TOOLTIP.." "..BOSS, expansion=5, level=100 },
  180. }
  181.  
  182. local _specialQuests = {
  183.   -- Isle of Thunder
  184.   [32610] = { zid=928, lid=94221 }, -- Shan'ze Ritual Stone looted
  185.   [32611] = { zid=928, lid1=95350 },-- Incantation of X looted
  186.   [32626] = { zid=928, lid=94222 }, -- Key to the Palace of Lei Shen looted
  187.   [32609] = { zid=928, aid=8104, aline="Left5"  }, -- Trove of the Thunder King (outdoor chest)
  188.   -- Timeless Isle
  189.   [32962] = { zid=951, aid=8743, daily=true },  -- Zarhym
  190.   [32961] = { zid=951, daily=true },  -- Scary Ghosts and Nice Sprites
  191.   [32956] = { zid=951, aid=8727, acid=2, aline="Right7" }, -- Blackguard's Jetsam
  192.   [32957] = { zid=951, aid=8727, acid=1, aline="Left7" },  -- Sunken Treasure
  193.   [32970] = { zid=951, aid=8727, acid=3, aline="Left8" },  -- Gleaming Treasure Satchel
  194.   [32968] = { zid=951, aid=8726, acid=2, aline="Right7" }, -- Rope-Bound Treasure Chest
  195.   [32969] = { zid=951, aid=8726, acid=1, aline="Left7" },  -- Gleaming Treasure Chest
  196.   [32971] = { zid=951, aid=8726, acid=3, aline="Left8" },  -- Mist-Covered Treasure Chest
  197.   -- Garrison
  198.   [37638] = { zone=GARRISON_LOCATION_TOOLTIP, aid=9162 }, -- Bronze Defender
  199.   [37639] = { zone=GARRISON_LOCATION_TOOLTIP, aid=9164 }, -- Silver Defender
  200.   [37640] = { zone=GARRISON_LOCATION_TOOLTIP, aid=9165 }, -- Golden Defender
  201.   [38482] = { zone=GARRISON_LOCATION_TOOLTIP, aid=9826 }, -- Platinum Defender
  202.   -- Tanaan Jungle
  203.   [39287] = { zid=945, daily=true }, -- Deathtalon
  204.   [39288] = { zid=945, daily=true }, -- Terrorfist
  205.   [39289] = { zid=945, daily=true }, -- Doomroller
  206.   [39290] = { zid=945, daily=true }, -- Vengeance
  207. }
  208. function addon:specialQuests()
  209.   for qid, qinfo in pairs(_specialQuests) do
  210.     qinfo.quest = qid
  211.  
  212.     if not qinfo.name and (qinfo.lid or qinfo.lid1) then
  213.       local itemname, itemlink = GetItemInfo(qinfo.lid or qinfo.lid1)
  214.       if itemlink and qinfo.lid then
  215.         qinfo.name = itemlink.." ("..LOOT..")"
  216.       elseif itemname and qinfo.lid1 then
  217.         local name = itemname:match("^[^%s]+")
  218.         if name and #name > 0 then
  219.           qinfo.name = name.." ("..LOOT..")"
  220.         end
  221.       end
  222.     elseif not qinfo.name and qinfo.aid and qinfo.acid then
  223.       local l = GetAchievementCriteriaInfo(qinfo.aid, qinfo.acid)
  224.       if l then
  225.         qinfo.name = l:gsub("%p$","")
  226.       end
  227.     elseif not qinfo.name and qinfo.aid then
  228.       scantt:SetOwner(UIParent,"ANCHOR_NONE")
  229.       scantt:SetAchievementByID(qinfo.aid)
  230.       local l = _G[scantt:GetName().."Text"..(qinfo.aline or "Left1")]
  231.       l = l and l:GetText()
  232.       if l then
  233.         qinfo.name = l:gsub("%p$","")
  234.       end
  235.     elseif not qinfo.name then
  236.       local title, link = addon:QuestInfo(qid)
  237.       if title then
  238.         title = title:gsub("%p?%s*[Tt]racking%s*[Qq]uest","")
  239.         title = strtrim(title)
  240.         qinfo.name = title
  241.       end
  242.     end
  243.  
  244.     if not qinfo.zone and qinfo.zid then
  245.       qinfo.zone = GetMapNameByID(qinfo.zid)
  246.     end
  247.   end
  248.   return _specialQuests
  249. end
  250.  
  251. local QuestExceptions = {
  252.   -- some quests are misidentified in scope
  253.   [7905]  = "Regular", -- Darkmoon Faire referral -- old addon versions misidentified this as monthly
  254.   [7926]  = "Regular", -- Darkmoon Faire referral
  255.   [37819] = "Regular", -- Darkmoon Faire races referral
  256.   [31752] = "AccountDaily", -- Blingtron
  257.   [34774] = "AccountDaily", -- Blingtron 5000
  258.   -- also pre-populate a few important quests
  259.   [32640] = "Weekly",  -- Champions of the Thunder King
  260.   [32641] = "Weekly",  -- Champions of the Thunder King
  261.   [32718] = "Regular",  -- Mogu Runes of Fate -- ticket 142: outdated quest flag still shows up
  262.   [32719] = "Regular",  -- Mogu Runes of Fate
  263.   [33133] = "Regular",  -- Warforged Seals outdated quests, no longer weekly
  264.   [33134] = "Regular",  -- Warforged Seals
  265.   [33338] = "Weekly",  -- Empowering the Hourglass
  266.   [33334] = "Weekly",  -- Strong Enough to Survive
  267. }
  268.  
  269. local WoDSealQuests = {
  270.   [36058] = "Weekly",  -- Seal of Dwarven Bunker
  271.   -- Seal of Ashran quests
  272.   [36054] = "Weekly",
  273.   [37454] = "Weekly",
  274.   [37455] = "Weekly",
  275.   [36056] = "Weekly",
  276.   [37456] = "Weekly",
  277.   [37457] = "Weekly",
  278.   [36057] = "Weekly",
  279.   [37458] = "Weekly",
  280.   [37459] = "Weekly",
  281.   [36055] = "Weekly",
  282.   [37452] = "Weekly",
  283.   [37453] = "Weekly",
  284. }
  285. for k,v in pairs(WoDSealQuests) do
  286.   QuestExceptions[k] = v
  287. end
  288.  
  289. function addon:QuestInfo(questid)
  290.   if not questid or questid == 0 then return nil end
  291.   scantt:SetOwner(UIParent,"ANCHOR_NONE")
  292.   scantt:SetHyperlink("\124cffffff00\124Hquest:"..questid..":90\124h[]\124h\124r")
  293.   local l = _G[scantt:GetName().."TextLeft1"]
  294.   l = l and l:GetText()
  295.   if not l or #l == 0 then return nil end -- cache miss
  296.   return l, "\124cffffff00\124Hquest:"..questid..":90\124h["..l.."]\124h\124r"
  297. end
  298.  
  299. local function chatMsg(...)
  300.   DEFAULT_CHAT_FRAME:AddMessage("\124cFFFF0000"..addonName.."\124r: "..string.format(...))
  301. end
  302. addon.chatMsg = chatMsg
  303. local function debug(...)
  304.   --addon.db.dbg = true
  305.   if addon.db.dbg then
  306.     chatMsg(...)
  307.   end
  308. end
  309. addon.debug = debug
  310. local function bugReport(msg)
  311.   addon.bugreport = addon.bugreport or {}
  312.   local now = GetTime()
  313.   if now < (addon.bugreport[msg] or 0)+60 then return end
  314.   addon.bugreport[msg] = now
  315.   chatMsg(msg)
  316.   if now < (addon.bugreport["url"] or 0)+5 then return end
  317.   chatMsg("Please report this bug at: http://www.wowace.com/addons/saved_instances/tickets/")
  318.   addon.bugreport["url"] = now
  319. end
  320.  
  321. local GTToffset = time() - GetTime()
  322. local function GetTimeToTime(val)
  323.   if not val then return nil end
  324.   return val + GTToffset
  325. end
  326.  
  327. function addon:timedebug()
  328.   chatMsg("Version: %s (%s)", addon.version, addon.revision)
  329.   chatMsg("Realm: %s (%s)", GetRealmName(), addon:GetRegion())
  330.   chatMsg("Zone: %s (%s)", GetRealZoneText(), addon:GetCurrentMapAreaID())
  331.   chatMsg("time()=%s GetTime()=%s", time(), GetTime())
  332.   chatMsg("Local time: %s local", date("%A %c"))
  333.   chatMsg("GetGameTime: %s:%s server",GetGameTime())
  334.   chatMsg("CalendarGetDate: %s %s/%s/%s server",CalendarGetDate())
  335.   chatMsg("GetQuestResetTime: %s",SecondsToTime(GetQuestResetTime()))
  336.   chatMsg(date("Daily reset: %a %c local (based on GetQuestResetTime)",time()+GetQuestResetTime()))
  337.   chatMsg("Local to Server offset: %d hours",SavedInstances:GetServerOffset())
  338.   local t = SavedInstances:GetNextDailyResetTime()
  339.   chatMsg("Next daily reset: %s local, %s server",date("%a %c",t), date("%a %c",t+3600*SavedInstances:GetServerOffset()))
  340.   local t = SavedInstances:GetNextWeeklyResetTime()
  341.   chatMsg("Next weekly reset: %s local, %s server",date("%a %c",t), date("%a %c",t+3600*SavedInstances:GetServerOffset()))
  342.   local t = SavedInstances:GetNextDailySkillResetTime()
  343.   chatMsg("Next skill reset: %s local, %s server",date("%a %c",t), date("%a %c",t+3600*SavedInstances:GetServerOffset()))
  344.   local t = SavedInstances:GetNextDarkmoonResetTime()
  345.   chatMsg("Next darkmoon reset: %s local, %s server",date("%a %c",t), date("%a %c",t+3600*SavedInstances:GetServerOffset()))
  346. end
  347.  
  348. -- abbreviate expansion names (which apparently are not localized in any western character set)
  349. local function abbreviate(iname)
  350.   iname = iname:gsub("Burning Crusade", "BC")
  351.   iname = iname:gsub("Wrath of the Lich King", "WotLK")
  352.   iname = iname:gsub("Cataclysm", "Cata")
  353.   iname = iname:gsub("Mists of Pandaria", "MoP")
  354.   iname = iname:gsub("Warlords of Draenor", "WoD")
  355.   return iname
  356. end
  357.  
  358. function addon:formatNumber(num,ismoney)
  359.   num = tonumber(num)
  360.   if not num then return "" end
  361.   local post = ""
  362.   if ismoney then
  363.     if num < 1000*10000 then -- less than 1k, show it all
  364.       return GetMoneyString(num)
  365.     end
  366.     num = math.floor(num / 10000)
  367.     post = " \124TInterface\\MoneyFrame\\UI-GoldIcon:0:0:2:0\124t"
  368.   end
  369.   if vars.db.Tooltip.NumberFormat then
  370.     local str = ""
  371.     local neg = num < 0
  372.     num = math.abs(num)
  373.     local int = math.floor(num)
  374.     local dec = num - int
  375.     local t = tostring(int)
  376.     if #t > 4 then -- leave 4 digit numbers
  377.       while #t > 3 do
  378.         str = LARGE_NUMBER_SEPERATOR .. t:sub(-3) .. str
  379.         t = t:sub(1,-4)
  380.     end
  381.     end
  382.     str = t..str
  383.     if dec > 0 then
  384.       str = str..string.format("%15g",dec):match("(%..*)$")
  385.     end
  386.     if neg then
  387.       str = "-"..str
  388.     end
  389.     return str..post
  390.   else
  391.     return num..post
  392.   end
  393. end
  394.  
  395. vars.defaultDB = {
  396.   DBVersion = 12,
  397.   History = { }, -- for tracking 5 instance per hour limit
  398.   -- key: instance string; value: time first entered
  399.   Toons = { },  -- table key: "Toon - Realm"; value:
  400.   -- Class: string
  401.   -- Level: integer
  402.   -- AlwaysShow: boolean REMOVED
  403.   -- Show: string "always", "never", "saved"
  404.   -- Daily1: expiry (normal) REMOVED
  405.   -- Daily2: expiry (heroic) REMOVED
  406.   -- LFG1: expiry (random dungeon)
  407.   -- LFG2: expiry (deserter)
  408.   -- WeeklyResetTime: expiry
  409.   -- DailyResetTime: expiry
  410.   -- DailyCount: integer REMOVED
  411.   -- PlayedLevel: integer
  412.   -- PlayedTotal: integer
  413.   -- Money: integer
  414.   -- Zone: string
  415.  
  416.   -- currency: key: currencyID  value:
  417.   -- amount: integer
  418.   -- earnedThisWeek: integer
  419.   -- weeklyMax: integer
  420.   -- totalMax: integer
  421.   -- season: integer
  422.  
  423.   -- Quests:  key: QuestID  value:
  424.   -- Title: string
  425.   -- Link: hyperlink
  426.   -- Zone: string
  427.   -- isDaily: boolean
  428.   -- Expires: expiration (non-daily)
  429.  
  430.   -- Skills: key: SpellID or CDID value:
  431.   -- Title: string
  432.   -- Link: hyperlink
  433.   -- Expires: expiration
  434.  
  435.   -- FarmPlanted: integer
  436.   -- FarmHarvested: integer
  437.   -- FarmCropPlanted: key: spellID value: count
  438.   -- FarmCropReady: key: spellID value: count
  439.   -- FarmExpires: expiration
  440.  
  441.   -- BonusRoll: key: int value:
  442.   -- name: string
  443.   -- time: int
  444.   -- currencyID: int
  445.   -- money: integer or nil
  446.   -- item: linkstring or nil
  447.  
  448.   Indicators = {
  449.     D1Indicator = "BLANK", -- indicator: ICON_*, BLANK
  450.     D1Text = "KILLED/TOTAL",
  451.     D1Color = { 0, 0.6, 0 }, -- dark green
  452.     D1ClassColor = true,
  453.     D2Indicator = "BLANK",
  454.     D2Text = "KILLED/TOTALH",
  455.     D2Color = { 0, 1, 0 }, -- green
  456.     D2ClassColor = true,
  457.     D3Indicator = "BLANK",
  458.     D3Text = "KILLED/TOTALM",
  459.     D3Color = { 1, 0, 0 }, -- red
  460.     D3ClassColor = true,
  461.     R0Indicator = "BLANK",
  462.     R0Text = "KILLED/TOTAL",
  463.     R0Color = { 0.6, 0.6, 0 }, -- dark yellow
  464.     R0ClassColor = true,
  465.     R1Indicator = "BLANK",
  466.     R1Text = "KILLED/TOTAL",
  467.     R1Color = { 0.6, 0.6, 0 }, -- dark yellow
  468.     R1ClassColor = true,
  469.     R2Indicator = "BLANK",
  470.     R2Text = "KILLED/TOTAL",
  471.     R2Color = { 0.6, 0, 0 }, -- dark red
  472.     R2ClassColor = true,
  473.     R3Indicator = "BLANK",
  474.     R3Text = "KILLED/TOTALH",
  475.     R3Color = { 1, 1, 0 }, -- yellow
  476.     R3ClassColor = true,
  477.     R4Indicator = "BLANK",
  478.     R4Text = "KILLED/TOTALH",
  479.     R4Color = { 1, 0, 0 }, -- red
  480.     R4ClassColor = true,
  481.     R5Indicator = "BLANK",
  482.     R5Text = "KILLED/TOTAL",
  483.     R5Color = { 0, 0, 1 }, -- blue
  484.     R5ClassColor = true,
  485.     R6Indicator = "BLANK",
  486.     R6Text = "KILLED/TOTAL",
  487.     R6Color = { 0, 1, 0 }, -- green
  488.     R6ClassColor = true,
  489.     R7Indicator = "BLANK",
  490.     R7Text = "KILLED/TOTALH",
  491.     R7Color = { 1, 1, 0 }, -- yellow
  492.     R7ClassColor = true,
  493.     R8Indicator = "BLANK",
  494.     R8Text = "KILLED/TOTALM",
  495.     R8Color = { 1, 0, 0 }, -- red
  496.     R8ClassColor = true,
  497.   },
  498.   Tooltip = {
  499.     ReverseInstances = false,
  500.     ShowExpired = false,
  501.     ShowHoliday = true,
  502.     ShowRandom = true,
  503.     CombineWorldBosses = false,
  504.     CombineLFR = true,
  505.     TrackDailyQuests = true,
  506.     TrackWeeklyQuests = true,
  507.     ShowCategories = false,
  508.     CategorySpaces = false,
  509.     RowHighlight = 0.1,
  510.     Scale = 1,
  511.     FitToScreen = true,
  512.     NewFirst = true,
  513.     RaidsFirst = true,
  514.     NumberFormat = true,
  515.     CategorySort = "EXPANSION", -- "EXPANSION", "TYPE"
  516.     ShowSoloCategory = false,
  517.     ShowHints = true,
  518.     ReportResets = true,
  519.     LimitWarn = true,
  520.     HistoryText = false,
  521.     ShowServer = false,
  522.     ServerSort = true,
  523.     ServerOnly = false,
  524.     ConnectedRealms = "group",
  525.     SelfFirst = true,
  526.     SelfAlways = false,
  527.     TrackLFG = true,
  528.     TrackDeserter = true,
  529.     TrackSkills = true,
  530.     TrackFarm = true,
  531.     TrackBonus = false,
  532.     TrackPlayed = true,
  533.     AugmentBonus = true,
  534.     CurrencyValueColor = true,
  535.     Currency776 = false, -- Warforged Seals
  536.     Currency738 = false, -- Lesser Charm of Good Fortune
  537.     Currency823 = true,  -- Apexis Crystal
  538.     Currency824 = true,  -- Garrison Resources
  539.     Currency1101= true,  -- Oil
  540.     Currency994 = false, -- Seal of Tempered Fate
  541.     Currency1129= false, -- Seal of Inevitable Fate
  542.     Currency1155= true,  -- Ancient Mana
  543.     Currency1166= true,  -- Timewarped Badge
  544.     Currency1191= true,  -- Valor Points
  545.     Currency1220= true,  -- Order Resources
  546.     Currency1226= true,  -- Nethershards
  547.     CurrencyMax = false,
  548.     CurrencyEarned = true,
  549.   },
  550.   Instances = { },  -- table key: "Instance name"; value:
  551.   -- Show: boolean
  552.   -- Raid: boolean
  553.   -- Holiday: boolean
  554.   -- Random: boolean
  555.   -- Expansion: integer
  556.   -- RecLevel: integer
  557.   -- LFDID: integer
  558.   -- LFDupdated: integer REMOVED
  559.   -- REMOVED Encounters[integer] = { GUID : integer, Name : string }
  560.   -- table key: "Toon - Realm"; value:
  561.   -- table key: "Difficulty"; value:
  562.   -- ID: integer, positive for a Blizzard Raid ID,
  563.   --  negative value for an LFR encounter count
  564.   -- Expires: integer
  565.   -- Locked: boolean, whether toon is locked to the save
  566.   -- Extended: boolean, whether this is an extended raid lockout
  567.   -- Link: string hyperlink to the save
  568.   -- 1..numEncounters: boolean LFR isLooted
  569.   MinimapIcon = { hide = false },
  570.   Quests = {},  -- Account-wide Quests:  key: QuestID  value: same as toon Quest database
  571.   QuestDB = {   -- permanent repeatable quest DB: key: questid  value: mapid
  572.     Daily = {},
  573.     Weekly = {},
  574.     Darkmoon = {},
  575.     AccountDaily = {},
  576.     AccountWeekly = {},
  577.   },
  578.   RealmMap = {},
  579. }
  580.  
  581. -- skinning support
  582. -- skinning addons should hook this function, eg:
  583. --   hooksecurefunc(SavedInstances,"SkinFrame",function(self,frame,name) frame:SetWhatever() end)
  584. function addon:SkinFrame(frame,name)
  585.   -- default behavior (ticket 81)
  586.   if IsAddOnLoaded("ElvUI") or IsAddOnLoaded("Tukui") then
  587.     if frame.StripTextures then
  588.       frame:StripTextures()
  589.     end
  590.     if frame.SetTemplate then
  591.       frame:SetTemplate("Transparent")
  592.     end
  593.     --local close = _G[name.."CloseButton"]
  594.     -- easy BugFix by q3fuba
  595.     local close = frame.CloseButton
  596.     if close and close.SetAlpha then
  597.       if ElvUI then
  598.         ElvUI[1]:GetModule('Skins'):HandleCloseButton(close)
  599.       end
  600.       if Tukui and Tukui[1] and Tukui[1].SkinCloseButton then
  601.         Tukui[1].SkinCloseButton(close)
  602.       end
  603.       close:SetAlpha(1)
  604.     end
  605.   end
  606. end
  607.  
  608. -- general helper functions below
  609.  
  610. local function ColorCodeOpenRGB(r,g,b,a)
  611.   return format("|c%02x%02x%02x%02x", math.floor(a * 255), math.floor(r * 255), math.floor(g * 255), math.floor(b * 255))
  612. end
  613.  
  614. local function ColorCodeOpen(color)
  615.   return ColorCodeOpenRGB(color[1] or color.r,
  616.     color[2] or color.g,
  617.     color[3] or color.b,
  618.     color[4] or color.a or 1)
  619. end
  620.  
  621. local function ClassColorise(class, targetstring)
  622.   local c = (CUSTOM_CLASS_COLORS and CUSTOM_CLASS_COLORS[class]) or RAID_CLASS_COLORS[class]
  623.   if c.colorStr then
  624.     c = "|c"..c.colorStr
  625.   else
  626.     c = ColorCodeOpen( c )
  627.   end
  628.   return c .. targetstring .. FONTEND
  629. end
  630.  
  631. function addon.ColoredToon(toon, fullname)
  632.   local str = (fullname and toon) or strsplit(' ',toon)
  633.   local t = vars.db.Toons[toon]
  634.   local class = t and t.Class
  635.   if class then
  636.     return ClassColorise(class, str)
  637.   else
  638.     return str
  639.   end
  640. end
  641.  
  642. local function CurrencyColor(amt, max)
  643.   amt = amt or 0
  644.   local samt = addon:formatNumber(amt)
  645.   if max == nil or max == 0 then
  646.     return samt
  647.   end
  648.   if vars.db.Tooltip.CurrencyValueColor then
  649.     local pct = amt / max
  650.     local color = GREENFONT
  651.     if pct == 1 then
  652.       color = REDFONT
  653.     elseif pct > 0.75 then
  654.       color = GOLDFONT
  655.     end
  656.     samt = color .. samt .. FONTEND
  657.   end
  658.   return samt
  659. end
  660.  
  661. local function TableLen(table)
  662.   local i = 0
  663.   for _, _ in pairs(table) do
  664.     i = i + 1
  665.   end
  666.   return i
  667. end
  668.  
  669. -- returns how many hours the server time is ahead of local time
  670. -- convert local time -> server time: add this value
  671. -- convert server time -> local time: subtract this value
  672. function addon:GetServerOffset()
  673.   local serverDay = CalendarGetDate() - 1 -- 1-based starts on Sun
  674.   local localDay = tonumber(date("%w")) -- 0-based starts on Sun
  675.   local serverHour, serverMinute = GetGameTime()
  676.   local localHour, localMinute = tonumber(date("%H")), tonumber(date("%M"))
  677.   if serverDay == (localDay + 1)%7 then -- server is a day ahead
  678.     serverHour = serverHour + 24
  679.   elseif localDay == (serverDay + 1)%7 then -- local is a day ahead
  680.     localHour = localHour + 24
  681.   end
  682.   local server = serverHour + serverMinute / 60
  683.   local localT = localHour + localMinute / 60
  684.   local offset = floor((server - localT) * 2 + 0.5) / 2
  685.   return offset
  686. end
  687.  
  688. function addon:GetRegion()
  689.   if not addon.region then
  690.     local reg
  691.     reg = GetCVar("portal")
  692.     if reg == "public-test" then -- PTR uses US region resets, despite the misleading realm name suffix
  693.       reg = "US"
  694.     end
  695.     if not reg or #reg ~= 2 then
  696.       local gcr = GetCurrentRegion()
  697.       reg = gcr and ({ "US", "KR", "EU", "TW", "CN" })[gcr]
  698.     end
  699.     if not reg or #reg ~= 2 then
  700.       reg = (GetCVar("realmList") or ""):match("^(%a+)%.")
  701.     end
  702.     if not reg or #reg ~= 2 then -- other test realms?
  703.       reg = (GetRealmName() or ""):match("%((%a%a)%)")
  704.     end
  705.     reg = reg and reg:upper()
  706.     if reg and #reg == 2 then
  707.       addon.region = reg
  708.     end
  709.   end
  710.   return addon.region
  711. end
  712.  
  713. function addon:GetNextDailyResetTime()
  714.   local resettime = GetQuestResetTime()
  715.   if not resettime or resettime <= 0 or -- ticket 43: can fail during startup
  716.     -- also right after a daylight savings rollover, when it returns negative values >.<
  717.     resettime > 24*3600+30 then -- can also be wrong near reset in an instance
  718.     return nil
  719.   end
  720.   if false then -- this should no longer be a problem after the 7.0 reset time changes
  721.     -- ticket 177/191: GetQuestResetTime() is wrong for Oceanic+Brazilian characters in PST instances
  722.     local serverHour, serverMinute = GetGameTime()
  723.     local serverResetTime = (serverHour*3600 + serverMinute*60 + resettime) % 86400 -- GetGameTime of the reported reset
  724.     local diff = serverResetTime - 10800 -- how far from 3AM server
  725.     if math.abs(diff) > 3.5*3600  -- more than 3.5 hours - ignore TZ differences of US continental servers
  726.       and addon:GetRegion() == "US" then
  727.       local diffhours = math.floor((diff + 1800)/3600)
  728.       resettime = resettime - diffhours*3600
  729.       if resettime < -900 then -- reset already passed, next reset
  730.         resettime = resettime + 86400
  731.       elseif resettime > 86400+900 then
  732.         resettime = resettime - 86400
  733.       end
  734.       debug("Adjusting GetQuestResetTime() discrepancy of %d seconds (%d hours). Reset in %d seconds", diff, diffhours, resettime)
  735.     end
  736.   end
  737.   return time() + resettime
  738. end
  739.  
  740. do
  741.   local midnight = {hour=23, min=59, sec=59}
  742.   function addon:GetNextDailySkillResetTime() -- trade skill reset time
  743.     -- this is just a "best guess" because in reality,
  744.     -- different trade skills reset at up to 3 different times
  745.  
  746.     if false then -- at next server midnight
  747.       midnight.month, midnight.day, midnight.year = select(2,CalendarGetDate()) -- date in server timezone
  748.       local ret = time(midnight)
  749.       local offset = addon:GetServerOffset() * 3600
  750.       ret = ret - offset
  751.       return ret
  752.   else -- at next daily quest reset time
  753.     local rt = addon:GetNextDailyResetTime()
  754.     if not rt then return nil end
  755.     --local info = date("*t"); print(info.isdst)
  756.     -- Blizzard's ridiculous reset crap:
  757.     -- trade skills ignore daylight savings after the date it changes UNTIL the next restart, then go back to observing it
  758.     if false then
  759.       rt = rt + 3600
  760.     end
  761.     if false then
  762.       rt = rt - 3600
  763.       if time() > rt then -- past trade reset but before daily reset, next day
  764.         rt = rt + 24*3600
  765.       end
  766.     end
  767.     return rt
  768.   end
  769.   end
  770. end
  771.  
  772. function addon:GetNextWeeklyResetTime()
  773.   if not addon.resetDays then
  774.     local region = addon:GetRegion()
  775.     if not region then return nil end
  776.     addon.resetDays = {}
  777.     addon.resetDays.DLHoffset = 0
  778.     if region == "US" then
  779.       addon.resetDays["2"] = true -- tuesday
  780.       -- ensure oceanic servers over the dateline still reset on tues UTC (wed 1/2 AM server)
  781.       addon.resetDays.DLHoffset = -3
  782.     elseif region == "EU" then
  783.       addon.resetDays["3"] = true -- wednesday
  784.     elseif region == "CN" or region == "KR" or region == "TW" then -- XXX: codes unconfirmed
  785.       addon.resetDays["4"] = true -- thursday
  786.     else
  787.       addon.resetDays["2"] = true -- tuesday?
  788.     end
  789.   end
  790.   local offset = (addon:GetServerOffset() + addon.resetDays.DLHoffset) * 3600
  791.   local nightlyReset = addon:GetNextDailyResetTime()
  792.   if not nightlyReset then return nil end
  793.   --while date("%A",nightlyReset+offset) ~= WEEKDAY_TUESDAY do
  794.   while not addon.resetDays[date("%w",nightlyReset+offset)] do
  795.     nightlyReset = nightlyReset + 24 * 3600
  796.   end
  797.   return nightlyReset
  798. end
  799.  
  800. do
  801.   local dmf_end = {hour=23, min=59}
  802.   function addon:GetNextDarkmoonResetTime()
  803.     -- Darkmoon faire runs from first Sunday of each month to following Saturday
  804.     -- this function returns an approximate time after the end of the current month's faire
  805.     local weekday, month, day, year = CalendarGetDate() -- date in server timezone (Sun==1)
  806.     local firstweekday = select(4,CalendarGetAbsMonth(month, year)) -- (Sun == 1)
  807.     local firstsunday = ((firstweekday == 1) and 1) or (9 - firstweekday)
  808.     dmf_end.year = year
  809.     dmf_end.month = month
  810.     dmf_end.day = firstsunday + 7 -- 1 days of "slop"
  811.     -- Unfortunately, DMF boundary ignores daylight savings, and the time of day varies across regions
  812.     -- Report a reset well past end to make sure we don't drop quests early
  813.     local ret = time(dmf_end)
  814.     local offset = addon:GetServerOffset() * 3600
  815.     ret = ret - offset
  816.     return ret
  817.   end
  818. end
  819.  
  820. function addon:QuestCount(toonname)
  821.   local t
  822.   if toonname then
  823.     t = vars and vars.db.Toons and vars.db.Toons[toonname]
  824.   else -- account-wide quests
  825.     t = db
  826.   end
  827.   if not t then return 0,0 end
  828.   local dailycount, weeklycount = 0,0
  829.   -- ticket 96: GetDailyQuestsCompleted() is unreliable, the response is laggy and it fails to count some quests
  830.   for _,info in pairs(t.Quests) do
  831.     if info.isDaily then
  832.       dailycount = dailycount + 1
  833.     else
  834.       weeklycount = weeklycount + 1
  835.     end
  836.   end
  837.   return dailycount, weeklycount
  838. end
  839.  
  840. -- local addon functions below
  841.  
  842. local function GetLastLockedInstance()
  843.   local numsaved = GetNumSavedInstances()
  844.   if numsaved > 0 then
  845.     for i = 1, numsaved do
  846.       local name, id, expires, diff, locked, extended, mostsig, raid, players, diffname = GetSavedInstanceInfo(i)
  847.       if locked then
  848.         return name, id, expires, diff, locked, extended, mostsig, raid, players, diffname
  849.       end
  850.     end
  851.   end
  852. end
  853.  
  854. function addon:normalizeName(str)
  855.   return str:gsub("%p",""):gsub("%s"," "):gsub("%s%s"," "):gsub("^%s+",""):gsub("%s+$",""):upper()
  856. end
  857.  
  858. addon.transInstance = {
  859.   -- lockout hyperlink id = LFDID
  860.   [543] = 188,  -- Hellfire Citadel: Ramparts
  861.   [540] = 189,  -- Hellfire Citadel: Shattered Halls : deDE
  862.   [542] = 187,  -- Hellfire Citadel: Blood Furnace esES
  863.   [534] = 195,  -- The Battle for Mount Hyjal
  864.   [509] = 160,  -- Ruins of Ahn'Qiraj
  865.   [557] = 179,  -- Auchindoun: Mana-Tombs : ticket 72 zhTW
  866.   [556] = 180,  -- Auchindoun: Sethekk Halls : ticket 151 frFR
  867.   [568] = 340,  -- Zul'Aman: frFR
  868.   [1004] = 474, -- Scarlet Monastary: deDE
  869.   [600] = 215,  -- Drak'Tharon: ticket 105 deDE
  870.   [560] = 183,  -- Escape from Durnholde Keep: ticket 124 deDE
  871.   [531] = 161,  -- AQ temple: ticket 137 frFR
  872.   [1228] = 897, -- Highmaul: ticket 175 ruRU
  873.   [552] = 1011, -- Arcatraz: ticket 216 frFR
  874. }
  875.  
  876. -- some instances (like sethekk halls) are named differently by GetSavedInstanceInfo() and LFGGetDungeonInfoByID()
  877. -- we use the latter name to key our database, and this function to convert as needed
  878. function addon:FindInstance(name, raid)
  879.   if not name or #name == 0 then return nil end
  880.   local nname = addon:normalizeName(name)
  881.   -- first pass, direct match
  882.   local info = vars.db.Instances[name]
  883.   if info then
  884.     return name, info.LFDID
  885.   end
  886.   -- hyperlink id lookup: must precede substring match for ticket 99
  887.   -- (so transInstance can override incorrect substring matches)
  888.   for i = 1, GetNumSavedInstances() do
  889.     local link = GetSavedInstanceChatLink(i) or ""
  890.     local lid,lname = link:match(":(%d+):%d+:%d+\124h%[(.+)%]\124h")
  891.     lname = lname and addon:normalizeName(lname)
  892.     lid = lid and tonumber(lid)
  893.     local lfdid = lid and addon.transInstance[lid]
  894.     if lname == nname and lfdid then
  895.       local truename = addon:UpdateInstance(lfdid)
  896.       if truename then
  897.         return truename, lfdid
  898.       end
  899.     end
  900.   end
  901.   -- normalized substring match
  902.   for truename, info in pairs(vars.db.Instances) do
  903.     local tname = addon:normalizeName(truename)
  904.     if (tname:find(nname, 1, true) or nname:find(tname, 1, true)) and
  905.       info.Raid == raid then -- Tempest Keep: The Botanica
  906.       --debug("FindInstance("..name..") => "..truename)
  907.       return truename, info.LFDID
  908.     end
  909.   end
  910.   return nil
  911. end
  912.  
  913. -- provide either id or name/raid to get the instance truename and db entry
  914. function addon:LookupInstance(id, name, raid)
  915.   --debug("LookupInstance("..(id or "nil")..","..(name or "nil")..","..(raid and "true" or "false")..")")
  916.   local truename, instance
  917.   if name then
  918.     truename, id = addon:FindInstance(name, raid)
  919.   end
  920.   if id then
  921.     truename = addon:UpdateInstance(id)
  922.   end
  923.   if truename then
  924.     instance = vars.db.Instances[truename]
  925.   end
  926.   if not instance then
  927.     debug("LookupInstance() failed to find instance: "..(name or "")..":"..(id or 0).." : "..GetLocale())
  928.     addon.warned = addon.warned or {}
  929.     if not addon.warned[name] then
  930.       addon.warned[name] = true
  931.       local lid
  932.       for i = 1, GetNumSavedInstances() do
  933.         local link = GetSavedInstanceChatLink(i) or ""
  934.         local tlid,tlname = link:match(":(%d+):%d+:%d+\124h%[(.+)%]\124h")
  935.         if tlname == name then lid = tlid end
  936.       end
  937.       bugReport("SavedInstances: ERROR: Refresh() failed to find instance: "..name.." : "..GetLocale().." : "..(lid or "x"))
  938.     end
  939.     instance = {}
  940.     --vars.db.Instances[name] = instance
  941.   end
  942.   return truename, instance
  943. end
  944.  
  945. function addon:InstanceCategory(instance)
  946.   if not instance then return nil end
  947.   local instance = vars.db.Instances[instance]
  948.   if instance.Holiday then return "H" end
  949.   if instance.Random then return "N" end
  950.   return ((instance.Raid and "R") or ((not instance.Raid) and "D")) .. instance.Expansion
  951. end
  952.  
  953. function addon:InstancesInCategory(targetcategory)
  954.   -- returns a table of the form { "instance1", "instance2", ... }
  955.   if (not targetcategory) then return { } end
  956.   local list = { }
  957.   for instance, _ in pairs(vars.db.Instances) do
  958.     if addon:InstanceCategory(instance) == targetcategory then
  959.       table.insert(list, instance)
  960.     end
  961.   end
  962.   return list
  963. end
  964.  
  965. function addon:CategorySize(category)
  966.   if not category then return nil end
  967.   local i = 0
  968.   for instance, _ in pairs(vars.db.Instances) do
  969.     if category == addon:InstanceCategory(instance) then
  970.       i = i + 1
  971.     end
  972.   end
  973.   return i
  974. end
  975.  
  976. local _instance_exceptions = {
  977.   -- workaround a Blizzard bug:
  978.   -- since 5.0, some old raid lockout tooltips are missing boss kill info
  979.   -- currently affects 25+ man BC/Vanilla raids (but not Kara or AQ Ruins, go figure)
  980.   -- starting in 6.1 we have the kill bitmap but no boss names
  981.   [48] = { -- Molten Core
  982.     12118, -- Lucifron
  983.     11982, -- Magmadar
  984.     12259, -- Gehennas
  985.     12057, -- Garr
  986.     12264, -- Shazzrah
  987.     12056, -- Baron Geddon
  988.     12098, -- Sulfuron Harbinger
  989.     11988, -- Golemagg the Incinerator
  990.     12018, -- Majordomo Executus
  991.     11502, -- Ragnaros
  992.   },
  993.   [50] = { -- Blackwing Lair
  994.     12435, -- Razorgore the Untamed
  995.     13020, -- Vaelastrasz the Corrupt
  996.     12017, -- Boodlord Lashlayer
  997.     11983, -- Firemaw
  998.     14601, -- Ebonroc
  999.     11981, -- Flamegor
  1000.     14020, -- Chromaggus
  1001.     11583, -- Nefarian
  1002.   },
  1003.   [161] = { -- Ahn'Qiraj Temple
  1004.     15263, -- Prophet Skeram
  1005.     15543, -- Princess Yauj (also Vem and Lord Kri)
  1006.     15516, -- Bodyguard Sartura
  1007.     15510, -- Fankriss the Unyielding
  1008.     15299, -- Viscidus
  1009.     15509, -- Princess Huhuran
  1010.     15276, -- Emperor Vek'lor
  1011.     15517, -- Ouro
  1012.     15727, -- C'Thun
  1013.   },
  1014.   [176] = { -- Magtheridon's Lair
  1015.     17257, -- Magtheridon
  1016.   },
  1017.   [177] = { -- Gruul's Lair
  1018.     18831, -- High King Maulgar
  1019.     19044, -- Gruul
  1020.   },
  1021.   [193] = { -- Tempest Keep
  1022.     19514, -- A'lar
  1023.     19516, -- Void Reaver
  1024.     18805, -- High Astromancer Solarian
  1025.     19622, -- Kael'thas Sunstrider
  1026.   },
  1027.   [194] = { -- Serpentshrine Cavern
  1028.     21216, -- Hydross the Unstable
  1029.     21217, -- The Lurker Below
  1030.     21215, -- Leotheras the Blind
  1031.     21214, -- Fathom-Lord Karathress
  1032.     21213, -- Morogrim Tidewalker
  1033.     21212, -- Lady Vashj
  1034.   },
  1035.   [195] = { -- Hyjal Past
  1036.     17767, -- Rage Winterchill
  1037.     17808, -- Anetheron
  1038.     17888, -- Kaz'rogal
  1039.     17842, -- Azgalor
  1040.     17968, -- Archimonde
  1041.   },
  1042.   [196] = { -- Black Temple
  1043.     22887, -- High Warlord Naj'entus
  1044.     22898, -- Supremus
  1045.     22841, -- Shade of Akama
  1046.     22871, -- Teron Gorefiend
  1047.     22948, -- Gurtogg Bloodboil
  1048.     22856, -- Reliquary of Souls
  1049.     22947, -- Mother Shahraz
  1050.     23426, -- Illidari Council
  1051.     22917, -- Illidan Stormrage
  1052.   },
  1053.   [199] = { -- Sunwell
  1054.     24850, -- Kalecgos
  1055.     24882, -- Brutallus
  1056.     25038, -- Felmyst
  1057.     25166, -- Grand Warlock Alythess
  1058.     25741, -- M'uru
  1059.     25315, -- Kil'jaeden
  1060.   },
  1061. }
  1062. function addon:instanceException(LFDID)
  1063.   if not LFDID then return nil end
  1064.   local exc = _instance_exceptions[LFDID]
  1065.   if exc then -- localize boss names
  1066.     local total = 0
  1067.     for idx, id in ipairs(exc) do
  1068.       if type(id) == "number" then
  1069.         scantt:SetOwner(UIParent,"ANCHOR_NONE")
  1070.         scantt:SetHyperlink(("unit:Creature-0-0-0-0-%d:0000000000"):format(id))
  1071.         local line = scantt:IsShown() and _G[scantt:GetName().."TextLeft1"]
  1072.         line = line and line:GetText()
  1073.         if line and #line > 0 then
  1074.           exc[idx] = line
  1075.         end
  1076.       end
  1077.       total = total + 1
  1078.     end
  1079.     exc.total = total
  1080.   end
  1081.   return exc
  1082. end
  1083.  
  1084. function addon:instanceBosses(instance,toon,diff)
  1085.   local killed,total,base = 0,0,1
  1086.   local remap = nil
  1087.   local inst = vars.db.Instances[instance]
  1088.   local save = inst and inst[toon] and inst[toon][diff]
  1089.   if inst.WorldBoss then
  1090.     return (save[1] and 1 or 0), 1, 1
  1091.   end
  1092.   if not inst or not inst.LFDID then return 0,0,1 end
  1093.   local exc = addon:instanceException(inst.LFDID)
  1094.   total = (exc and exc.total) or GetLFGDungeonNumEncounters(inst.LFDID)
  1095.   local LFR = addon.LFRInstances[inst.LFDID]
  1096.   if LFR then
  1097.     total = LFR.total or total
  1098.     base = LFR.base or base
  1099.     remap = LFR.remap
  1100.   end
  1101.   if not save then
  1102.     return killed, total, base, remap
  1103.   elseif save.Link then
  1104.     local bits = save.Link:match(":(%d+)\124h")
  1105.     bits = bits and tonumber(bits)
  1106.     if bits then
  1107.       while bits > 0 do
  1108.         if bit.band(bits,1) > 0 then
  1109.           killed = killed + 1
  1110.         end
  1111.         bits = bit.rshift(bits,1)
  1112.       end
  1113.     end
  1114.   elseif save.ID < 0 then
  1115.     for i=1,-1*save.ID do
  1116.       killed = killed + (save[i] and 1 or 0)
  1117.     end
  1118.   end
  1119.   return killed, total, base, remap
  1120. end
  1121.  
  1122. local lfrkey = "^"..L["LFR"]..": "
  1123. local function instanceSort(i1, i2)
  1124.   local instance1 = vars.db.Instances[i1]
  1125.   local instance2 = vars.db.Instances[i2]
  1126.   local level1 = instance1.RecLevel or 0
  1127.   local level2 = instance2.RecLevel or 0
  1128.   local id1 = instance1.LFDID or instance1.WorldBoss or 0
  1129.   local id2 = instance2.LFDID or instance2.WorldBoss or 0
  1130.   local key1 = level1*1000000+id1
  1131.   local key2 = level2*1000000+id2
  1132.   if i1:match(lfrkey) then key1 = key1 - 20000 end
  1133.   if i2:match(lfrkey) then key2 = key2 - 20000 end
  1134.   if instance1.WorldBoss then key1 = key1 - 30000 end
  1135.   if instance2.WorldBoss then key2 = key2 - 30000 end
  1136.   if vars.db.Tooltip.ReverseInstances then
  1137.     return key1 < key2
  1138.   else
  1139.     return key2 < key1
  1140.   end
  1141. end
  1142.  
  1143. addon.oi_cache = {}
  1144. function addon:OrderedInstances(category)
  1145.   -- returns a table of the form { "instance1", "instance2", ... }
  1146.   local instances = addon.oi_cache[category]
  1147.   if not instances then
  1148.     instances = addon:InstancesInCategory(category)
  1149.     table.sort(instances, instanceSort)
  1150.     if addon.instancesUpdated then
  1151.       addon.oi_cache[category] = instances
  1152.     end
  1153.   end
  1154.   return instances
  1155. end
  1156.  
  1157. function addon:OrderedCategories()
  1158.   -- returns a table of the form { "category1", "category2", ... }
  1159.   if addon.oc_cache then return addon.oc_cache end
  1160.   local orderedlist = { }
  1161.   local firstexpansion, lastexpansion, expansionstep, firsttype, lasttype
  1162.   if vars.db.Tooltip.NewFirst then
  1163.     firstexpansion = maxExpansion
  1164.     lastexpansion = 0
  1165.     expansionstep = -1
  1166.   else
  1167.     firstexpansion = 0
  1168.     lastexpansion = maxExpansion
  1169.     expansionstep = 1
  1170.   end
  1171.   if vars.db.Tooltip.RaidsFirst then
  1172.     firsttype = "R"
  1173.     lasttype = "D"
  1174.   else
  1175.     firsttype = "D"
  1176.     lasttype = "R"
  1177.   end
  1178.   for i = firstexpansion, lastexpansion, expansionstep do
  1179.     table.insert(orderedlist, firsttype .. i)
  1180.     if vars.db.Tooltip.CategorySort == "EXPANSION" then
  1181.       table.insert(orderedlist, lasttype .. i)
  1182.     end
  1183.   end
  1184.   if vars.db.Tooltip.CategorySort == "TYPE" then
  1185.     for i = firstexpansion, lastexpansion, expansionstep do
  1186.       table.insert(orderedlist, lasttype .. i)
  1187.     end
  1188.   end
  1189.   addon.oc_cache = orderedlist
  1190.   return orderedlist
  1191. end
  1192.  
  1193. local function DifficultyString(instance, diff, toon, expired, killoverride, totoverride)
  1194.   local setting,color
  1195.   if not instance then
  1196.     setting = "D1"
  1197.   else
  1198.     local inst = vars.db.Instances[instance]
  1199.     if not inst or not inst.Raid then -- 5-man
  1200.       if diff == 2 then -- heroic
  1201.         setting = "D2"
  1202.     elseif diff == 23 then -- mythic
  1203.       setting = "D3"
  1204.     else -- normal?
  1205.       setting = "D1"
  1206.     end
  1207.     elseif inst.Expansion == 0 then -- classic raid
  1208.       setting = "R0"
  1209.     elseif diff >= 3 and diff <= 7 then -- pre-WoD raids
  1210.       setting = "R"..(diff-2)
  1211.     elseif diff >= 14 and diff <= 16 then -- WoD raids
  1212.       setting = "R"..(diff-8)
  1213.     else -- don't know
  1214.       setting = "D1"
  1215.     end
  1216.   end
  1217.   local prefs = vars.db.Indicators
  1218.   local classcolor = prefs[setting .. "ClassColor"]
  1219.   if classcolor == nil then
  1220.     classcolor = vars.defaultDB.Indicators[setting .. "ClassColor"]
  1221.   end
  1222.   if expired then
  1223.     color = GRAY_COLOR
  1224.   elseif classcolor then
  1225.     color = (CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS)[vars.db.Toons[toon].Class]
  1226.   else
  1227.     prefs[setting.."Color"]  = prefs[setting.."Color"] or vars.defaultDB.Indicators[setting.."Color"]
  1228.     color = prefs[setting.."Color"]
  1229.   end
  1230.   local text = prefs[setting.."Text"] or vars.defaultDB.Indicators[setting.."Text"]
  1231.   local indicator = prefs[setting.."Indicator"] or vars.defaultDB.Indicators[setting.."Indicator"]
  1232.   text = ColorCodeOpen(color) .. text .. FONTEND
  1233.   if text:find("ICON", 1, true) and indicator ~= "BLANK" then
  1234.     text = text:gsub("ICON", FONTEND .. vars.Indicators[indicator] .. ColorCodeOpen(color))
  1235.   end
  1236.   if text:find("KILLED", 1, true) or text:find("TOTAL", 1, true) then
  1237.     local killed, total
  1238.     if killoverride then
  1239.       killed, total = killoverride, totoverride
  1240.     else
  1241.       killed, total = addon:instanceBosses(instance,toon,diff)
  1242.     end
  1243.     if killed == 0 and total == 0 then -- boss kill info missing
  1244.       killed = "*"
  1245.       total = "*"
  1246.     elseif killed == 1 and total == 1 and not expired then
  1247.       text = "\124T"..READY_CHECK_READY_TEXTURE..":0|t" -- checkmark
  1248.     end
  1249.     text = text:gsub("KILLED",killed)
  1250.     text = text:gsub("TOTAL",total)
  1251.   end
  1252.   return text
  1253. end
  1254.  
  1255. -- run about once per session to update our database of instance info
  1256. function addon:UpdateInstanceData()
  1257.   --debug("UpdateInstanceData()")
  1258.   if addon.instancesUpdated then return end  -- nil before first use in UI
  1259.   addon.instancesUpdated = true
  1260.   local added = 0
  1261.   local lfdid_to_name = {}
  1262.   local wbid_to_name = {}
  1263.   local id_blacklist = {}
  1264.   local starttime = debugprofilestop()
  1265.   local maxid = 1500
  1266.   -- previously we used GetFullRaidList() and LFDDungeonList to help populate the instance list
  1267.   -- Unfortunately those are loaded lazily, and forcing them to load from here can lead to taint.
  1268.   -- They are also somewhat incomplete, so instead we just brute force it, which is reasonably fast anyhow
  1269.   for id=1,maxid do
  1270.     local instname, newentry, blacklist = addon:UpdateInstance(id)
  1271.     if newentry then
  1272.       added = added + 1
  1273.     end
  1274.     if blacklist then
  1275.       id_blacklist[id] = true
  1276.     end
  1277.     if instname then
  1278.       if lfdid_to_name[id] then
  1279.         debug("Duplicate entry in lfdid_to_name: "..id..":"..lfdid_to_name[id]..":"..instname)
  1280.       end
  1281.       lfdid_to_name[id] = instname
  1282.     end
  1283.   end
  1284.   for eid,info in pairs(addon.WorldBosses) do
  1285.     info.eid = eid
  1286.     if not info.name then
  1287.       info.name = select(2,EJ_GetCreatureInfo(1,eid))
  1288.     end
  1289.     info.name = info.name or "UNKNOWN"..eid
  1290.     local instance = vars.db.Instances[info.name]
  1291.     if info.remove then -- cleanup hook
  1292.       vars.db.Instances[info.name] = nil
  1293.       addon.WorldBosses[eid] = nil
  1294.     else
  1295.       if not instance then
  1296.         added = added + 1
  1297.         instance = {}
  1298.         vars.db.Instances[info.name] = instance
  1299.       end
  1300.       instance.Show = instance.Show or "saved"
  1301.       instance.WorldBoss = eid
  1302.       instance.Expansion = info.expansion
  1303.       instance.RecLevel = info.level
  1304.       instance.Raid = true
  1305.       wbid_to_name[eid] = info.name
  1306.     end
  1307.   end
  1308.  
  1309.   -- instance merging: this algorithm removes duplicate entries created by client locale changes using the same database
  1310.   -- we really should re-key the database by ID, but this is sufficient for now
  1311.   local renames = 0
  1312.   local merges = 0
  1313.   local conflicts = 0
  1314.   for instname, inst in pairs(vars.db.Instances) do
  1315.     local truename
  1316.     if inst.WorldBoss then
  1317.       truename = wbid_to_name[inst.WorldBoss]
  1318.     elseif inst.LFDID then
  1319.       truename = lfdid_to_name[inst.LFDID]
  1320.     else
  1321.       debug("Ignoring bogus entry in instance database: "..instname)
  1322.     end
  1323.     if not truename then
  1324.       if inst.LFDID and id_blacklist[inst.LFDID] then
  1325.         debug("Removing blacklisted entry in instance database: "..instname)
  1326.         vars.db.Instances[instname] = nil
  1327.       else
  1328.         debug("Ignoring unmatched entry in instance database: "..instname)
  1329.       end
  1330.     elseif instname == truename then
  1331.     -- this is the canonical entry, nothing to do
  1332.     else -- this is a stale entry, merge data and remove it
  1333.       local trueinst = vars.db.Instances[truename]
  1334.       if not trueinst or trueinst == inst then
  1335.         debug("Merge error in UpdateInstanceData: "..truename)
  1336.       else
  1337.         for key, info in pairs(inst) do
  1338.           if key:find(" - ") then -- is a character key
  1339.             if trueinst[key] then
  1340.               -- merge conflict: keep the trueinst data
  1341.               debug("Merge conflict on "..truename..":"..instname..":"..key)
  1342.               conflicts = conflicts + 1
  1343.           else
  1344.             trueinst[key] = info
  1345.             merges = merges + 1
  1346.           end
  1347.           end
  1348.         end
  1349.         -- copy config settings, favoring old entry
  1350.         trueinst.Show = inst.Show or trueinst.Show
  1351.         -- clear stale entry
  1352.         vars.db.Instances[instname] = nil
  1353.         renames = renames + 1
  1354.       end
  1355.     end
  1356.   end
  1357.   -- addon.lfdid_to_name = lfdid_to_name
  1358.   -- addon.wbid_to_name = wbid_to_name
  1359.  
  1360.   vars.config:BuildOptions() -- refresh config table
  1361.  
  1362.   starttime = debugprofilestop()-starttime
  1363.   debug("UpdateInstanceData(): completed in %.3f ms : %d added, %d renames, %d merges, %d conflicts.",
  1364.     starttime, added, renames, merges, conflicts)
  1365.   if addon.RefreshPending then
  1366.     addon.RefreshPending = nil
  1367.     core:Refresh()
  1368.   end
  1369. end
  1370.  
  1371. --if LFDParentFrame then hooksecurefunc(LFDParentFrame,"Show",function() addon:UpdateInstanceData() end) end
  1372. function addon:UpdateInstance(id)
  1373.   -- returns: <instance_name>, <is_new_instance>, <blacklisted_id>
  1374.   --debug("UpdateInstance: "..id)
  1375.   if not id or id <= 0 then return end
  1376.   local name, typeID, subtypeID,
  1377.     minLevel, maxLevel, recLevel, minRecLevel, maxRecLevel,
  1378.     expansionLevel, groupID, textureFilename,
  1379.     difficulty, maxPlayers, description, isHoliday = GetLFGDungeonInfo(id)
  1380.   -- name is nil for non-existent ids
  1381.   -- isHoliday is for single-boss holiday instances that don't generate raid saves
  1382.   -- typeID 4 = outdoor area, typeID 6 = random
  1383.   maxPlayers = tonumber(maxPlayers)
  1384.   if not name or not expansionLevel or not recLevel or (typeID > 2 and typeID ~= TYPEID_RANDOM_DUNGEON) then return end
  1385.   if name:find(PVP_RATED_BATTLEGROUND) then return nil, nil, true end -- ignore 10v10 rated bg
  1386.   if subtypeID == LFG_SUBTYPEID_SCENARIO and typeID ~= TYPEID_RANDOM_DUNGEON then -- ignore non-random scenarios
  1387.     return nil, nil, true
  1388.   end
  1389.   if typeID == 2 and subtypeID == 0 and difficulty == 14 and maxPlayers == 0 then
  1390.     --print("ignoring "..id, GetLFGDungeonInfo(id))
  1391.     return nil, nil, true -- ignore bogus LFR entries
  1392.   end
  1393.   if typeID == 1 and subtypeID == 5 and difficulty == 14 and maxPlayers == 25 then
  1394.     --print("ignoring "..id, GetLFGDungeonInfo(id))
  1395.     return nil, nil, true -- ignore old Flex entries
  1396.   end
  1397.   if addon.LFRInstances[id] then -- ensure uniqueness (eg TeS LFR)
  1398.     local lfrid = vars.db.Instances[name] and vars.db.Instances[name].LFDID
  1399.     if lfrid and addon.LFRInstances[lfrid] then
  1400.       vars.db.Instances[name] = nil -- clean old LFR entries
  1401.     end
  1402.     vars.db.Instances[L["Flex"]..": "..name] = nil -- clean old flex entries
  1403.     name = L["LFR"]..": "..name
  1404.   end
  1405.   if id == 852 and expansionLevel == 5 then -- XXX: Molten Core hack
  1406.     return nil, nil, true -- ignore Molten Core holiday version, which has no save
  1407.   end
  1408.   if id == 767 then -- ignore bogus Ordos entry
  1409.     return nil, nil, true
  1410.   end
  1411.   if id == 768 then -- ignore bogus Celestials entry
  1412.     return nil, nil, true
  1413.   end
  1414.  
  1415.   local instance = vars.db.Instances[name]
  1416.   local newinst = false
  1417.   if not instance then
  1418.     debug("UpdateInstance: "..id.." "..(name or "nil").." "..(expansionLevel or "nil").." "..(recLevel or "nil").." "..(maxPlayers or "nil"))
  1419.     instance = {}
  1420.     newinst = true
  1421.   end
  1422.   vars.db.Instances[name] = instance
  1423.   instance.Show = instance.Show or "saved"
  1424.   instance.Encounters = nil -- deprecated
  1425.   instance.LFDupdated = nil
  1426.   instance.LFDID = id
  1427.   instance.Holiday = isHoliday or nil
  1428.   instance.Expansion = expansionLevel
  1429.   if not instance.RecLevel or instance.RecLevel < 1 then instance.RecLevel = recLevel end
  1430.   if recLevel > 0 and recLevel < instance.RecLevel then instance.RecLevel = recLevel end -- favor non-heroic RecLevel
  1431.   instance.Raid = (maxPlayers > 5 or (maxPlayers == 0 and typeID == 2))
  1432.   if typeID == TYPEID_RANDOM_DUNGEON then
  1433.     instance.Random = true
  1434.   end
  1435.   if subtypeID == LFG_SUBTYPEID_SCENARIO then
  1436.     instance.Scenario = true
  1437.   end
  1438.   return name, newinst
  1439. end
  1440.  
  1441. function addon:updateSpellTip(spellid)
  1442.   local slot
  1443.   vars.db.spelltip = vars.db.spelltip or {}
  1444.   vars.db.spelltip[spellid] = vars.db.spelltip[spellid] or {}
  1445.   for i=1,20 do
  1446.     local id = select(11,UnitDebuff("player",i))
  1447.     if id == spellid then slot = i end
  1448.   end
  1449.   if slot then
  1450.     scantt:SetOwner(UIParent,"ANCHOR_NONE")
  1451.     scantt:SetUnitDebuff("player",slot)
  1452.     for i=1,scantt:NumLines()-1 do
  1453.       local left = _G[scantt:GetName().."TextLeft"..i]
  1454.       vars.db.spelltip[spellid][i] = left:GetText()
  1455.     end
  1456.   end
  1457. end
  1458.  
  1459. -- run regularly to update lockouts and cached data for this toon
  1460. function addon:UpdateToonData()
  1461.   addon.activeHolidays = addon.activeHolidays or {}
  1462.   wipe(addon.activeHolidays)
  1463.   -- blizz internally conflates all the holiday flags, so we have to detect which is really active
  1464.   for i=1, GetNumRandomDungeons() do
  1465.     local id, name = GetLFGRandomDungeonInfo(i);
  1466.     local d = vars.db.Instances[name]
  1467.     if d and d.Holiday then
  1468.       addon.activeHolidays[name] = true
  1469.     end
  1470.   end
  1471.  
  1472.   local nextreset = addon:GetNextDailyResetTime()
  1473.   for instance, i in pairs(vars.db.Instances) do
  1474.     for toon, t in pairs(vars.db.Toons) do
  1475.       if i[toon] then
  1476.         for difficulty, d in pairs(i[toon]) do
  1477.           if d.Expires and d.Expires < time() then
  1478.             d.Locked = false
  1479.             d.Expires = 0
  1480.             if d.ID < 0 then
  1481.               i[toon][difficulty] = nil
  1482.             end
  1483.           end
  1484.         end
  1485.       end
  1486.     end
  1487.     if (i.Holiday and addon.activeHolidays[instance]) or
  1488.       (i.Random and not i.Holiday) then
  1489.       local id = i.LFDID
  1490.       GetLFGDungeonInfo(id) -- forces update
  1491.       local donetoday, money = GetLFGDungeonRewards(id)
  1492.       if donetoday and i.Random and (
  1493.         (i.LFDID == 258) or  -- random classic dungeon
  1494.         (i.LFDID == 995 or i.LFDID == 744) or  -- timewalking dungeons
  1495.         (UnitLevel("player") == 85 and
  1496.         (i.LFDID == 300 or i.LFDID == 301 or i.LFDID == 434)) -- reg/her cata and HoT at 85
  1497.         ) then -- donetoday flag is falsely set for some level/dungeon combos where no daily incentive is available
  1498.         donetoday = false
  1499.       end
  1500.       if nextreset and donetoday and (i.Holiday or (money and money > 0)) then
  1501.         i[thisToon] = i[thisToon] or {}
  1502.         i[thisToon][1] = i[thisToon][1] or {}
  1503.         local d = i[thisToon][1]
  1504.         d.ID = -1
  1505.         d.Locked = false
  1506.         d.Expires = nextreset
  1507.       end
  1508.     end
  1509.   end
  1510.   -- update random toon info
  1511.   local t = vars.db.Toons[thisToon]
  1512.   local now = time()
  1513.   if addon.logout or addon.PlayedTime or addon.playedpending then
  1514.     if addon.PlayedTime then
  1515.       local more = now - addon.PlayedTime
  1516.       t.PlayedTotal = t.PlayedTotal + more
  1517.       t.PlayedLevel = t.PlayedLevel + more
  1518.       addon.PlayedTime = now
  1519.     end
  1520.   else
  1521.     addon.playedpending = true
  1522.     addon.playedreg = addon.playedreg or {}
  1523.     wipe(addon.playedreg)
  1524.     for i=1,10 do
  1525.       local c = _G["ChatFrame"..i]
  1526.       if c and c:IsEventRegistered("TIME_PLAYED_MSG") then
  1527.         c:UnregisterEvent("TIME_PLAYED_MSG") -- prevent spam
  1528.         addon.playedreg[c] = true
  1529.       end
  1530.     end
  1531.     RequestTimePlayed()
  1532.   end
  1533.   t.LFG1 = GetTimeToTime(GetLFGRandomCooldownExpiration()) or t.LFG1
  1534.   t.LFG2 = GetTimeToTime(select(7,UnitDebuff("player",GetSpellInfo(71041)))) or t.LFG2 -- GetLFGDeserterExpiration()
  1535.   if t.LFG2 then addon:updateSpellTip(71041) end
  1536.   addon.pvpdesertids = addon.pvpdesertids or { 26013,   -- BG queue
  1537.     194958 } -- Ashran
  1538.   for _,id in ipairs(addon.pvpdesertids) do
  1539.     t.pvpdesert = GetTimeToTime(select(7,UnitDebuff("player",GetSpellInfo(id)))) or t.pvpdesert
  1540.     if t.pvpdesert then addon:updateSpellTip(id) end
  1541.     end
  1542.     for toon, ti in pairs(vars.db.Toons) do
  1543.       if ti.LFG1 and (ti.LFG1 < now) then ti.LFG1 = nil end
  1544.       if ti.LFG2 and (ti.LFG2 < now) then ti.LFG2 = nil end
  1545.       if ti.pvpdesert and (ti.pvpdesert < now) then ti.pvpdesert = nil end
  1546.       ti.Quests = ti.Quests or {}
  1547.     end
  1548.     local IL,ILe = GetAverageItemLevel()
  1549.     if IL and tonumber(IL) and tonumber(IL) > 0 then -- can fail during logout
  1550.       t.IL, t.ILe = tonumber(IL), tonumber(ILe)
  1551.     end
  1552.     local rating = (GetPersonalRatedBGInfo and GetPersonalRatedBGInfo()) -- 5.3
  1553.       or (GetPersonalRatedInfo and GetPersonalRatedInfo(4))
  1554.     t.RBGrating = tonumber(rating) or t.RBGrating
  1555.     core:scan_item_cds()
  1556.     -- Daily Reset
  1557.     if nextreset and nextreset > time() then
  1558.       for toon, ti in pairs(vars.db.Toons) do
  1559.         if not ti.DailyResetTime or (ti.DailyResetTime < time()) then
  1560.           for id,qi in pairs(ti.Quests) do
  1561.             if qi.isDaily then
  1562.               ti.Quests[id] = nil
  1563.             end
  1564.           end
  1565.           ti.DailyResetTime = (ti.DailyResetTime and ti.DailyResetTime + 24*3600) or nextreset
  1566.         end
  1567.       end
  1568.       t.DailyResetTime = nextreset
  1569.       if not db.DailyResetTime or (db.DailyResetTime < time()) then -- AccountDaily reset
  1570.         for id,qi in pairs(db.Quests) do
  1571.           if qi.isDaily then
  1572.             db.Quests[id] = nil
  1573.           end
  1574.       end
  1575.       db.DailyResetTime = nextreset
  1576.       end
  1577.     end
  1578.     -- Skill Reset
  1579.     for toon, ti in pairs(vars.db.Toons) do
  1580.       if ti.Skills then
  1581.         for spellid, sinfo in pairs(ti.Skills) do
  1582.           if sinfo.Expires and sinfo.Expires < time() then
  1583.             ti.Skills[spellid] = nil
  1584.           end
  1585.         end
  1586.       end
  1587.       if ti.FarmExpires and ti.FarmExpires < time() then
  1588.         ti.FarmPlanted = 0
  1589.         ti.FarmHarvested = 0
  1590.         if ti.FarmCropPlanted and next(ti.FarmCropPlanted) then
  1591.           ti.FarmCropReady = ti.FarmCropPlanted
  1592.           ti.FarmCropPlanted = nil
  1593.         end
  1594.         ti.FarmExpires = nil
  1595.       end
  1596.     end
  1597.     -- Weekly Reset
  1598.     local nextreset = addon:GetNextWeeklyResetTime()
  1599.     if nextreset and nextreset > time() then
  1600.       for toon, ti in pairs(vars.db.Toons) do
  1601.         if not ti.WeeklyResetTime or (ti.WeeklyResetTime < time()) then
  1602.           ti.currency = ti.currency or {}
  1603.           for _,idx in ipairs(currency) do
  1604.             local ci = ti.currency[idx]
  1605.             if ci and ci.earnedThisWeek then
  1606.               ci.earnedThisWeek = 0
  1607.             end
  1608.           end
  1609.           ti.WeeklyResetTime = (ti.WeeklyResetTime and ti.WeeklyResetTime + 7*24*3600) or nextreset
  1610.         end
  1611.       end
  1612.       t.WeeklyResetTime = nextreset
  1613.     end
  1614.     for toon, ti in pairs(vars.db.Toons) do
  1615.       for id,qi in pairs(ti.Quests) do
  1616.         if not qi.isDaily and (qi.Expires or 0) < time() then
  1617.           ti.Quests[id] = nil
  1618.         end
  1619.         if QuestExceptions[id] == "Regular" then -- adjust exceptions
  1620.           ti.Quests[id] = nil
  1621.         end
  1622.       end
  1623.     end
  1624.     for id,qi in pairs(db.Quests) do -- AccountWeekly reset
  1625.       if not qi.isDaily and (qi.Expires or 0) < time() then
  1626.         db.Quests[id] = nil
  1627.     end
  1628.     end
  1629.     addon:UpdateCurrency()
  1630.     local zone = GetRealZoneText()
  1631.     if zone and #zone > 0 then
  1632.       t.Zone = zone
  1633.     end
  1634.     local lrace, race = UnitRace("player")
  1635.     local faction, lfaction = UnitFactionGroup("player")
  1636.     t.Faction = faction
  1637.     if race == "Pandaren" then
  1638.       t.Race = lrace.." ("..lfaction..")"
  1639.     else
  1640.       t.Race = lrace
  1641.     end
  1642.  
  1643.     t.LastSeen = time()
  1644. end
  1645.  
  1646. function addon:UpdateCurrency()
  1647.   if addon.logout then return end -- currency is unreliable during logout
  1648.   local t = vars.db.Toons[thisToon]
  1649.   t.Money = GetMoney()
  1650.   t.currency = wipe(t.currency or {})
  1651.   for _,idx in ipairs(currency) do
  1652.     local _, amount, _, earnedThisWeek, weeklyMax, totalMax, discovered = GetCurrencyInfo(idx)
  1653.     if idx == 390 and amount == 0 then
  1654.       discovered = false -- discovery flag broken for conquest points
  1655.     end
  1656.     if not discovered then
  1657.       t.currency[idx] = nil
  1658.     else
  1659.       local ci = t.currency[idx] or {}
  1660.       ci.amount, ci.earnedThisWeek, ci.weeklyMax, ci.totalMax = amount, earnedThisWeek, weeklyMax, totalMax
  1661.       if idx == 396 then -- VP has a weekly max scaled by 100
  1662.         ci.weeklyMax = ci.weeklyMax and math.floor(ci.weeklyMax/100)
  1663.       end
  1664.       if idx == 390 or idx == 395 or idx == 396 then -- these have a total max scaled by 100
  1665.         ci.totalMax = ci.totalMax and math.floor(ci.totalMax/100)
  1666.       end
  1667.       if idx == 390 then -- these have a weekly earned scaled by 100
  1668.         ci.earnedThisWeek = ci.earnedThisWeek and math.floor(ci.earnedThisWeek/100)
  1669.       end
  1670.       if idx == 1129 then -- Seal of Tempered Fate returns zero for weekly quantities
  1671.         ci.weeklyMax = 3 -- the max via quests
  1672.         ci.earnedThisWeek = 0
  1673.         for id in pairs(WoDSealQuests) do
  1674.           if IsQuestFlaggedCompleted(id) then
  1675.             ci.earnedThisWeek = ci.earnedThisWeek + 1
  1676.           end
  1677.         end
  1678.       end
  1679.       ci.season = addon:GetSeasonCurrency(idx)
  1680.       if ci.weeklyMax == 0 then ci.weeklyMax = nil end -- don't store useless info
  1681.       if ci.totalMax == 0 then ci.totalMax = nil end -- don't store useless info
  1682.       if ci.earnedThisWeek == 0 then ci.earnedThisWeek = nil end -- don't store useless info
  1683.       t.currency[idx] = ci
  1684.     end
  1685.   end
  1686. end
  1687.  
  1688. function addon:QuestIsDarkmoonMonthly()
  1689.   if QuestIsDaily() then return false end
  1690.   local id = GetQuestID()
  1691.   local scope = id and QuestExceptions[id]
  1692.   if scope and scope ~= "Darkmoon" then return false end -- one-time referral quests
  1693.   for i=1,GetNumRewardCurrencies() do
  1694.     local name,texture,amount = GetQuestCurrencyInfo("reward",i)
  1695.     if texture:find("_ticket_darkmoon_") then
  1696.       return true
  1697.     end
  1698.   end
  1699.   return false
  1700. end
  1701.  
  1702. function addon:GetCurrentMapAreaID()
  1703.   local oldmap = GetCurrentMapAreaID()
  1704.   local oldlvl = GetCurrentMapDungeonLevel()
  1705.   SetMapToCurrentZone()
  1706.   local map = GetCurrentMapAreaID()
  1707.   SetMapByID(oldmap)
  1708.   if oldlvl and oldlvl > 0 then
  1709.     SetDungeonMapLevel(oldlvl)
  1710.   end
  1711.   return map
  1712. end
  1713.  
  1714. local function SI_GetQuestReward()
  1715.   local t = vars and vars.db.Toons[thisToon]
  1716.   if not t then return end
  1717.   local id = GetQuestID() or -1
  1718.   local title = GetTitleText() or ""
  1719.   local link = nil
  1720.   local isMonthly = addon:QuestIsDarkmoonMonthly()
  1721.   local isWeekly = QuestIsWeekly()
  1722.   local isDaily = QuestIsDaily()
  1723.   local isAccount
  1724.  
  1725.   local index = GetQuestLogIndexByID(id)
  1726.   if index and index > 0 then
  1727.     link = GetQuestLink(index)
  1728.   end
  1729.   if id > 1 then -- try harder to fetch names
  1730.     local t,l = addon:QuestInfo(id)
  1731.     if not (link and #link > 0) then
  1732.       link = l
  1733.     end
  1734.     if not (title and #title > 0) then
  1735.       title = t or "<unknown>"
  1736.     end
  1737.   end
  1738.   local questTagID, tagName = GetQuestTagInfo(id)
  1739.   if questTagID and tagName then
  1740.     isAccount = (questTagID == QUEST_TAG_ACCOUNT)
  1741.   else
  1742.     isAccount = db.QuestDB.AccountDaily[id] or db.QuestDB.AccountWeekly[id]
  1743.     debug("Fetched isAccount")
  1744.   end
  1745.   if QuestExceptions[id] then
  1746.     local qe = QuestExceptions[id]
  1747.     isAccount = qe:find("Account") and true
  1748.     isDaily =   qe:find("Daily") and true
  1749.     isWeekly =  qe:find("Weekly") and true
  1750.     isMonthly = qe:find("Darkmoon") and true
  1751.   end
  1752.   local expires
  1753.   local questDB
  1754.   if isWeekly then
  1755.     expires = addon:GetNextWeeklyResetTime()
  1756.     questDB = (isAccount and db.QuestDB.AccountWeekly) or db.QuestDB.Weekly
  1757.   elseif isMonthly then
  1758.     expires = addon:GetNextDarkmoonResetTime()
  1759.     questDB = db.QuestDB.Darkmoon
  1760.   elseif isDaily then
  1761.     questDB = (isAccount and db.QuestDB.AccountDaily) or db.QuestDB.Daily
  1762.   end
  1763.   debug("Quest Complete: "..(link or title).." "..id.." : "..title.." "..
  1764.     (isAccount and "(Account) " or "")..
  1765.     (isMonthly and "(Monthly)" or isWeekly and "(Weekly)" or isDaily and "(Daily)" or "(Regular)").."  "..
  1766.     (expires and date("%c",expires) or ""))
  1767.   if not isMonthly and not isWeekly and not isDaily then return end
  1768.   local mapid = addon:GetCurrentMapAreaID()
  1769.   questDB[id] = mapid
  1770.   local qinfo =  { ["Title"] = title, ["Link"] = link,
  1771.     ["isDaily"] = isDaily,
  1772.     ["Expires"] = expires,
  1773.     ["Zone"] = GetMapNameByID(mapid) }
  1774.   local scope = t
  1775.   if isAccount then
  1776.     scope = db
  1777.     if t.Quests then t.Quests[id] = nil end -- make sure we promote account quests
  1778.   end
  1779.   scope.Quests = scope.Quests or {}
  1780.   scope.Quests[id] = qinfo
  1781.   local dc, wc = addon:QuestCount(thisToon)
  1782.   local adc, awc = addon:QuestCount(nil)
  1783.   debug("DailyCount: "..dc.."  WeeklyCount: "..wc.." AccountDailyCount: "..adc.."  AccountWeeklyCount: "..awc)
  1784. end
  1785. hooksecurefunc("GetQuestReward", SI_GetQuestReward)
  1786.  
  1787. local function coloredText(fontstring)
  1788.   if not fontstring then return nil end
  1789.   local text = fontstring:GetText()
  1790.   if not text then return nil end
  1791.   local textR, textG, textB, textAlpha = fontstring:GetTextColor()
  1792.   return string.format("|c%02x%02x%02x%02x"..text.."|r",
  1793.     textAlpha*255, textR*255, textG*255, textB*255)
  1794. end
  1795.  
  1796. local function openIndicator(...)
  1797.   indicatortip = QTip:Acquire("SavedInstancesIndicatorTooltip", ...)
  1798.   indicatortip:Clear()
  1799.   indicatortip:SetHeaderFont(core:HeaderFont())
  1800.   indicatortip:SetScale(vars.db.Tooltip.Scale)
  1801. end
  1802.  
  1803. local function finishIndicator(parent)
  1804.   parent = parent or tooltip
  1805.   indicatortip:SetAutoHideDelay(0.1, parent)
  1806.   indicatortip.OnRelease = function() indicatortip = nil end -- extra-safety: update our variable on auto-release
  1807.   indicatortip:SmartAnchorTo(parent)
  1808.   indicatortip:SetFrameLevel(150) -- ensure visibility when forced to overlap main tooltip
  1809.   addon:SkinFrame(indicatortip,"SavedInstancesIndicatorTooltip")
  1810.   indicatortip:Show()
  1811. end
  1812.  
  1813. local function ShowToonTooltip(cell, arg, ...)
  1814.   local toon = arg
  1815.   if not toon then return end
  1816.   local t = vars.db.Toons[toon]
  1817.   if not t then return end
  1818.   openIndicator(2, "LEFT","RIGHT")
  1819.   local ftex = ""
  1820.   if t.Faction == "Alliance" then
  1821.     ftex = "\124TInterface\\TargetingFrame\\UI-PVP-Alliance:0:0:0:0:100:100:0:50:0:55\124t "
  1822.   elseif t.Faction == "Horde" then
  1823.     ftex = "\124TInterface\\TargetingFrame\\UI-PVP-Horde:0:0:0:0:100:100:10:70:0:55\124t"
  1824.   end
  1825.   indicatortip:SetCell(indicatortip:AddHeader(),1,ftex..ClassColorise(t.Class, toon))
  1826.   indicatortip:SetCell(1,2,ClassColorise(t.Class, LEVEL.." "..t.Level.." "..(t.LClass or "")))
  1827.   indicatortip:AddLine(STAT_AVERAGE_ITEM_LEVEL,("%d "):format(t.IL or 0)..STAT_AVERAGE_ITEM_LEVEL_EQUIPPED:format(t.ILe or 0))
  1828.   if t.RBGrating and t.RBGrating > 0 then
  1829.     indicatortip:AddLine(BATTLEGROUND_RATING, t.RBGrating)
  1830.   end
  1831.   if t.Money then
  1832.     indicatortip:AddLine(MONEY,addon:formatNumber(t.Money,true))
  1833.   end
  1834.   if t.Zone then
  1835.     indicatortip:AddLine(ZONE,t.Zone)
  1836.   end
  1837.   --[[
  1838.   if t.Race then
  1839.     indicatortip:AddLine(RACE,t.Race)
  1840.   end
  1841.   ]]
  1842.   if t.LastSeen then
  1843.     local when = date("%c",t.LastSeen)
  1844.     indicatortip:AddLine(L["Last updated"],when)
  1845.   end
  1846.   if vars.db.Tooltip.TrackPlayed and t.PlayedTotal and t.PlayedLevel and ChatFrame_TimeBreakDown then
  1847.     --indicatortip:AddLine((TIME_PLAYED_TOTAL):format((TIME_DAYHOURMINUTESECOND):format(ChatFrame_TimeBreakDown(t.PlayedTotal))))
  1848.     --indicatortip:AddLine((TIME_PLAYED_LEVEL):format((TIME_DAYHOURMINUTESECOND):format(ChatFrame_TimeBreakDown(t.PlayedLevel))))
  1849.     indicatortip:AddLine((TIME_PLAYED_TOTAL):format(""),SecondsToTime(t.PlayedTotal))
  1850.     indicatortip:AddLine((TIME_PLAYED_LEVEL):format(""),SecondsToTime(t.PlayedLevel))
  1851.   end
  1852.   finishIndicator()
  1853. end
  1854.  
  1855. local function ShowQuestTooltip(cell, arg, ...)
  1856.   local toon,cnt,isDaily = unpack(arg)
  1857.   local qstr = cnt.." "..(isDaily and L["Daily Quests"] or L["Weekly Quests"])
  1858.   local t = db
  1859.   local scopestr = L["Account"]
  1860.   local reset
  1861.   if toon then
  1862.     t = vars.db.Toons[toon]
  1863.     if not t then return end
  1864.     scopestr = ClassColorise(t.Class, toon)
  1865.     reset = (isDaily and t.DailyResetTime) or (not isDaily and t.WeeklyResetTime)
  1866.   end
  1867.   openIndicator(2, "LEFT","RIGHT")
  1868.   indicatortip:AddHeader(scopestr, qstr)
  1869.   if not reset then
  1870.     reset = (isDaily and addon:GetNextDailyResetTime()) or (not isDaily and addon:GetNextWeeklyResetTime())
  1871.   end
  1872.   if reset then
  1873.     indicatortip:AddLine(YELLOWFONT .. L["Time Left"] .. ":" .. FONTEND,
  1874.       SecondsToTime(reset - time()))
  1875.   end
  1876.   local ql = {}
  1877.   for id,qi in pairs(t.Quests) do
  1878.     if (not isDaily) == (not qi.isDaily) then
  1879.       table.insert(ql,(qi.Zone or "").." # "..id)
  1880.     end
  1881.   end
  1882.   table.sort(ql)
  1883.   for _,e in ipairs(ql) do
  1884.     local id = tonumber(e:match("# (%d+)"))
  1885.     local qi = id and t.Quests[id]
  1886.     local line = indicatortip:AddLine()
  1887.     local link = qi.Link
  1888.     if not link then -- sometimes missing the actual link due to races, fake it for display to prevent confusion
  1889.       if qi.Title:find("("..LOOT..")") then
  1890.         link = qi.Title
  1891.     else
  1892.       link = "\124cffffff00["..(qi.Title or "???").."]\124r"
  1893.     end
  1894.     end
  1895.     indicatortip:SetCell(line,1,(qi.Zone or ""),"LEFT")
  1896.     indicatortip:SetCell(line,2,link,"RIGHT")
  1897.   end
  1898.   finishIndicator()
  1899. end
  1900.  
  1901. local function skillsort(s1, s2)
  1902.   if s1.Expires ~= s2.Expires then
  1903.     return (s1.Expires or 0) < (s2.Expires or 0)
  1904.   else
  1905.     return (s1.Title or "") < (s2.Title or "")
  1906.   end
  1907. end
  1908.  
  1909. local function ShowSkillTooltip(cell, arg, ...)
  1910.   local toon, cnt = unpack(arg)
  1911.   local cstr = cnt.." "..L["Trade Skill Cooldowns"]
  1912.   local t = vars.db.Toons[toon]
  1913.   if not t then return end
  1914.   openIndicator(3, "LEFT","RIGHT","RIGHT")
  1915.   local tname = ClassColorise(t.Class, toon)
  1916.   indicatortip:AddHeader()
  1917.   indicatortip:SetCell(1,1,tname,"LEFT")
  1918.   indicatortip:SetCell(1,2,cstr,"RIGHT",2)
  1919.  
  1920.   local tmp = {}
  1921.   for _,sinfo in pairs(t.Skills) do
  1922.     table.insert(tmp,sinfo)
  1923.   end
  1924.   table.sort(tmp, skillsort)
  1925.  
  1926.   for _,sinfo in ipairs(tmp) do
  1927.     local line = indicatortip:AddLine()
  1928.     local title = sinfo.Link or sinfo.Title or "???"
  1929.     local tstr = SecondsToTime((sinfo.Expires or 0) - time())
  1930.     indicatortip:SetCell(line,1,title,"LEFT",2)
  1931.     indicatortip:SetCell(line,3,tstr,"RIGHT")
  1932.   end
  1933.   finishIndicator()
  1934. end
  1935.  
  1936. function addon:plantName(spellid)
  1937.   local name = GetSpellInfo(spellid)
  1938.   if not name then return "unknown" end
  1939.   name = name:gsub(L["Plant"],"")
  1940.   name = name:gsub(L["Throw"],"")
  1941.   name = name:gsub(L["Seeds"],"")
  1942.   name = name:gsub(L["Seed"],"")
  1943.   name = strtrim(name)
  1944.   return name
  1945. end
  1946.  
  1947. local function ShowFarmTooltip(cell, arg, ...)
  1948.   local toon = arg
  1949.   local t = vars.db.Toons[toon]
  1950.   if not t then return end
  1951.   openIndicator(2, "LEFT","RIGHT")
  1952.   local tname = ClassColorise(t.Class, toon)
  1953.   indicatortip:AddHeader()
  1954.   indicatortip:SetCell(1,1,tname,"LEFT")
  1955.   indicatortip:SetCell(1,2,L["Farm Crops"],"RIGHT")
  1956.  
  1957.   local exp = t.FarmExpires
  1958.   if exp and exp > time() then
  1959.     indicatortip:AddLine(YELLOWFONT .. L["Time Left"] .. ":" .. FONTEND, SecondsToTime(exp - time()))
  1960.   end
  1961.   indicatortip:AddLine(YELLOWFONT .. L["Crops harvested today"] .. ":" .. FONTEND,(t.FarmHarvested or 0))
  1962.   indicatortip:AddLine(YELLOWFONT .. L["Crops planted today"] .. ":" .. FONTEND,  (t.FarmPlanted or 0))
  1963.   local crops
  1964.   if t.FarmCropPlanted and next(t.FarmCropPlanted) then
  1965.     crops = t.FarmCropPlanted
  1966.     indicatortip:AddLine(YELLOWFONT .. L["Crops growing"] .. ":" .. FONTEND)
  1967.   elseif t.FarmCropReady and next(t.FarmCropReady) then
  1968.     crops = t.FarmCropReady
  1969.     indicatortip:AddLine(YELLOWFONT .. L["Crops ready"] .. ":" .. FONTEND)
  1970.   end
  1971.   if crops then
  1972.     for spellid,cnt in pairs(crops) do
  1973.       local line = indicatortip:AddLine()
  1974.       indicatortip:SetCell(line,1, addon:plantName(spellid),"LEFT")
  1975.       indicatortip:SetCell(line,2,"x"..cnt,"RIGHT")
  1976.     end
  1977.   end
  1978.   finishIndicator()
  1979. end
  1980.  
  1981. local function ShowBonusTooltip(cell, arg, ...)
  1982.   local toon = arg
  1983.   local parent
  1984.   if type(toon) == "table" then
  1985.     toon, parent = unpack(toon)
  1986.   end
  1987.   local t = vars.db.Toons[toon]
  1988.   if not t or not t.BonusRoll then return end
  1989.   openIndicator(4, "LEFT","LEFT","LEFT","LEFT")
  1990.   local tname = ClassColorise(t.Class, toon)
  1991.   indicatortip:AddHeader()
  1992.   indicatortip:SetCell(1,1,tname,"LEFT",2)
  1993.   indicatortip:SetCell(1,3,L["Recent Bonus Rolls"],"RIGHT",2)
  1994.  
  1995.   local line = indicatortip:AddLine()
  1996.   for i,roll in ipairs(t.BonusRoll) do
  1997.     if i > 10 then break end
  1998.     local line = indicatortip:AddLine()
  1999.     local icon = roll.currencyID and select(3,GetCurrencyInfo(roll.currencyID))
  2000.     if icon then
  2001.       indicatortip:SetCell(line,1, " \124T"..icon..":0\124t ")
  2002.     end
  2003.     if roll.name then
  2004.       indicatortip:SetCell(line,2,roll.name)
  2005.     end
  2006.     if roll.item then
  2007.       indicatortip:SetCell(line,3,roll.item)
  2008.     elseif roll.money then
  2009.       indicatortip:SetCell(line,3,GetMoneyString(roll.money))
  2010.     end
  2011.     if roll.time then
  2012.       indicatortip:SetCell(line,4,date("%b %d %H:%M",roll.time))
  2013.     end
  2014.   end
  2015.   finishIndicator(parent)
  2016. end
  2017.  
  2018. local function ShowAccountSummary(cell, arg, ...)
  2019.   openIndicator(2, "LEFT","RIGHT")
  2020.   indicatortip:SetCell(indicatortip:AddHeader(),1,GOLDFONT..L["Account Summary"]..FONTEND,"LEFT",2)
  2021.  
  2022.   local tmoney = 0
  2023.   local ttime = 0
  2024.   local ttoons = 0
  2025.   local tmaxtoons = 0
  2026.   local r = {}
  2027.   for toon, t in pairs(vars.db.Toons) do -- deliberately include ALL toons
  2028.     local realm = toon:match(" %- (.+)$")
  2029.     local money = t.Money or 0
  2030.     tmoney = tmoney + money
  2031.     local ri = r[realm] or { ["realm"] = realm, ["money"] = 0, ["cnt"] = 0 }
  2032.     ri.money = ri.money + money
  2033.     ri.cnt = ri.cnt + 1
  2034.     r[realm] = ri
  2035.     ttime = ttime + (t.PlayedTotal or 0)
  2036.     ttoons = ttoons + 1
  2037.     if t.Level == maxlvl then
  2038.       tmaxtoons = tmaxtoons + 1
  2039.     end
  2040.   end
  2041.   indicatortip:AddLine(L["Characters"], ttoons)
  2042.   indicatortip:AddLine(string.format(L["Level %d Characters"],maxlvl), tmaxtoons)
  2043.   if vars.db.Tooltip.TrackPlayed then
  2044.     indicatortip:AddLine((TIME_PLAYED_TOTAL):format(""),SecondsToTime(ttime))
  2045.   end
  2046.   indicatortip:AddLine(TOTAL.." "..MONEY,addon:formatNumber(tmoney,true))
  2047.   local rmoney = {}
  2048.   for _,ri in pairs(r) do table.insert(rmoney,ri) end
  2049.   table.sort(rmoney,function(a,b) return a.money > b.money end)
  2050.   for _,ri in ipairs(rmoney) do
  2051.     if ri.money > 10000*10000 and ri.cnt > 1 then -- show multi-toon servers with over 10k wealth
  2052.       indicatortip:AddLine(ri.realm.." "..MONEY,addon:formatNumber(ri.money,true))
  2053.     end
  2054.   end
  2055.  
  2056.   -- history information
  2057.   indicatortip:AddLine("")
  2058.   addon:HistoryUpdate()
  2059.   local tmp = {}
  2060.   local cnt = 0
  2061.   for _,ii in pairs(db.History) do
  2062.     table.insert(tmp,ii)
  2063.   end
  2064.   local cnt = #tmp
  2065.   table.sort(tmp, function(i1,i2) return i1.last < i2.last end)
  2066.   indicatortip:SetHeaderFont(tooltip:GetHeaderFont())
  2067.   indicatortip:SetCell(indicatortip:AddHeader(),1,GOLDFONT..cnt.." "..L["Recent Instances"]..": "..FONTEND,"LEFT",2)
  2068.   for _,ii in ipairs(tmp) do
  2069.     local tstr = REDFONT..SecondsToTime(ii.last+addon.histReapTime - time(),false,false,1)..FONTEND
  2070.     indicatortip:AddLine(tstr, ii.desc)
  2071.   end
  2072.   indicatortip:AddLine("")
  2073.   indicatortip:SetCell(indicatortip:AddLine(),1,
  2074.     string.format(L["These are the instances that count towards the %i instances per hour account limit, and the time until they expire."],
  2075.       addon.histLimit),"LEFT",2,nil,nil,nil,250)
  2076.   finishIndicator()
  2077. end
  2078.  
  2079. local function ShowWorldBossTooltip(cell, arg, ...)
  2080.   local worldbosses = arg[1]
  2081.   local toon = arg[2]
  2082.   local saved = arg[3]
  2083.   if not worldbosses or not toon then return end
  2084.   openIndicator(2, "LEFT","RIGHT")
  2085.   local line = indicatortip:AddHeader()
  2086.   local toonstr = (db.Tooltip.ShowServer and toon) or strsplit(' ', toon)
  2087.   local t = vars.db.Toons[toon]
  2088.   local reset = t.WeeklyResetTime or addon:GetNextWeeklyResetTime()
  2089.   indicatortip:SetCell(line, 1, ClassColorise(vars.db.Toons[toon].Class, toonstr), indicatortip:GetHeaderFont(), "LEFT")
  2090.   indicatortip:SetCell(line, 2, GOLDFONT .. L["World Bosses"] .. FONTEND, indicatortip:GetHeaderFont(), "RIGHT")
  2091.   indicatortip:AddLine(YELLOWFONT .. L["Time Left"] .. ":" .. FONTEND, SecondsToTime(reset - time()))
  2092.   for _, instance in ipairs(worldbosses) do
  2093.     local thisinstance = vars.db.Instances[instance]
  2094.     if thisinstance then
  2095.       local info = thisinstance[toon] and thisinstance[toon][2]
  2096.       local n = indicatortip:AddLine()
  2097.       indicatortip:SetCell(n, 1, instance, "LEFT")
  2098.       if info and info[1] then
  2099.         indicatortip:SetCell(n, 2, REDFONT..ERR_LOOT_GONE..FONTEND, "RIGHT")
  2100.       else
  2101.         indicatortip:SetCell(n, 2, GREENFONT..AVAILABLE..FONTEND, "RIGHT")
  2102.       end
  2103.     end
  2104.   end
  2105.   finishIndicator()
  2106. end
  2107.  
  2108. local function ShowLFRTooltip(cell, arg, ...)
  2109.   local boxname = arg[1]
  2110.   local toon = arg[2]
  2111.   local lfrmap = arg[3]
  2112.   local t = vars.db.Toons[toon]
  2113.   if not boxname or not t or not lfrmap then return end
  2114.   openIndicator(3, "LEFT", "LEFT","RIGHT")
  2115.   local line = indicatortip:AddHeader()
  2116.   local toonstr = (db.Tooltip.ShowServer and toon) or strsplit(' ', toon)
  2117.   local reset = t.WeeklyResetTime or addon:GetNextWeeklyResetTime()
  2118.   indicatortip:SetCell(line, 1, ClassColorise(vars.db.Toons[toon].Class, toonstr), indicatortip:GetHeaderFont(), "LEFT", 1)
  2119.   indicatortip:SetCell(line, 2, GOLDFONT .. boxname .. FONTEND, indicatortip:GetHeaderFont(), "RIGHT", 2)
  2120.   indicatortip:AddLine(YELLOWFONT .. L["Time Left"] .. ":" .. FONTEND, nil, SecondsToTime(reset - time()))
  2121.   for i=1,20 do
  2122.     local instance = lfrmap[boxname..":"..i]
  2123.     local diff = 2
  2124.     if instance then
  2125.       indicatortip:SetCell(indicatortip:AddLine(), 1, YELLOWFONT .. instance .. FONTEND, "CENTER",3)
  2126.       local thisinstance = vars.db.Instances[instance]
  2127.       local info = thisinstance[toon] and thisinstance[toon][diff]
  2128.       local killed, total, base, remap = addon:instanceBosses(instance,toon,diff)
  2129.       for i=base,base+total-1 do
  2130.         local bossid = i
  2131.         if remap then
  2132.           bossid = remap[i-base+1]
  2133.         end
  2134.         local bossname = GetLFGDungeonEncounterInfo(thisinstance.LFDID, bossid);
  2135.         local n = indicatortip:AddLine()
  2136.         indicatortip:SetCell(n, 1, bossname, "LEFT", 2)
  2137.         if info and info[bossid] then
  2138.           indicatortip:SetCell(n, 3, REDFONT..ERR_LOOT_GONE..FONTEND, "RIGHT", 1)
  2139.         else
  2140.           indicatortip:SetCell(n, 3, GREENFONT..AVAILABLE..FONTEND, "RIGHT", 1)
  2141.         end
  2142.       end
  2143.     end
  2144.   end
  2145.   finishIndicator()
  2146. end
  2147.  
  2148. local function ShowIndicatorTooltip(cell, arg, ...)
  2149.   local instance = arg[1]
  2150.   local toon = arg[2]
  2151.   local diff = arg[3]
  2152.   if not instance or not toon or not diff then return end
  2153.   openIndicator(3, "LEFT", "LEFT","RIGHT")
  2154.   local thisinstance = vars.db.Instances[instance]
  2155.   local worldboss = thisinstance and thisinstance.WorldBoss
  2156.   local info = thisinstance[toon][diff]
  2157.   local id = info.ID
  2158.   local nameline = indicatortip:AddHeader()
  2159.   indicatortip:SetCell(nameline, 1, DifficultyString(instance, diff, toon), indicatortip:GetHeaderFont(), "LEFT", 1)
  2160.   indicatortip:SetCell(nameline, 2, GOLDFONT .. instance .. FONTEND, indicatortip:GetHeaderFont(), "RIGHT", 2)
  2161.   local toonline = indicatortip:AddHeader()
  2162.   local toonstr = (db.Tooltip.ShowServer and toon) or strsplit(' ', toon)
  2163.   indicatortip:SetCell(toonline, 1, ClassColorise(vars.db.Toons[toon].Class, toonstr), indicatortip:GetHeaderFont(), "LEFT", 1)
  2164.   indicatortip:SetCell(toonline, 2, addon:idtext(thisinstance,diff,info), "RIGHT", 2)
  2165.   local EMPH = " !!! "
  2166.   if info.Extended then
  2167.     indicatortip:SetCell(indicatortip:AddLine(),1,WHITEFONT .. EMPH .. L["Extended Lockout - Not yet saved"] .. EMPH .. FONTEND,"CENTER",3)
  2168.   elseif info.Locked == false and info.ID > 0 then
  2169.     indicatortip:SetCell(indicatortip:AddLine(),1,WHITEFONT .. EMPH .. L["Expired Lockout - Can be extended"] .. EMPH .. FONTEND,"CENTER",3)
  2170.   end
  2171.   if info.Expires > 0 then
  2172.     indicatortip:AddLine(YELLOWFONT .. L["Time Left"] .. ":" .. FONTEND, nil, SecondsToTime(thisinstance[toon][diff].Expires - time()))
  2173.   end
  2174.   if (info.ID or 0) > 0 and (
  2175.     (thisinstance.Raid and (diff == 5 or diff == 6 or diff == 16)) -- raid: 10 heroic, 25 heroic or mythic
  2176.     or
  2177.     (diff == 23) -- mythic 5-man
  2178.     ) then
  2179.     local n = indicatortip:AddLine()
  2180.     indicatortip:SetCell(n, 1, YELLOWFONT .. ID .. ":" .. FONTEND, "LEFT", 1)
  2181.     indicatortip:SetCell(n, 2, info.ID, "RIGHT", 2)
  2182.   end
  2183.   if info.Link then
  2184.     scantt:SetOwner(UIParent,"ANCHOR_NONE")
  2185.     scantt:SetHyperlink(info.Link)
  2186.     local name = scantt:GetName()
  2187.     local gotbossinfo
  2188.     for i=2,scantt:NumLines() do
  2189.       local left,right = _G[name.."TextLeft"..i], _G[name.."TextRight"..i]
  2190.       if right and right:GetText() then
  2191.         local n = indicatortip:AddLine()
  2192.         indicatortip:SetCell(n, 1, coloredText(left), "LEFT", 2)
  2193.         indicatortip:SetCell(n, 3, coloredText(right), "RIGHT", 1)
  2194.         gotbossinfo = true
  2195.       else
  2196.         indicatortip:SetCell(indicatortip:AddLine(),1,coloredText(left),"CENTER",3)
  2197.       end
  2198.     end
  2199.     if not gotbossinfo then
  2200.       local exc = addon:instanceException(thisinstance.LFDID)
  2201.       local bits = tonumber(info.Link:match(":(%d+)\124h"))
  2202.       if exc and bits then
  2203.         for i=1,exc.total do
  2204.           local n = indicatortip:AddLine()
  2205.           indicatortip:SetCell(n, 1, exc[i], "LEFT", 2)
  2206.           local text = "\124cff00ff00"..BOSS_ALIVE.."\124r"
  2207.           if bit.band(bits,1) > 0 then
  2208.             text = "\124cffff1f1f"..BOSS_DEAD.."\124r"
  2209.           end
  2210.           indicatortip:SetCell(n, 3, text, "RIGHT", 1)
  2211.           bits = bit.rshift(bits,1)
  2212.         end
  2213.       else
  2214.         indicatortip:SetCell(indicatortip:AddLine(),1,WHITEFONT ..
  2215.           L["Boss kill information is missing for this lockout.\nThis is a Blizzard bug affecting certain old raids."] ..
  2216.           FONTEND,"CENTER",3)
  2217.       end
  2218.     end
  2219.   end
  2220.   if info.ID < 0 then
  2221.     local killed, total, base, remap = addon:instanceBosses(instance,toon,diff)
  2222.     for i=base,base+total-1 do
  2223.       local bossid = i
  2224.       if remap then
  2225.         bossid = remap[i-base+1]
  2226.       end
  2227.       local bossname
  2228.       if worldboss then
  2229.         bossname = addon.WorldBosses[worldboss].name or "UNKNOWN"
  2230.       else
  2231.         bossname = GetLFGDungeonEncounterInfo(thisinstance.LFDID, bossid);
  2232.       end
  2233.       local n = indicatortip:AddLine()
  2234.       indicatortip:SetCell(n, 1, bossname, "LEFT", 2)
  2235.       if info[bossid] then
  2236.         indicatortip:SetCell(n, 3, REDFONT..ERR_LOOT_GONE..FONTEND, "RIGHT", 1)
  2237.       else
  2238.         indicatortip:SetCell(n, 3, GREENFONT..AVAILABLE..FONTEND, "RIGHT", 1)
  2239.       end
  2240.     end
  2241.   end
  2242.   finishIndicator()
  2243. end
  2244.  
  2245. local colorpat = "\124c%c%c%c%c%c%c%c%c"
  2246. local weeklycap = CURRENCY_WEEKLY_CAP:gsub("%%%d*\$?([ds])","%%%1")
  2247. local weeklycap_scan = weeklycap:gsub("%%d","(%%d+)"):gsub("%%s","(\124c%%x%%x%%x%%x%%x%%x%%x%%x)")
  2248. weeklycap = weeklycap:gsub("%%d","%%s")
  2249. local totalcap = CURRENCY_TOTAL_CAP:gsub("%%%d*\$?([ds])","%%%1")
  2250. local totalcap_scan = totalcap:gsub("%%d","(%%d+)"):gsub("%%s","(\124c%%x%%x%%x%%x%%x%%x%%x%%x)")
  2251. totalcap = totalcap:gsub("%%d","%%s")
  2252. local season_scan = CURRENCY_SEASON_TOTAL:gsub("%%%d*\$?([ds])","(%%%1*)")
  2253.  
  2254. function addon:GetSeasonCurrency(idx)
  2255.   scantt:SetOwner(UIParent,"ANCHOR_NONE")
  2256.   scantt:SetCurrencyByID(idx)
  2257.   local name = scantt:GetName()
  2258.   for i=1,scantt:NumLines() do
  2259.     local left = _G[name.."TextLeft"..i]
  2260.     if left:GetText():find(season_scan) then
  2261.       return left:GetText()
  2262.     end
  2263.   end
  2264.   return nil
  2265. end
  2266.  
  2267. local function ShowSpellIDTooltip(cell, arg, ...)
  2268.   local toon, spellid, timestr = unpack(arg)
  2269.   if not toon or not spellid or not timestr then return end
  2270.   openIndicator(2, "LEFT","RIGHT")
  2271.   indicatortip:AddHeader(ClassColorise(vars.db.Toons[toon].Class, strsplit(' ', toon)), timestr)
  2272.   if spellid > 0 then
  2273.     local tip = vars.db.spelltip and vars.db.spelltip[spellid]
  2274.     for i=1,#tip do
  2275.       indicatortip:AddLine("")
  2276.       indicatortip:SetCell(indicatortip:GetLineCount(),1,tip[i], nil, "LEFT",2, nil, nil, nil, 250)
  2277.     end
  2278.   else
  2279.     local queuestr = LFG_RANDOM_COOLDOWN_YOU:match("^(.+)\n")
  2280.     indicatortip:AddLine(LFG_TYPE_RANDOM_DUNGEON)
  2281.     indicatortip:AddLine("")
  2282.     indicatortip:SetCell(indicatortip:GetLineCount(),1,queuestr, nil, "LEFT",2, nil, nil, nil, 250)
  2283.   end
  2284.   finishIndicator()
  2285. end
  2286.  
  2287. local function ShowCurrencyTooltip(cell, arg, ...)
  2288.   local toon, idx, ci = unpack(arg)
  2289.   if not toon or not idx or not ci then return end
  2290.   local name,_,tex = GetCurrencyInfo(idx)
  2291.   tex = " \124T"..tex..":0\124t"
  2292.   openIndicator(2, "LEFT","RIGHT")
  2293.   indicatortip:AddHeader(ClassColorise(vars.db.Toons[toon].Class, strsplit(' ', toon)), CurrencyColor(ci.amount or 0,ci.totalMax)..tex)
  2294.  
  2295.   scantt:SetOwner(UIParent,"ANCHOR_NONE")
  2296.   scantt:SetCurrencyByID(idx)
  2297.   local name = scantt:GetName()
  2298.   local spacer
  2299.   for i=1,scantt:NumLines() do
  2300.     local left = _G[name.."TextLeft"..i]
  2301.     local text = left:GetText()
  2302.     if text:find(weeklycap_scan) or
  2303.       text:find(totalcap_scan) or
  2304.       text:find(season_scan) then
  2305.     -- omit player's values
  2306.     else
  2307.       indicatortip:AddLine("")
  2308.       indicatortip:SetCell(indicatortip:GetLineCount(),1,coloredText(left), nil, "LEFT",2, nil, nil, nil, 250)
  2309.       spacer = #strtrim(text) == 0
  2310.     end
  2311.   end
  2312.   if ci.weeklyMax and ci.weeklyMax > 0 then
  2313.     if not spacer then indicatortip:AddLine(" "); spacer = true end
  2314.     indicatortip:AddLine(weeklycap:format("", CurrencyColor(ci.earnedThisWeek or 0,ci.weeklyMax), addon:formatNumber(ci.weeklyMax)))
  2315.   end
  2316.   if ci.totalMax and ci.totalMax > 0 then
  2317.     if not spacer then indicatortip:AddLine(" "); spacer = true end
  2318.     indicatortip:AddLine(totalcap:format("", CurrencyColor(ci.amount or 0,ci.totalMax), addon:formatNumber(ci.totalMax)))
  2319.   end
  2320.   if ci.season and #ci.season > 0 then
  2321.     if not spacer then indicatortip:AddLine(" "); spacer = true end
  2322.     local str = ci.season
  2323.     local num = str:match("(%d+)")
  2324.     if num then
  2325.       str = str:gsub(num,addon:formatNumber(num))
  2326.     end
  2327.     indicatortip:AddLine(str)
  2328.   end
  2329.   finishIndicator()
  2330. end
  2331.  
  2332. local function ShowCurrencySummary(cell, arg, ...)
  2333.   local idx = arg
  2334.   if not idx then return end
  2335.   local name,_,tex = GetCurrencyInfo(idx)
  2336.   tex = " \124T"..tex..":0\124t"
  2337.   openIndicator(2, "LEFT","RIGHT")
  2338.   indicatortip:AddHeader(name, "")
  2339.   local total = 0
  2340.   local tmax
  2341.   local temp = {}
  2342.   for toon, t in pairs(vars.db.Toons) do -- deliberately include ALL toons
  2343.     local ci = t.currency and t.currency[idx]
  2344.     if ci and ci.amount then
  2345.       tmax = tmax or ci.totalMax
  2346.       table.insert(temp, { ["toon"] = toon, ["amount"] = ci.amount,
  2347.         ["str1"] = ClassColorise(t.Class, toon),
  2348.         ["str2"] = CurrencyColor(ci.amount or 0,tmax)..tex,
  2349.       })
  2350.       total = total + ci.amount
  2351.     end
  2352.   end
  2353.   indicatortip:SetCell(1,2,CurrencyColor(total,0)..tex)
  2354.   --indicatortip:AddLine(TOTAL, CurrencyColor(total,tmax)..tex)
  2355.   --indicatortip:AddLine(" ")
  2356.   addon.currency_sort = addon.currency_sort or function(a,b)
  2357.     if a.amount > b.amount then
  2358.       return true
  2359.     elseif a.amount < b.amount then
  2360.       return false
  2361.     end
  2362.     local an, as = a.toon:match('^(.*) [-] (.*)$')
  2363.     local bn, bs = b.toon:match('^(.*) [-] (.*)$')
  2364.     if db.Tooltip.ServerSort and as ~= bs then
  2365.       return as < bs
  2366.     else
  2367.       return a.toon < b.toon
  2368.     end
  2369.   end
  2370.   table.sort(temp, addon.currency_sort)
  2371.   for _,t in ipairs(temp) do
  2372.     indicatortip:AddLine(t.str1, t.str2)
  2373.   end
  2374.  
  2375.   finishIndicator()
  2376. end
  2377.  
  2378.  
  2379. -- global addon code below
  2380.  
  2381. function core:toonInit()
  2382.   local ti = db.Toons[thisToon] or { }
  2383.   db.Toons[thisToon] = ti
  2384.   ti.LClass, ti.Class = UnitClass("player")
  2385.   ti.Level = UnitLevel("player")
  2386.   ti.Show = ti.Show or "saved"
  2387.   ti.Quests = ti.Quests or {}
  2388.   ti.Skills = ti.Skills or {}
  2389.   -- try to get a reset time, but don't overwrite existing, which could break quest list
  2390.   -- real update comes later in UpdateToonData
  2391.   ti.DailyResetTime = ti.DailyResetTime or addon:GetNextDailyResetTime()
  2392.   ti.WeeklyResetTime = ti.WeeklyResetTime or addon:GetNextWeeklyResetTime()
  2393. end
  2394.  
  2395. function core:OnInitialize()
  2396.   SavedInstancesDB = SavedInstancesDB or vars.defaultDB
  2397.   -- begin backwards compatibility
  2398.   if not SavedInstancesDB.DBVersion or SavedInstancesDB.DBVersion < 10 then
  2399.     SavedInstancesDB = vars.defaultDB
  2400.   elseif SavedInstancesDB.DBVersion < 12 then
  2401.     SavedInstancesDB.Indicators = vars.defaultDB.Indicators
  2402.     SavedInstancesDB.DBVersion = 12
  2403.   end
  2404.   -- end backwards compatibilty
  2405.   db = db or SavedInstancesDB
  2406.   vars.db = db
  2407.   config = vars.config
  2408.   core:toonInit()
  2409.   db.Lockouts = nil -- deprecated
  2410.   db.History = db.History or {}
  2411.   db.Quests = db.Quests or vars.defaultDB.Quests
  2412.   db.QuestDB = db.QuestDB or vars.defaultDB.QuestDB
  2413.   for name,default in pairs(vars.defaultDB.Tooltip) do
  2414.     db.Tooltip[name] = (db.Tooltip[name]==nil and default) or db.Tooltip[name]
  2415.   end
  2416.   for _, id in ipairs(addon.currency) do
  2417.     local name = "Currency"..id
  2418.     db.Tooltip[name] = (db.Tooltip[name]==nil and  vars.defaultDB.Tooltip[name]) or db.Tooltip[name]
  2419.   end
  2420.   local currtmp = {}
  2421.   for _,idx in ipairs(currency) do currtmp[idx] = true end
  2422.   for toon, t in pairs(vars.db.Toons) do
  2423.     t.Order = t.Order or 50
  2424.     if t.currency then -- clean old undiscovered currency entries
  2425.       for idx, ci in pairs(t.currency) do
  2426.         -- detect outdated entries because new version doesn't explicitly store max zeros
  2427.         if (ci.amount == 0 and (ci.weeklyMax == 0 or ci.totalMax == 0))
  2428.           or ci.amount == nil -- another outdated entry type created by old weekly reset logic
  2429.           or not currtmp[idx] -- removed currency
  2430.         then
  2431.           t.currency[idx] = nil
  2432.         end
  2433.     end
  2434.     end
  2435.   end
  2436.   for qid, _ in pairs(db.QuestDB.Daily) do
  2437.     if db.QuestDB.AccountDaily[qid] then
  2438.       debug("Removing duplicate questDB entry: "..qid)
  2439.       db.QuestDB.Daily[qid] = nil
  2440.     end
  2441.   end
  2442.   for qid, escope in pairs(QuestExceptions) do -- upgrade QuestDB with new exceptions
  2443.     local val = -1 -- default to a blank zone
  2444.     for scope, qdb in pairs(db.QuestDB) do
  2445.       val = qdb[qid] or val
  2446.       qdb[qid] = nil
  2447.     end
  2448.     if db.QuestDB[escope] then
  2449.       db.QuestDB[escope][qid] = val
  2450.     end
  2451.   end
  2452.   addon:SetupVersion()
  2453.   RequestRaidInfo() -- get lockout data
  2454.   RequestLFDPlayerLockInfo()
  2455.   vars.dataobject = vars.LDB and vars.LDB:NewDataObject("SavedInstances", {
  2456.     text = addonAbbrev,
  2457.     type = "launcher",
  2458.     icon = "Interface\\Addons\\SavedInstances\\icon.tga",
  2459.     OnEnter = function(frame)
  2460.       if not addon:IsDetached() and not db.Tooltip.DisableMouseover then
  2461.         core:ShowTooltip(frame)
  2462.       end
  2463.     end,
  2464.     OnLeave = function(frame) end,
  2465.     OnClick = function(frame, button)
  2466.       if button == "MiddleButton" then
  2467.         if InCombatLockdown() then return end
  2468.         ToggleFriendsFrame(4) -- open Blizzard Raid window
  2469.         RaidInfoFrame:Show()
  2470.       elseif button == "LeftButton" then
  2471.         addon:ToggleDetached()
  2472.       else
  2473.         config:ShowConfig()
  2474.       end
  2475.     end
  2476.   })
  2477.   if vars.icon then
  2478.     vars.icon:Register(addonName, vars.dataobject, db.MinimapIcon)
  2479.     vars.icon:Refresh(addonName)
  2480.   end
  2481.   addon.BonusRollShow() -- catch roll-on-load
  2482. end
  2483.  
  2484. function addon:SetupVersion()
  2485.   if addon.version then return end
  2486.   local svnrev = 0
  2487.   local T_svnrev = addon.svnrev
  2488.   T_svnrev["X-Build"] = tonumber((GetAddOnMetadata(addonName, "X-Build") or ""):match("%d+"))
  2489.   T_svnrev["X-Revision"] = tonumber((GetAddOnMetadata(addonName, "X-Revision") or ""):match("%d+"))
  2490.   for _,v in pairs(T_svnrev) do -- determine highest file revision
  2491.     if v and v > svnrev then
  2492.       svnrev = v
  2493.   end
  2494.   end
  2495.   addon.revision = svnrev
  2496.  
  2497.   T_svnrev["X-Curse-Packaged-Version"] = GetAddOnMetadata(addonName, "X-Curse-Packaged-Version")
  2498.   T_svnrev["Version"] = GetAddOnMetadata(addonName, "Version")
  2499.   addon.version = T_svnrev["X-Curse-Packaged-Version"] or T_svnrev["Version"] or "@"
  2500.   if string.find(addon.version, "@") then -- dev copy uses "@.project-version.@"
  2501.     addon.version = "r"..svnrev
  2502.   end
  2503. end
  2504.  
  2505. function core:OnEnable()
  2506.   self:RegisterBucketEvent("UPDATE_INSTANCE_INFO", 2, function() core:Refresh(nil) end)
  2507.   self:RegisterBucketEvent("LOOT_CLOSED", 1, function() core:QuestRefresh(nil) end)
  2508.   self:RegisterBucketEvent("LFG_UPDATE_RANDOM_INFO", 1, function() addon:UpdateInstanceData(); addon:UpdateToonData() end)
  2509.   self:RegisterBucketEvent("RAID_INSTANCE_WELCOME", 1, RequestRaidInfo)
  2510.   self:RegisterEvent("CHAT_MSG_SYSTEM", "CheckSystemMessage")
  2511.   self:RegisterEvent("CHAT_MSG_CURRENCY", "CheckSystemMessage")
  2512.   self:RegisterEvent("CHAT_MSG_LOOT", "CheckSystemMessage")
  2513.   self:RegisterBucketEvent("CURRENCY_DISPLAY_UPDATE", 0.25, function() addon:UpdateCurrency() end)
  2514.   self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
  2515.   self:RegisterBucketEvent("TRADE_SKILL_LIST_UPDATE", 1)
  2516.   self:RegisterBucketEvent("PLAYER_ENTERING_WORLD", 1, RequestRaidInfo)
  2517.   self:RegisterBucketEvent("LFG_LOCK_INFO_RECEIVED", 1, RequestRaidInfo)
  2518.   self:RegisterEvent("BONUS_ROLL_RESULT", "BonusRollResult")
  2519.   self:RegisterEvent("PLAYER_LOGOUT", function() addon.logout = true ; addon:UpdateToonData() end) -- update currency spent
  2520.   self:RegisterEvent("LFG_COMPLETION_REWARD", "RefreshLockInfo") -- for random daily dungeon tracking
  2521.   self:RegisterEvent("BOSS_KILL")
  2522.   self:RegisterEvent("ENCOUNTER_END", "EncounterEnd")
  2523.   self:RegisterEvent("CHAT_MSG_MONSTER_YELL")
  2524.   self:RegisterEvent("TIME_PLAYED_MSG", function(_,total,level)
  2525.     local t = thisToon and vars and vars.db and vars.db.Toons[thisToon]
  2526.     if total > 0 and t then
  2527.       t.PlayedTotal = total
  2528.       t.PlayedLevel = level
  2529.     end
  2530.     addon.PlayedTime = time()
  2531.     if addon.playedpending then
  2532.       for c,_ in pairs(addon.playedreg) do
  2533.         c:RegisterEvent("TIME_PLAYED_MSG") -- Restore default
  2534.       end
  2535.       addon.playedpending = false
  2536.     end
  2537.   end)
  2538.   self:RegisterEvent("ADDON_LOADED")
  2539.   core:ADDON_LOADED()
  2540.   if not addon.resetDetect then
  2541.     addon.resetDetect = CreateFrame("Button", "SavedInstancesResetDetectHiddenFrame", UIParent)
  2542.     for _,e in pairs({
  2543.       "RAID_INSTANCE_WELCOME",
  2544.       "PLAYER_ENTERING_WORLD", "CHAT_MSG_SYSTEM", "CHAT_MSG_ADDON",
  2545.       "ZONE_CHANGED_NEW_AREA",
  2546.       "INSTANCE_BOOT_START", "INSTANCE_BOOT_STOP", "GROUP_ROSTER_UPDATE",
  2547.     }) do
  2548.       addon.resetDetect:RegisterEvent(e)
  2549.     end
  2550.   end
  2551.   addon.resetDetect:SetScript("OnEvent", addon.HistoryEvent)
  2552.   RegisterAddonMessagePrefix(addonName)
  2553.   addon:HistoryEvent("PLAYER_ENTERING_WORLD") -- update after initial load
  2554.   addon:specialQuests()
  2555.   core:updateRealmMap()
  2556. end
  2557.  
  2558. function core:ADDON_LOADED()
  2559.   if DBM and DBM.EndCombat and not addon.dbmhook then
  2560.     addon.dbmhook = true
  2561.     hooksecurefunc(DBM, "EndCombat", function(self, mod, wipe)
  2562.       core:BossModEncounterEnd("DBM:EndCombat", mod and mod.combatInfo and mod.combatInfo.name)
  2563.     end)
  2564.   end
  2565.   if BigWigsLoader and not addon.bigwigshook then
  2566.     addon.bigwigshook = true
  2567.     BigWigsLoader.RegisterMessage(self, "BigWigs_OnBossWin", function(self, event, mod)
  2568.       core:BossModEncounterEnd("BigWigs_OnBossWin", mod and mod.displayName)
  2569.     end)
  2570.   end
  2571. end
  2572.  
  2573. function core:OnDisable()
  2574.   self:UnregisterAllEvents()
  2575.   addon.resetDetect:SetScript("OnEvent", nil)
  2576. end
  2577.  
  2578. function core:RequestLockInfo() -- request lock info from the server immediately
  2579.   RequestRaidInfo()
  2580.   RequestLFDPlayerLockInfo()
  2581. end
  2582.  
  2583. function core:RefreshLockInfo() -- throttled lock update with retry
  2584.   local now = GetTime()
  2585.   if now > (core.lastrefreshlock or 0) + 1 then
  2586.     core.lastrefreshlock = now
  2587.     core:RequestLockInfo()
  2588.   end
  2589.   if now > (core.lastrefreshlocksched or 0) + 120 then
  2590.     -- make sure we update any lockout info (sometimes there's server-side delay)
  2591.     core.lastrefreshlockshed = now
  2592.     core:ScheduleTimer("RequestLockInfo",5)
  2593.     core:ScheduleTimer("RequestLockInfo",30)
  2594.     core:ScheduleTimer("RequestLockInfo",60)
  2595.     core:ScheduleTimer("RequestLockInfo",90)
  2596.     core:ScheduleTimer("RequestLockInfo",120)
  2597.   end
  2598. end
  2599.  
  2600. local currency_msg = CURRENCY_GAINED:gsub(":.*$","")
  2601. function core:CheckSystemMessage(event, msg)
  2602.   local inst, t = IsInInstance()
  2603.   -- note: currency is already updated in TooltipShow,
  2604.   -- here we just hook JP/VP currency messages to capture lockout changes
  2605.   if inst and (t == "party" or t == "raid") and -- dont update on bg honor
  2606.     (msg:find(INSTANCE_SAVED) or -- first boss kill
  2607.     msg:find(currency_msg)) -- subsequent boss kills (unless capped or over level)
  2608.   then
  2609.     core:RefreshLockInfo()
  2610.   end
  2611. end
  2612.  
  2613. function core:updateRealmMap()
  2614.   local realm = GetRealmName():gsub("%s+","")
  2615.   local lmap = GetAutoCompleteRealms()
  2616.   local rmap = vars.db.RealmMap or {}
  2617.   vars.db.RealmMap = rmap
  2618.   if lmap and next(lmap) then -- connected realms detected
  2619.     table.sort(lmap)
  2620.     local mapid = rmap[realm] -- find existing map
  2621.     if not mapid then
  2622.       for _,r in ipairs(lmap) do
  2623.         mapid = mapid or rmap[r]
  2624.       end
  2625.     end
  2626.     if mapid then -- check for possible expansion
  2627.       local oldmap = rmap[mapid]
  2628.       if oldmap and #lmap > #oldmap then
  2629.         rmap[mapid] = lmap
  2630.       end
  2631.     else -- new map
  2632.       mapid = #rmap + 1
  2633.       rmap[mapid] = lmap
  2634.     end
  2635.     for _,r in ipairs(rmap[mapid]) do -- maintain inverse mapping
  2636.       rmap[r] = mapid
  2637.     end
  2638.   end
  2639. end
  2640.  
  2641. function core:getRealmGroup(realm)
  2642.   -- returns realm-group-id, { realm1, realm2, ...} for connected realm, or nil,nil for unconnected
  2643.   realm = realm:gsub("%s+","")
  2644.   local rmap = vars.db.RealmMap
  2645.   local gid = rmap and rmap[realm]
  2646.   return gid, gid and rmap[gid]
  2647. end
  2648.  
  2649. function core:CHAT_MSG_MONSTER_YELL(event, msg, bossname)
  2650.   -- cheapest possible outdoor boss detection for players lacking a proper boss mod
  2651.   -- should work for sha and nalak, oon and gal report a related mob
  2652.   local t = vars.db.Toons[thisToon]
  2653.   local now = time()
  2654.   if bossname and t then
  2655.     bossname = tostring(bossname) -- for safety
  2656.     local diff = select(4,GetInstanceInfo())
  2657.     if diff and #diff > 0 then bossname = bossname .. ": ".. diff end
  2658.     t.lastbossyell = bossname
  2659.     t.lastbossyelltime = now
  2660.     --debug("CHAT_MSG_MONSTER_YELL: "..tostring(bossname));
  2661.   end
  2662. end
  2663.  
  2664. function core:BossModEncounterEnd(modname, bossname)
  2665.   local t = vars.db.Toons[thisToon]
  2666.   local now = time()
  2667.   if bossname and t and now > (t.lastbosstime or 0) + 2*60 then
  2668.     -- boss mods can often detect completion before ENCOUNTER_END
  2669.     -- also some world bosses never send ENCOUNTER_END
  2670.     -- enough timeout to prevent overwriting, but short enough to prevent cross-boss contamination
  2671.     bossname = tostring(bossname) -- for safety
  2672.     local diff = select(4,GetInstanceInfo())
  2673.     if diff and #diff > 0 then bossname = bossname .. ": ".. diff end
  2674.     t.lastboss = bossname
  2675.     t.lastbosstime = now
  2676.   end
  2677.   debug("%s refresh: %s",(modname or "BossMod"),tostring(bossname));
  2678.   core:RefreshLockInfo()
  2679. end
  2680.  
  2681. function core:EncounterEnd(event, encounterID, encounterName, difficultyID, raidSize, endStatus)
  2682.   debug("EncounterEnd:%s:%s:%s:%s:%s",tostring(encounterID),tostring(encounterName),tostring(difficultyID),tostring(raidSize),tostring(endStatus))
  2683.   if endStatus ~= 1 then return end -- wipe
  2684.   core:RefreshLockInfo()
  2685.   local t = vars.db.Toons[thisToon]
  2686.   if not t then return end
  2687.   local name = encounterName
  2688.   if difficultyID and difficultyID > 0 then
  2689.     local diff = GetDifficultyInfo(difficultyID)
  2690.     if diff and #diff > 0 then
  2691.       name = name ..": "..diff
  2692.     end
  2693.   end
  2694.   t.lastboss = name
  2695.   t.lastbosstime = time()
  2696. end
  2697.  
  2698. function core:BOSS_KILL(event, encounterID, encounterName, ...)
  2699.   debug("BOSS_KILL:%s:%s",tostring(encounterID),tostring(encounterName)) -- ..":"..strjoin(":",...))
  2700.   local name = encounterName
  2701.   if name and type(name) == "string" then
  2702.     name = name:gsub(",.*$","") -- remove extraneous trailing boss titles
  2703.     name = strtrim(name)
  2704.     core:BossModEncounterEnd("BOSS_KILL", name)
  2705.   end
  2706. end
  2707.  
  2708. function addon:InGroup()
  2709.   if IsInRaid() then return "RAID"
  2710.   elseif GetNumGroupMembers() > 0 then return "PARTY"
  2711.   else return nil end
  2712. end
  2713.  
  2714. local function doExplicitReset(instancemsg, failed)
  2715.   if HasLFGRestrictions() or IsInInstance() or
  2716.     (addon:InGroup() and not UnitIsGroupLeader("player")) then return end
  2717.   if not failed then
  2718.     addon:HistoryUpdate(true)
  2719.   end
  2720.  
  2721.   local reportchan = addon:InGroup()
  2722.   if reportchan then
  2723.     if not failed then
  2724.       SendAddonMessage(addonName, "GENERATION_ADVANCE", reportchan)
  2725.     end
  2726.     if vars.db.Tooltip.ReportResets then
  2727.       local msg = instancemsg or RESET_INSTANCES
  2728.       msg = msg:gsub("\1241.+;.+;","") -- ticket 76, remove |1;; escapes on koKR
  2729.       SendChatMessage("<"..addonName.."> "..msg, reportchan)
  2730.     end
  2731.   end
  2732. end
  2733. hooksecurefunc("ResetInstances", doExplicitReset)
  2734.  
  2735. local resetmsg = INSTANCE_RESET_SUCCESS:gsub("%%s",".+")
  2736. local resetfails = { INSTANCE_RESET_FAILED, INSTANCE_RESET_FAILED_OFFLINE, INSTANCE_RESET_FAILED_ZONING }
  2737. for k,v in pairs(resetfails) do
  2738.   resetfails[k] = v:gsub("%%s",".+")
  2739. end
  2740. local raiddiffmsg = ERR_RAID_DIFFICULTY_CHANGED_S:gsub("%%s",".+")
  2741. local dungdiffmsg = ERR_DUNGEON_DIFFICULTY_CHANGED_S:gsub("%%s",".+")
  2742. local delaytime = 3 -- seconds to wait on zone change for settings to stabilize
  2743. function addon.HistoryEvent(f, evt, ...)
  2744.   --myprint("HistoryEvent: "..evt, ...)
  2745.   if evt == "CHAT_MSG_ADDON" then
  2746.     local prefix, message, channel, sender = ...
  2747.     if prefix ~= addonName then return end
  2748.     if message:match("^GENERATION_ADVANCE$") and not UnitIsUnit(sender,"player") then
  2749.       addon:HistoryUpdate(true)
  2750.     end
  2751.   elseif evt == "CHAT_MSG_SYSTEM" then
  2752.     local msg = ...
  2753.     if msg:match("^"..resetmsg.."$") then -- I performed expicit reset
  2754.       doExplicitReset(msg)
  2755.     elseif msg:match("^"..INSTANCE_SAVED.."$") then -- just got saved
  2756.       core:ScheduleTimer("HistoryUpdate", delaytime+1)
  2757.     elseif (msg:match("^"..raiddiffmsg.."$") or msg:match("^"..dungdiffmsg.."$")) and
  2758.       not addon:histZoneKey() then -- ignore difficulty messages when creating a party while inside an instance
  2759.       addon:HistoryUpdate(true)
  2760.     elseif msg:match(TRANSFER_ABORT_TOO_MANY_INSTANCES) then
  2761.       addon:HistoryUpdate(false,true)
  2762.     else
  2763.       for _,m in pairs(resetfails) do
  2764.         if msg:match("^"..m.."$") then
  2765.           doExplicitReset(msg, true) -- send failure chat message
  2766.         end
  2767.       end
  2768.     end
  2769.   elseif evt == "INSTANCE_BOOT_START" then -- left group inside instance, resets on boot
  2770.     addon:HistoryUpdate(true)
  2771.   elseif evt == "INSTANCE_BOOT_STOP" and addon:InGroup() then -- invited back
  2772.     addon.delayedReset = false
  2773.   elseif evt == "GROUP_ROSTER_UPDATE" and
  2774.     addon.histInGroup and not addon:InGroup() and -- ignore failed invites when solo
  2775.     not addon:histZoneKey() then -- left group outside instance, resets now
  2776.     addon:HistoryUpdate(true)
  2777.   elseif evt == "PLAYER_ENTERING_WORLD" or evt == "ZONE_CHANGED_NEW_AREA" or evt == "RAID_INSTANCE_WELCOME" then
  2778.     -- delay updates while settings stabilize
  2779.     local waittime = delaytime + math.max(0,10 - GetFramerate())
  2780.     addon.delayUpdate = time() + waittime
  2781.     core:ScheduleTimer("HistoryUpdate", waittime+1)
  2782.   end
  2783. end
  2784.  
  2785.  
  2786. addon.histReapTime = 60*60 -- 1 hour
  2787. addon.histLimit = 10 -- instances per hour
  2788. function addon:histZoneKey()
  2789.   local instname, insttype, diff, diffname, maxPlayers, playerDifficulty, isDynamicInstance = GetInstanceInfo()
  2790.   if insttype == nil or insttype == "none" or insttype == "arena" or insttype == "pvp" then -- pvp doesnt count
  2791.     return nil
  2792.   end
  2793.   if IsInLFGDungeon() or IsInScenarioGroup() then -- LFG instances don't count
  2794.     return nil
  2795.   end
  2796.   if C_Garrison.IsOnGarrisonMap() then -- Garrisons don't count
  2797.     return nil
  2798.   end
  2799.   -- check if we're locked (using FindInstance so we don't complain about unsaved unknown instances)
  2800.   local truename = addon:FindInstance(instname, insttype == "raid")
  2801.   local locked = false
  2802.   local inst = truename and vars.db.Instances[truename]
  2803.   inst = inst and inst[thisToon]
  2804.   for d=1,maxdiff do
  2805.     if inst and inst[d] and inst[d].Locked then
  2806.       locked = true
  2807.     end
  2808.   end
  2809.   if diff == 1 and maxPlayers == 5 then -- never locked to 5-man regs
  2810.     locked = false
  2811.   end
  2812.   local toonstr = thisToon
  2813.   if not db.Tooltip.ShowServer then
  2814.     toonstr = strsplit(" - ", toonstr)
  2815.   end
  2816.   local desc = toonstr .. ": " .. instname
  2817.   if diffname and #diffname > 0 then
  2818.     desc = desc .. " - " .. diffname
  2819.   end
  2820.   local key = thisToon..":"..instname..":"..insttype..":"..diff
  2821.   if not locked then
  2822.     key = key..":"..vars.db.histGeneration
  2823.   end
  2824.   return key, desc, locked
  2825. end
  2826.  
  2827. function addon:HistoryUpdate(forcereset, forcemesg)
  2828.   vars.db.histGeneration = vars.db.histGeneration or 1
  2829.   if forcereset and addon:histZoneKey() then -- delay reset until we zone out
  2830.     debug("HistoryUpdate reset delayed")
  2831.     addon.delayedReset = true
  2832.   end
  2833.   if (forcereset or addon.delayedReset) and not addon:histZoneKey() then
  2834.     debug("HistoryUpdate generation advance")
  2835.     vars.db.histGeneration = (vars.db.histGeneration + 1) % 100000
  2836.     addon.delayedReset = false
  2837.   end
  2838.   local now = time()
  2839.   if addon.delayUpdate and now < addon.delayUpdate then
  2840.     debug("HistoryUpdate delayed")
  2841.     return
  2842.   end
  2843.   local zoningin = false
  2844.   local newzone, newdesc, locked = addon:histZoneKey()
  2845.   -- touch zone we left
  2846.   if addon.histLastZone then
  2847.     local lz = vars.db.History[addon.histLastZone]
  2848.     if lz then
  2849.       lz.last = now
  2850.     end
  2851.   elseif newzone then
  2852.     zoningin = true
  2853.   end
  2854.   addon.histLastZone = newzone
  2855.   addon.histInGroup = addon:InGroup()
  2856.   -- touch/create new zone
  2857.   if newzone then
  2858.     local nz = vars.db.History[newzone]
  2859.     if not nz then
  2860.       nz = { create = now, desc = newdesc }
  2861.       vars.db.History[newzone] = nz
  2862.       if locked then -- creating a locked instance, delete unlocked version
  2863.         vars.db.History[newzone..":"..vars.db.histGeneration] = nil
  2864.       end
  2865.     end
  2866.     nz.last = now
  2867.   end
  2868.   -- reap old zones
  2869.   local livecnt = 0
  2870.   local oldestkey, oldesttime
  2871.   for zk, zi in pairs(vars.db.History) do
  2872.     if now > zi.last + addon.histReapTime or
  2873.       zi.last > (now + 3600) then -- temporary bug fix
  2874.       debug("Reaping %s",zi.desc)
  2875.       vars.db.History[zk] = nil
  2876.     else
  2877.       livecnt = livecnt + 1
  2878.       if not oldesttime or zi.last < oldesttime then
  2879.         oldestkey = zk
  2880.         oldesttime = zi.last
  2881.       end
  2882.     end
  2883.   end
  2884.   local oldestrem = oldesttime and (oldesttime+addon.histReapTime-now)
  2885.   local oldestremt = (oldestrem and SecondsToTime(oldestrem,false,false,1)) or "n/a"
  2886.   local oldestremtm = (oldestrem and SecondsToTime(math.floor((oldestrem+59)/60)*60,false,false,1)) or "n/a"
  2887.   if addon.db.dbg then
  2888.     local msg = livecnt.." live instances, oldest ("..(oldestkey or "none")..") expires in "..oldestremt..". Current Zone="..(newzone or "nil")
  2889.     if msg ~= addon.lasthistdbg then
  2890.       addon.lasthistdbg = msg
  2891.       debug(msg)
  2892.     end
  2893.     --myprint(vars.db.History)
  2894.   end
  2895.   -- display update
  2896.  
  2897.   if forcemesg or (vars.db.Tooltip.LimitWarn and zoningin and livecnt >= addon.histLimit-1) then
  2898.     chatMsg(L["Warning: You've entered about %i instances recently and are approaching the %i instance per hour limit for your account. More instances should be available in %s."],livecnt, addon.histLimit, oldestremt)
  2899.   end
  2900.   addon.histLiveCount = livecnt
  2901.   addon.histOldest = oldestremt
  2902.   if db.Tooltip.HistoryText and livecnt > 0 then
  2903.     vars.dataobject.text = "("..livecnt.."/"..(oldestremt or "?")..")"
  2904.     addon.histTextthrottle = math.min(oldestrem+1, addon.histTextthrottle or 15)
  2905.     addon.resetDetect:SetScript("OnUpdate", addon.histTextUpdate)
  2906.   else
  2907.     vars.dataobject.text = addonAbbrev
  2908.     addon.resetDetect:SetScript("OnUpdate", nil)
  2909.   end
  2910. end
  2911. function core:HistoryUpdate(...) return addon:HistoryUpdate(...) end
  2912. function addon.histTextUpdate(self, elap)
  2913.   addon.histTextthrottle = addon.histTextthrottle - elap
  2914.   if addon.histTextthrottle > 0 then return end
  2915.   addon.histTextthrottle = 15
  2916.   addon:HistoryUpdate()
  2917. end
  2918.  
  2919. local function localarr(name) -- save on memory churn by reusing arrays in updates
  2920.   name = "localarr#"..name
  2921.   core[name] = core[name] or {}
  2922.   return wipe(core[name])
  2923. end
  2924.  
  2925. function core:memcheck(context)
  2926.   UpdateAddOnMemoryUsage()
  2927.   local newval = GetAddOnMemoryUsage("SavedInstances")
  2928.   core.memusage = core.memusage or 0
  2929.   if newval ~= core.memusage then
  2930.     debug("%.3f KB in %s",(newval - core.memusage),context)
  2931.     core.memusage = newval
  2932.   end
  2933. end
  2934.  
  2935. -- Lightweight refresh of just quest flag information
  2936. -- all may be nil if not instantiataed
  2937. function core:QuestRefresh(recoverdaily, questcomplete, nextreset, weeklyreset)
  2938.   local tiq = vars.db.Toons[thisToon]
  2939.   tiq = tiq and tiq.Quests
  2940.   if not tiq then return end
  2941.   nextreset = nextreset or addon:GetNextDailyResetTime()
  2942.   weeklyreset = weeklyreset or addon:GetNextWeeklyResetTime()
  2943.   if not nextreset or not weeklyreset then return end
  2944.  
  2945.   for _, qinfo in pairs(addon:specialQuests()) do
  2946.     local qid = qinfo.quest
  2947.     if IsQuestFlaggedCompleted(qid) or (questcomplete and questcomplete[qid]) then
  2948.       local q = tiq[qid] or {}
  2949.       tiq[qid] = q
  2950.       q.Title = qinfo.name
  2951.       q.Zone = qinfo.zone
  2952.       if qinfo.daily then
  2953.         q.Expires = nextreset
  2954.         q.isDaily = true
  2955.       else
  2956.         q.Expires = weeklyreset
  2957.         q.isDaily = nil
  2958.       end
  2959.     end
  2960.   end
  2961.  
  2962.   local now = time()
  2963.   db.QuestDB.Weekly.expires = weeklyreset
  2964.   db.QuestDB.AccountWeekly.expires = weeklyreset
  2965.   db.QuestDB.Darkmoon.expires = addon:GetNextDarkmoonResetTime()
  2966.   for scope, list in pairs(db.QuestDB) do
  2967.     local questlist = tiq
  2968.     if scope:find("Account") then
  2969.       questlist = db.Quests
  2970.     end
  2971.     if recoverdaily or (scope ~= "Daily") then
  2972.       for qid, mapid in pairs(list) do
  2973.         if tonumber(qid) and (IsQuestFlaggedCompleted(qid) or
  2974.           (questcomplete and questcomplete[qid])) and not questlist[qid] and -- recovering a lost quest
  2975.           (list.expires == nil or list.expires > now) then -- don't repop darkmoon quests from last faire
  2976.           local title, link = addon:QuestInfo(qid)
  2977.           if title then
  2978.             local found
  2979.             for _,info in pairs(questlist) do
  2980.               if title == info.Title then -- avoid faction duplicates, since both flags are set
  2981.                 found = true
  2982.                 break
  2983.               end
  2984.             end
  2985.             if not found then
  2986.               debug("Recovering lost quest: "..title.." ("..scope..")")
  2987.               questlist[qid] = { ["Title"] = title, ["Link"] = link,
  2988.                 ["isDaily"] = (scope:find("Daily") and true) or nil,
  2989.                 ["Expires"] = list.expires,
  2990.                 ["Zone"] = GetMapNameByID(mapid) }
  2991.             end
  2992.           end
  2993.         end
  2994.       end
  2995.     end
  2996.   end
  2997.   addon:QuestCount(thisToon)
  2998. end
  2999.  
  3000. function core:Refresh(recoverdaily)
  3001.   -- update entire database from the current character's perspective
  3002.   addon:UpdateInstanceData()
  3003.   if not addon.instancesUpdated then addon.RefreshPending = true; return end -- wait for UpdateInstanceData to succeed
  3004.   local nextreset = addon:GetNextDailyResetTime()
  3005.   if not nextreset or ((nextreset - time()) > (24*3600 - 5*60)) then  -- allow 5 minutes for quest DB to update after daily rollover
  3006.     debug("Skipping core:Refresh() near daily reset")
  3007.     addon:UpdateToonData()
  3008.     return
  3009.   end
  3010.   local temp = localarr("RefreshTemp")
  3011.   for name, instance in pairs(vars.db.Instances) do -- clear current toons lockouts before refresh
  3012.     local id = instance.LFDID
  3013.     if instance[thisToon]
  3014.     -- disabled for ticket 178/195:
  3015.     --and not (id and addon.LFRInstances[id] and select(2,GetLFGDungeonNumEncounters(id)) == 0) -- ticket 103
  3016.     then
  3017.       temp[name] = instance[thisToon] -- use a temp to reduce memory churn
  3018.       for diff,info in pairs(temp[name]) do
  3019.         wipe(info)
  3020.       end
  3021.       instance[thisToon] = nil
  3022.     end
  3023.   end
  3024.   local numsaved = GetNumSavedInstances()
  3025.   if numsaved > 0 then
  3026.     for i = 1, numsaved do
  3027.       local name, id, expires, diff, locked, extended, mostsig, raid, players, diffname = GetSavedInstanceInfo(i)
  3028.       local truename, instance = addon:LookupInstance(nil, name, raid)
  3029.       if expires and expires > 0 then
  3030.         expires = expires + time()
  3031.       else
  3032.         expires = 0
  3033.       end
  3034.       instance.Raid = instance.Raid or raid
  3035.       instance[thisToon] = instance[thisToon] or temp[truename] or { }
  3036.       local info = instance[thisToon][diff] or {}
  3037.       wipe(info)
  3038.       info.ID = id
  3039.       info.Expires = expires
  3040.       info.Link = GetSavedInstanceChatLink(i)
  3041.       info.Locked = locked
  3042.       info.Extended = extended
  3043.       instance[thisToon][diff] = info
  3044.     end
  3045.   end
  3046.  
  3047.   local weeklyreset = addon:GetNextWeeklyResetTime()
  3048.   for id,_ in pairs(addon.LFRInstances) do
  3049.     local numEncounters, numCompleted = GetLFGDungeonNumEncounters(id);
  3050.     if ( numCompleted and numCompleted > 0 and weeklyreset ) then
  3051.       local truename, instance = addon:LookupInstance(id, nil, true)
  3052.       instance[thisToon] = instance[thisToon] or temp[truename] or { }
  3053.       local info = instance[thisToon][2] or {}
  3054.       instance[thisToon][2] = info
  3055.       if not (info.Expires and info.Expires < (time() + 300)) then -- ticket 109: don't refresh expiration close to reset
  3056.         wipe(info)
  3057.         info.Expires = weeklyreset
  3058.       end
  3059.       info.ID = -1*numEncounters
  3060.       for i=1, numEncounters do
  3061.         local bossName, texture, isKilled = GetLFGDungeonEncounterInfo(id, i);
  3062.         info[i] = isKilled
  3063.       end
  3064.     end
  3065.   end
  3066.  
  3067.   local questcomplete = GetQuestsCompleted(localarr("QuestCompleteTemp"))
  3068.   local wbsave = localarr("wbsave")
  3069.   if GetNumSavedWorldBosses and GetSavedWorldBossInfo then -- 5.4
  3070.     for i=1,GetNumSavedWorldBosses() do
  3071.       local name, id, reset = GetSavedWorldBossInfo(i)
  3072.       wbsave[name] = true
  3073.   end
  3074.   end
  3075.   for _,einfo in pairs(addon.WorldBosses) do
  3076.     if weeklyreset and (
  3077.       (einfo.quest and IsQuestFlaggedCompleted(einfo.quest)) or
  3078.       (questcomplete and einfo.quest and questcomplete[einfo.quest]) or
  3079.       wbsave[einfo.savename or einfo.name]
  3080.       ) then
  3081.       local truename = einfo.name
  3082.       local instance = vars.db.Instances[truename]
  3083.       instance[thisToon] = instance[thisToon] or temp[truename] or { }
  3084.       local info = instance[thisToon][2] or {}
  3085.       wipe(info)
  3086.       instance[thisToon][2] = info
  3087.       info.Expires = weeklyreset
  3088.       info.ID = -1
  3089.       info[1] = true
  3090.     end
  3091.   end
  3092.  
  3093.   core:QuestRefresh(recoverdaily, questcomplete, nextreset, weeklyreset)
  3094.  
  3095.   local icnt, dcnt = 0,0
  3096.   for name, _ in pairs(temp) do
  3097.     if vars.db.Instances[name][thisToon] then
  3098.       for diff,info in pairs(vars.db.Instances[name][thisToon]) do
  3099.         if not info.ID then
  3100.           vars.db.Instances[name][thisToon][diff] = nil
  3101.           dcnt = dcnt + 1
  3102.         end
  3103.       end
  3104.     else
  3105.       icnt = icnt + 1
  3106.     end
  3107.   end
  3108.   --debug("Refresh temp reaped "..icnt.." instances and "..dcnt.." diffs")
  3109.   wipe(temp)
  3110.   addon:UpdateToonData()
  3111. end
  3112.  
  3113. local function UpdateTooltip(self,elap)
  3114.   if not tooltip or not tooltip:IsShown() then return end
  3115.   if addon.firstupdate then
  3116.     -- ticket 155: fix QTip backdrop which somehow gets corrupted sometimes, no idea why
  3117.     tooltip:SetBackdrop(GameTooltip:GetBackdrop())
  3118.     tooltip:SetBackdropColor(GameTooltip:GetBackdropColor());
  3119.     tooltip:SetBackdropBorderColor(GameTooltip:GetBackdropBorderColor())
  3120.     addon.firstupdate = false
  3121.     -- easy BugFix by q3fuba
  3122.     addon:SkinFrame(tooltip, tooltip:GetName())
  3123.   end
  3124.   addon.updatetooltip_throttle = (addon.updatetooltip_throttle or 10) + elap
  3125.   if addon.updatetooltip_throttle < 0.5 then return end
  3126.   addon.updatetooltip_throttle = 0
  3127.   if tooltip.anchorframe then
  3128.     core:ShowTooltip(tooltip.anchorframe)
  3129.   end
  3130. end
  3131.  
  3132. -- sorted traversal function for character table
  3133. local cpairs
  3134. do
  3135.   local cnext_list = {}
  3136.   local cnext_pos
  3137.   local cnext_ekey
  3138.   local function cnext(t,i)
  3139.     local e = cnext_list[cnext_pos]
  3140.     if not e then
  3141.       return nil
  3142.     else
  3143.       cnext_pos = cnext_pos + 1
  3144.       local n = e[cnext_ekey]
  3145.       return n, t[n]
  3146.     end
  3147.   end
  3148.  
  3149.   local function cpairs_sort(a,b)
  3150.     -- generic multi-key sort
  3151.     for k,av in ipairs(a) do
  3152.       local bv = b[k]
  3153.       if av ~= bv then
  3154.         return av < bv
  3155.       end
  3156.     end
  3157.     return false -- required for sort stability when a==a
  3158.   end
  3159.  
  3160.   cpairs = function(t, usecache)
  3161.     local settings = db.Tooltip
  3162.     local realmgroup_key
  3163.     local realmgroup_min
  3164.     if not usecache then
  3165.       local thisrealm = GetRealmName()
  3166.       if settings.ConnectedRealms ~= "ignore" then
  3167.         local group = core:getRealmGroup(thisrealm)
  3168.         thisrealm = group or thisrealm
  3169.       end
  3170.       wipe(cnext_list)
  3171.       cnext_pos = 1
  3172.       for n,_ in pairs(t) do
  3173.         local t = vars.db.Toons[n]
  3174.         local tn, tr = n:match('^(.*) [-] (.*)$')
  3175.         if t and
  3176.           (t.Show ~= "never" or (n == thisToon and settings.SelfAlways))  and
  3177.           (not settings.ServerOnly
  3178.           or thisrealm == tr
  3179.           or thisrealm == core:getRealmGroup(tr))
  3180.         then
  3181.           local e = {}
  3182.           cnext_ekey = 1
  3183.  
  3184.           if settings.SelfFirst then
  3185.             if n == thisToon then
  3186.               e[cnext_ekey] = 1
  3187.             else
  3188.               e[cnext_ekey] = 2
  3189.             end
  3190.             cnext_ekey = cnext_ekey + 1
  3191.           end
  3192.  
  3193.           if settings.ServerSort then
  3194.             if settings.ConnectedRealms == "ignore" then
  3195.               e[cnext_ekey] = tr
  3196.               cnext_ekey = cnext_ekey + 1
  3197.             else
  3198.               local rgroup = core:getRealmGroup(tr)
  3199.               if rgroup then -- connected realm
  3200.                 realmgroup_min = realmgroup_min or {}
  3201.                 if not realmgroup_min[rgroup] or tr < realmgroup_min[rgroup] then
  3202.                   realmgroup_min[rgroup] = tr -- lowest active realm in group
  3203.                 end
  3204.               else
  3205.                 rgroup = tr
  3206.               end
  3207.               realmgroup_key = cnext_ekey
  3208.               e[cnext_ekey] = rgroup
  3209.               cnext_ekey = cnext_ekey + 1
  3210.  
  3211.               if settings.ConnectedRealms == "group" then
  3212.                 e[cnext_ekey] = tr
  3213.                 cnext_ekey = cnext_ekey + 1
  3214.               end
  3215.             end
  3216.           end
  3217.  
  3218.           e[cnext_ekey] = t.Order
  3219.           cnext_ekey = cnext_ekey + 1
  3220.  
  3221.           e[cnext_ekey] = n
  3222.           cnext_list[cnext_pos] = e
  3223.           cnext_pos = cnext_pos + 1
  3224.         end
  3225.       end
  3226.       if realmgroup_key then -- second pass, convert group id to min name
  3227.         for _,e in ipairs(cnext_list) do
  3228.           local id = e[realmgroup_key]
  3229.           if type(id) == "number" then
  3230.             e[realmgroup_key] = realmgroup_min[id]
  3231.           end
  3232.       end
  3233.       end
  3234.       table.sort(cnext_list, cpairs_sort)
  3235.       --myprint(cnext_list)
  3236.     end
  3237.     cnext_pos = 1
  3238.     return cnext, t, nil
  3239.   end
  3240. end
  3241.  
  3242. function addon:IsDetached()
  3243.   return addon.detachframe and addon.detachframe:IsShown()
  3244. end
  3245. function addon:HideDetached()
  3246.   addon.detachframe:Hide()
  3247. end
  3248. function addon:ToggleDetached()
  3249.   if addon:IsDetached() then
  3250.     addon:HideDetached()
  3251.   else
  3252.     addon:ShowDetached()
  3253.   end
  3254. end
  3255.  
  3256. function addon:ShowDetached()
  3257.   if not addon.detachframe then
  3258.     local f = CreateFrame("Frame","SavedInstancesDetachHeader",UIParent,"BasicFrameTemplate")
  3259.     f:SetMovable(true)
  3260.     f:SetFrameStrata("TOOLTIP")
  3261.     f:SetFrameLevel(100) -- prevent weird interlacings with other tooltips
  3262.     f:SetClampedToScreen(true)
  3263.     f:EnableMouse(true)
  3264.     f:SetUserPlaced(true)
  3265.     f:SetAlpha(0.5)
  3266.     if vars.db.Tooltip.posx and vars.db.Tooltip.posy then
  3267.       f:SetPoint("TOPLEFT",vars.db.Tooltip.posx,-vars.db.Tooltip.posy)
  3268.     else
  3269.       f:SetPoint("CENTER")
  3270.     end
  3271.     f:SetScript("OnMouseDown", function() f:StartMoving() end)
  3272.     f:SetScript("OnMouseUp", function() f:StopMovingOrSizing()
  3273.       vars.db.Tooltip.posx = f:GetLeft()
  3274.       vars.db.Tooltip.posy = UIParent:GetTop() - (f:GetTop()*f:GetScale())
  3275.     end)
  3276.     f:SetScript("OnHide", function() if tooltip then QTip:Release(tooltip); tooltip = nil end  end )
  3277.     f:SetScript("OnUpdate", function(self)
  3278.       if not tooltip then f:Hide(); return end
  3279.       local w,h = tooltip:GetSize()
  3280.       self:SetSize(w*tooltip:GetScale(),(h+20)*tooltip:GetScale())
  3281.       --[[
  3282.       tooltip:ClearAllPoints()
  3283.       tooltip:SetPoint("BOTTOMLEFT",addon.detachframe)
  3284.       tooltip:SetFrameLevel(addon.detachframe:GetFrameLevel()+1)
  3285.             tooltip:Show()
  3286.       --]]
  3287.     end)
  3288.     f:SetScript("OnKeyDown", function(self,key)
  3289.       if key == "ESCAPE" then
  3290.         f:SetPropagateKeyboardInput(false)
  3291.         f:Hide();
  3292.       end
  3293.     end)
  3294.     f:EnableKeyboard(true)
  3295.     addon.detachframe = f
  3296.   end
  3297.   local f = addon.detachframe
  3298.   f:Show()
  3299.   addon:SkinFrame(f,f:GetName())
  3300.   f:SetPropagateKeyboardInput(true)
  3301.   if tooltip then tooltip:Hide() end
  3302.   core:ShowTooltip(f)
  3303. end
  3304.  
  3305. -----------------------------------------------------------------------------------------------
  3306. -- tooltip event handlers
  3307.  
  3308. local function OpenLFD(self, instanceid, button)
  3309.   if LFDParentFrame and LFDParentFrame:IsVisible() and LFDQueueFrame.type ~= instanceid then
  3310.   -- changing entries
  3311.   else
  3312.     ToggleLFDParentFrame()
  3313.   end
  3314.   if LFDParentFrame and LFDParentFrame:IsVisible() and LFDQueueFrame_SetType then
  3315.     LFDQueueFrame_SetType(instanceid)
  3316.   end
  3317. end
  3318.  
  3319. local function OpenLFR(self, instanceid, button)
  3320.   if RaidFinderFrame and RaidFinderFrame:IsVisible() and RaidFinderQueueFrame.raid ~= instanceid then
  3321.   -- changing entries
  3322.   else
  3323.     PVEFrame_ToggleFrame("GroupFinderFrame", RaidFinderFrame)
  3324.   end
  3325.   if RaidFinderFrame and RaidFinderFrame:IsVisible() and RaidFinderQueueFrame_SetRaid then
  3326.     RaidFinderQueueFrame_SetRaid(instanceid)
  3327.   end
  3328. end
  3329.  
  3330. local function OpenLFS(self, instanceid, button)
  3331.   if ScenarioFinderFrame and ScenarioFinderFrame:IsVisible() and ScenarioQueueFrame.type ~= instanceid then
  3332.   -- changing entries
  3333.   else
  3334.     PVEFrame_ToggleFrame("GroupFinderFrame", ScenarioFinderFrame)
  3335.   end
  3336.   if ScenarioFinderFrame and ScenarioFinderFrame:IsVisible() and ScenarioQueueFrame_SetType then
  3337.     ScenarioQueueFrame_SetType(instanceid)
  3338.   end
  3339. end
  3340.  
  3341. local function OpenCurrency(self, _, button)
  3342.   ToggleCharacter("TokenFrame")
  3343. end
  3344.  
  3345. local function ChatLink(self, link, button)
  3346.   if not link then return end
  3347.   if ChatEdit_GetActiveWindow() then
  3348.     ChatEdit_InsertLink(link)
  3349.   else
  3350.     ChatFrame_OpenChat(link, DEFAULT_CHAT_FRAME)
  3351.   end
  3352. end
  3353.  
  3354. local function CloseTooltips()
  3355.   GameTooltip:Hide()
  3356.   if indicatortip then
  3357.     indicatortip:Hide()
  3358.   end
  3359. end
  3360.  
  3361. local function DoNothing() end
  3362.  
  3363. -----------------------------------------------------------------------------------------------
  3364.  
  3365. local function ShowAll()
  3366.   return (IsAltKeyDown() and true) or false
  3367. end
  3368.  
  3369. local columnCache = { [true] = {}, [false] = {} }
  3370. local function addColumns(columns, toon, tooltip)
  3371.   for c = 1, maxcol do
  3372.     columns[toon..c] = columns[toon..c] or tooltip:AddColumn("CENTER")
  3373.   end
  3374.   columnCache[ShowAll()][toon] = true
  3375. end
  3376. addon.scaleCache = {}
  3377.  
  3378. function core:HeaderFont()
  3379.   if not addon.headerfont then
  3380.     local temp = QTip:Acquire("SavedInstancesHeaderTooltip", 1, "LEFT")
  3381.     addon.headerfont = CreateFont("SavedInstancedTooltipHeaderFont")
  3382.     local hFont = temp:GetHeaderFont()
  3383.     local hFontPath, hFontSize,_ hFontPath, hFontSize, _ = hFont:GetFont()
  3384.     addon.headerfont:SetFont(hFontPath, hFontSize, "OUTLINE")
  3385.     QTip:Release(temp)
  3386.   end
  3387.   return addon.headerfont
  3388. end
  3389.  
  3390. function core:ShowTooltip(anchorframe)
  3391.   local showall = ShowAll()
  3392.   if tooltip and tooltip:IsShown() and
  3393.     core.showall == showall and
  3394.     core.scale == (addon.scaleCache[showall] or vars.db.Tooltip.Scale)
  3395.   then
  3396.     return -- skip update
  3397.   end
  3398.   local starttime = debugprofilestop()
  3399.   core.showall = showall
  3400.   local showexpired = showall or vars.db.Tooltip.ShowExpired
  3401.   if tooltip then QTip:Release(tooltip) end
  3402.   tooltip = QTip:Acquire("SavedInstancesTooltip", 1, "LEFT")
  3403.   tooltip:SetCellMarginH(0)
  3404.   tooltip.anchorframe = anchorframe
  3405.   tooltip:SetScript("OnUpdate", UpdateTooltip)
  3406.   addon.firstupdate = true
  3407.   tooltip:Clear()
  3408.   core.scale = addon.scaleCache[showall] or vars.db.Tooltip.Scale
  3409.   tooltip:SetScale(core.scale)
  3410.   tooltip:SetHeaderFont(core:HeaderFont())
  3411.   addon:HistoryUpdate()
  3412.   local headText
  3413.   if addon.histLiveCount and addon.histLiveCount > 0 then
  3414.     headText = string.format("%s%s (%d/%s)%s",GOLDFONT,addonName,addon.histLiveCount,(addon.histOldest or "?"),FONTEND)
  3415.   else
  3416.     headText = string.format("%s%s%s",GOLDFONT,addonName,FONTEND)
  3417.   end
  3418.   local headLine = tooltip:AddHeader(headText)
  3419.   tooltip:SetCellScript(headLine, 1, "OnEnter", ShowAccountSummary )
  3420.   tooltip:SetCellScript(headLine, 1, "OnLeave", CloseTooltips)
  3421.   addon:UpdateToonData()
  3422.   local columns = localarr("columns")
  3423.   for toon,_ in cpairs(columnCache[showall]) do
  3424.     addColumns(columns, toon, tooltip)
  3425.     columnCache[showall][toon] = false
  3426.   end
  3427.   -- allocating columns for characters
  3428.   for toon, t in cpairs(vars.db.Toons) do
  3429.     if vars.db.Toons[toon].Show == "always" or
  3430.       (toon == thisToon and vars.db.Tooltip.SelfAlways) then
  3431.       addColumns(columns, toon, tooltip)
  3432.     end
  3433.   end
  3434.   -- determining how many instances will be displayed per category
  3435.   local categoryshown = localarr("categoryshown") -- remember if each category will be shown
  3436.   local instancesaved = localarr("instancesaved") -- remember if each instance has been saved or not (boolean)
  3437.   local wbcons = vars.db.Tooltip.CombineWorldBosses
  3438.   local worldbosses = wbcons and localarr("worldbosses")
  3439.   local wbalways = false
  3440.   local lfrcons = vars.db.Tooltip.CombineLFR
  3441.   local lfrbox = lfrcons and localarr("lfrbox")
  3442.   local lfrmap = lfrcons and localarr("lfrmap")
  3443.   for _, category in ipairs(addon:OrderedCategories()) do
  3444.     for _, instance in ipairs(addon:OrderedInstances(category)) do
  3445.       local inst = vars.db.Instances[instance]
  3446.       if inst.Show == "always" then
  3447.         categoryshown[category] = true
  3448.       end
  3449.       if inst.Show ~= "never" then
  3450.         if wbcons and inst.WorldBoss and inst.Expansion <= GetExpansionLevel() then
  3451.           if vars.db.Tooltip.ReverseInstances then
  3452.             table.insert(worldbosses, instance)
  3453.           else
  3454.             table.insert(worldbosses, 1, instance)
  3455.           end
  3456.           wbalways = wbalways or (inst.Show == "always")
  3457.         end
  3458.         local lfrinfo = lfrcons and inst.LFDID and addon.LFRInstances[inst.LFDID]
  3459.         local lfrboxid
  3460.         if lfrinfo then
  3461.           lfrboxid = lfrinfo.parent
  3462.           lfrmap[inst.LFDID] = instance
  3463.           if inst.Show == "always" then
  3464.             lfrbox[lfrboxid] = true
  3465.           end
  3466.         end
  3467.         for toon, t in cpairs(vars.db.Toons, true) do
  3468.           for diff = 1, maxdiff do
  3469.             if inst[toon] and inst[toon][diff] then
  3470.               if (inst[toon][diff].Expires > 0) then
  3471.                 if lfrinfo then
  3472.                   lfrbox[lfrboxid] = true
  3473.                   instancesaved[lfrboxid] = true
  3474.                 elseif wbcons and inst.WorldBoss then
  3475.                   instancesaved[L["World Bosses"]] = true
  3476.                 else
  3477.                   instancesaved[instance] = true
  3478.                 end
  3479.                 categoryshown[category] = true
  3480.               elseif showall then
  3481.                 categoryshown[category] = true
  3482.               end
  3483.             end
  3484.           end
  3485.         end
  3486.       end
  3487.     end
  3488.   end
  3489.   local categories = 0
  3490.   -- determining how many categories have instances that will be shown
  3491.   if vars.db.Tooltip.ShowCategories then
  3492.     for category, _ in pairs(categoryshown) do
  3493.       categories = categories + 1
  3494.     end
  3495.   end
  3496.   -- allocating tooltip space for instances, categories, and space between categories
  3497.   local categoryrow = localarr("categoryrow") -- remember where each category heading goes
  3498.   local instancerow = localarr("instancerow") -- remember where each instance goes
  3499.   local blankrow = localarr("blankrow") -- track blank lines
  3500.   local firstcategory = true -- use this to skip spacing before the first category
  3501.   local function addsep()
  3502.     local line = tooltip:AddSeparator(6,0,0,0,0)
  3503.     blankrow[line] = true
  3504.     return line
  3505.   end
  3506.   for _, category in ipairs(addon:OrderedCategories()) do
  3507.     if categoryshown[category] then
  3508.       if not firstcategory and vars.db.Tooltip.CategorySpaces then
  3509.         addsep()
  3510.       end
  3511.       if (categories > 1 or vars.db.Tooltip.ShowSoloCategory) and categoryshown[category] then
  3512.         local line = tooltip:AddLine()
  3513.         categoryrow[category] = line
  3514.         blankrow[line] = true
  3515.       end
  3516.       for _, instance in ipairs(addon:OrderedInstances(category)) do
  3517.         local inst = vars.db.Instances[instance]
  3518.         if not (wbcons and inst.WorldBoss) and
  3519.           not (lfrcons and addon.LFRInstances[inst.LFDID]) then
  3520.           if inst.Show == "always" then
  3521.             instancerow[instance] = instancerow[instance] or tooltip:AddLine()
  3522.           end
  3523.           if inst.Show ~= "never" then
  3524.             for toon, t in cpairs(vars.db.Toons, true) do
  3525.               for diff = 1, maxdiff do
  3526.                 if inst[toon] and inst[toon][diff] and (inst[toon][diff].Expires > 0 or showexpired) then
  3527.                   instancerow[instance] = instancerow[instance] or tooltip:AddLine()
  3528.                   addColumns(columns, toon, tooltip)
  3529.                 end
  3530.               end
  3531.             end
  3532.           end
  3533.         end
  3534.         if lfrcons and inst.LFDID then
  3535.           -- check if this parent instance has corresponding lfrboxes, and create them
  3536.           if lfrbox[inst.LFDID] then
  3537.             lfrbox[L["LFR"]..": "..instance] = tooltip:AddLine()
  3538.           end
  3539.           lfrbox[inst.LFDID] = nil
  3540.         end
  3541.       end
  3542.       firstcategory = false
  3543.     end
  3544.   end
  3545.   -- now printing instance data
  3546.   for instance, row in pairs(instancerow) do
  3547.     local inst = vars.db.Instances[instance]
  3548.     tooltip:SetCell(row, 1, (instancesaved[instance] and GOLDFONT or GRAYFONT) .. instance .. FONTEND)
  3549.     if addon.LFRInstances[inst.LFDID] then
  3550.       tooltip:SetLineScript(row, "OnMouseDown", OpenLFR, inst.LFDID)
  3551.     end
  3552.     for toon, t in cpairs(vars.db.Toons, true) do
  3553.       if inst[toon] then
  3554.         local showcol = localarr("showcol")
  3555.         local showcnt = 0
  3556.         for diff = 1, maxdiff do
  3557.           if inst[toon][diff] and (inst[toon][diff].Expires > 0 or showexpired) then
  3558.             showcnt = showcnt + 1
  3559.             showcol[diff] = true
  3560.           end
  3561.         end
  3562.         local base = 1
  3563.         local span = maxcol
  3564.         if showcnt > 1 then
  3565.           span = 1
  3566.         end
  3567.         if showcnt > maxcol then
  3568.           bugReport("Column overflow! showcnt="..showcnt)
  3569.         end
  3570.         for diff = 1, maxdiff do
  3571.           if showcol[diff] then
  3572.             local col = columns[toon..base]
  3573.             tooltip:SetCell(row, col,
  3574.               DifficultyString(instance, diff, toon, inst[toon][diff].Expires == 0), span)
  3575.             tooltip:SetCellScript(row, col, "OnEnter", ShowIndicatorTooltip, {instance, toon, diff})
  3576.             tooltip:SetCellScript(row, col, "OnLeave", CloseTooltips)
  3577.             if addon.LFRInstances[inst.LFDID] then
  3578.               tooltip:SetCellScript(row, col, "OnMouseDown", OpenLFR, inst.LFDID)
  3579.             else
  3580.               local link = inst[toon][diff].Link
  3581.               if link then
  3582.                 tooltip:SetCellScript(row, col, "OnMouseDown", ChatLink, link)
  3583.               end
  3584.             end
  3585.             base = base + 1
  3586.           elseif columns[toon..diff] and showcnt > 1 then
  3587.             tooltip:SetCell(row, columns[toon..diff], "")
  3588.           end
  3589.         end
  3590.       end
  3591.     end
  3592.   end
  3593.  
  3594.   -- combined LFRs
  3595.   if lfrcons then
  3596.     for boxname, line in pairs(lfrbox) do
  3597.       if type(boxname) == "number" then
  3598.         bugReport("Unrecognized LFR instance parent id= "..boxname)
  3599.         lfrbox[boxname] = nil
  3600.       end
  3601.     end
  3602.     for boxname, line in pairs(lfrbox) do
  3603.       local boxtype, pinstance = boxname:match("^([^:]+): (.+)$")
  3604.       local pinst = vars.db.Instances[pinstance]
  3605.       local boxid = pinst.LFDID
  3606.       local firstid
  3607.       local total = 0
  3608.       for lfdid, lfrinfo in pairs(addon.LFRInstances) do
  3609.         if lfrinfo.parent == pinst.LFDID and lfrmap[lfdid] then
  3610.           firstid = math.min(lfdid, firstid or lfdid)
  3611.           total = total + lfrinfo.total
  3612.           lfrmap[boxname..":"..lfrinfo.base] = lfrmap[lfdid]
  3613.         end
  3614.       end
  3615.       tooltip:SetCell(line, 1, (instancesaved[boxid] and GOLDFONT or GRAYFONT) .. boxname .. FONTEND)
  3616.       tooltip:SetLineScript(line, "OnMouseDown", OpenLFR, firstid)
  3617.       for toon, t in cpairs(vars.db.Toons, true) do
  3618.         local saved = 0
  3619.         local diff = 2
  3620.         for key, instance in pairs(lfrmap) do
  3621.           if string.match(key,boxname..":%d+$") then
  3622.             saved = saved + addon:instanceBosses(instance, toon, diff)
  3623.           end
  3624.         end
  3625.         if saved > 0 then
  3626.           addColumns(columns, toon, tooltip)
  3627.           local col = columns[toon..1]
  3628.           tooltip:SetCell(line, col, DifficultyString(pinstance, diff, toon, false, saved, total),4)
  3629.           tooltip:SetCellScript(line, col, "OnEnter", ShowLFRTooltip, {boxname, toon, lfrmap})
  3630.           tooltip:SetCellScript(line, col, "OnLeave", CloseTooltips)
  3631.         end
  3632.       end
  3633.     end
  3634.   end
  3635.  
  3636.   -- combined world bosses
  3637.   if wbcons and next(worldbosses) and (wbalways or instancesaved[L["World Bosses"]]) then
  3638.     if not firstcategory and vars.db.Tooltip.CategorySpaces then
  3639.       addsep()
  3640.     end
  3641.     local line = tooltip:AddLine((instancesaved[L["World Bosses"]] and YELLOWFONT or GRAYFONT) .. L["World Bosses"] .. FONTEND)
  3642.     for toon, t in cpairs(vars.db.Toons, true) do
  3643.       local saved = 0
  3644.       local diff = 2
  3645.       for _, instance in ipairs(worldbosses) do
  3646.         local inst = vars.db.Instances[instance]
  3647.         if inst[toon] and inst[toon][diff] and inst[toon][diff].Expires > 0 then
  3648.           saved = saved + 1
  3649.         end
  3650.       end
  3651.       if saved > 0 then
  3652.         addColumns(columns, toon, tooltip)
  3653.         local col = columns[toon..1]
  3654.         tooltip:SetCell(line, col, DifficultyString(worldbosses[1], diff, toon, false, saved, #worldbosses),4)
  3655.         tooltip:SetCellScript(line, col, "OnEnter", ShowWorldBossTooltip, {worldbosses, toon, saved})
  3656.         tooltip:SetCellScript(line, col, "OnLeave", CloseTooltips)
  3657.       end
  3658.     end
  3659.   end
  3660.  
  3661.   local holidayinst = localarr("holidayinst")
  3662.   local firstlfd = true
  3663.   for instance, info in pairs(vars.db.Instances) do
  3664.     if showall or
  3665.       (info.Holiday and vars.db.Tooltip.ShowHoliday) or
  3666.       (info.Random and vars.db.Tooltip.ShowRandom) then
  3667.       for toon, t in cpairs(vars.db.Toons, true) do
  3668.         local d = info[toon] and info[toon][1]
  3669.         if d then
  3670.           addColumns(columns, toon, tooltip)
  3671.           local row = holidayinst[instance]
  3672.           if not row then
  3673.             if not firstcategory and vars.db.Tooltip.CategorySpaces and firstlfd then
  3674.               addsep()
  3675.               firstlfd = false
  3676.             end
  3677.             row = tooltip:AddLine(YELLOWFONT .. abbreviate(instance) .. FONTEND)
  3678.             holidayinst[instance] = row
  3679.           end
  3680.           local tstr = SecondsToTime(d.Expires - time(), false, false, 1)
  3681.           tooltip:SetCell(row, columns[toon..1], ClassColorise(t.Class,tstr), "CENTER",maxcol)
  3682.           if info.Scenario then
  3683.             tooltip:SetLineScript(row, "OnMouseDown", OpenLFS, info.LFDID)
  3684.           else
  3685.             tooltip:SetLineScript(row, "OnMouseDown", OpenLFD, info.LFDID)
  3686.           end
  3687.         end
  3688.       end
  3689.     end
  3690.   end
  3691.  
  3692.   -- random dungeon
  3693.   if vars.db.Tooltip.TrackLFG or showall then
  3694.     local cd1,cd2 = false,false
  3695.     for toon, t in cpairs(vars.db.Toons, true) do
  3696.       cd2 = cd2 or t.LFG2
  3697.       cd1 = cd1 or (t.LFG1 and (not t.LFG2 or showall))
  3698.       if t.LFG1 or t.LFG2 then
  3699.         addColumns(columns, toon, tooltip)
  3700.       end
  3701.     end
  3702.     local randomLine
  3703.     if cd1 or cd2 then
  3704.       if not firstcategory and vars.db.Tooltip.CategorySpaces and firstlfd then
  3705.         addsep()
  3706.         firstlfd = false
  3707.       end
  3708.       local cooldown = ITEM_COOLDOWN_TOTAL:gsub("%%s",""):gsub("%p","")
  3709.       cd1 = cd1 and tooltip:AddLine(YELLOWFONT .. LFG_TYPE_RANDOM_DUNGEON..cooldown .. FONTEND)
  3710.       cd2 = cd2 and tooltip:AddLine(YELLOWFONT .. GetSpellInfo(71041) .. FONTEND)
  3711.     end
  3712.     for toon, t in cpairs(vars.db.Toons, true) do
  3713.       local d1 = (t.LFG1 and t.LFG1 - time()) or -1
  3714.       local d2 = (t.LFG2 and t.LFG2 - time()) or -1
  3715.       if d1 > 0 and (d2 < 0 or showall) then
  3716.         local col = columns[toon..1]
  3717.         local tstr = SecondsToTime(d1, false, false, 1)
  3718.         tooltip:SetCell(cd1, col, ClassColorise(t.Class,tstr), "CENTER",maxcol)
  3719.         tooltip:SetCellScript(cd1, col, "OnEnter", ShowSpellIDTooltip, {toon,-1,tstr})
  3720.         tooltip:SetCellScript(cd1, col, "OnLeave", CloseTooltips)
  3721.       end
  3722.       if d2 > 0 then
  3723.         local col = columns[toon..1]
  3724.         local tstr = SecondsToTime(d2, false, false, 1)
  3725.         tooltip:SetCell(cd2, col, ClassColorise(t.Class,tstr), "CENTER",maxcol)
  3726.         tooltip:SetCellScript(cd2, col, "OnEnter", ShowSpellIDTooltip, {toon,71041,tstr})
  3727.         tooltip:SetCellScript(cd2, col, "OnLeave", CloseTooltips)
  3728.       end
  3729.     end
  3730.   end
  3731.   if vars.db.Tooltip.TrackDeserter or showall then
  3732.     local show = false
  3733.     for toon, t in cpairs(vars.db.Toons, true) do
  3734.       if t.pvpdesert then
  3735.         show = true
  3736.         addColumns(columns, toon, tooltip)
  3737.       end
  3738.     end
  3739.     if show then
  3740.       if not firstcategory and vars.db.Tooltip.CategorySpaces and firstlfd then
  3741.         addsep()
  3742.         firstlfd = false
  3743.       end
  3744.       show = tooltip:AddLine(YELLOWFONT .. DESERTER .. FONTEND)
  3745.     end
  3746.     for toon, t in cpairs(vars.db.Toons, true) do
  3747.       if t.pvpdesert and time() < t.pvpdesert then
  3748.         local col = columns[toon..1]
  3749.         local tstr = SecondsToTime(t.pvpdesert - time(), false, false, 1)
  3750.         tooltip:SetCell(show, col, ClassColorise(t.Class,tstr), "CENTER",maxcol)
  3751.         tooltip:SetCellScript(show, col, "OnEnter", ShowSpellIDTooltip, {toon,26013,tstr})
  3752.         tooltip:SetCellScript(show, col, "OnLeave", CloseTooltips)
  3753.       end
  3754.     end
  3755.   end
  3756.  
  3757.   do
  3758.     local showd, showw
  3759.     for toon, t in cpairs(vars.db.Toons, true) do
  3760.       local dc, wc = addon:QuestCount(toon)
  3761.       if dc > 0 and (vars.db.Tooltip.TrackDailyQuests or showall) then
  3762.         showd = true
  3763.         addColumns(columns, toon, tooltip)
  3764.       end
  3765.       if wc > 0 and (vars.db.Tooltip.TrackWeeklyQuests or showall) then
  3766.         showw = true
  3767.         addColumns(columns, toon, tooltip)
  3768.       end
  3769.     end
  3770.     local adc, awc = addon:QuestCount(nil)
  3771.     if adc > 0 and (vars.db.Tooltip.TrackDailyQuests or showall) then showd = true end
  3772.     if awc > 0 and (vars.db.Tooltip.TrackWeeklyQuests or showall) then showw = true end
  3773.     if not firstcategory and vars.db.Tooltip.CategorySpaces and (showd or showw) then
  3774.       addsep()
  3775.     end
  3776.     if showd then
  3777.       showd = tooltip:AddLine(YELLOWFONT .. L["Daily Quests"] .. (adc > 0 and " ("..adc..")" or "") .. FONTEND)
  3778.       if adc > 0 then
  3779.         tooltip:SetCellScript(showd, 1, "OnEnter", ShowQuestTooltip, {nil,adc,true})
  3780.         tooltip:SetCellScript(showd, 1, "OnLeave", CloseTooltips)
  3781.       end
  3782.     end
  3783.     if showw then
  3784.       showw = tooltip:AddLine(YELLOWFONT .. L["Weekly Quests"] .. (awc > 0 and " ("..awc..")" or "") .. FONTEND)
  3785.       if awc > 0 then
  3786.         tooltip:SetCellScript(showw, 1, "OnEnter", ShowQuestTooltip, {nil,awc,false})
  3787.         tooltip:SetCellScript(showw, 1, "OnLeave", CloseTooltips)
  3788.       end
  3789.     end
  3790.     for toon, t in cpairs(vars.db.Toons, true) do
  3791.       local dc, wc = addon:QuestCount(toon)
  3792.       local col = columns[toon..1]
  3793.       if showd and col and dc > 0 then
  3794.         tooltip:SetCell(showd, col, ClassColorise(t.Class,dc), "CENTER",maxcol)
  3795.         tooltip:SetCellScript(showd, col, "OnEnter", ShowQuestTooltip, {toon,dc,true})
  3796.         tooltip:SetCellScript(showd, col, "OnLeave", CloseTooltips)
  3797.       end
  3798.       if showw and col and wc > 0 then
  3799.         tooltip:SetCell(showw, col, ClassColorise(t.Class,wc), "CENTER",maxcol)
  3800.         tooltip:SetCellScript(showw, col, "OnEnter", ShowQuestTooltip, {toon,wc,false})
  3801.         tooltip:SetCellScript(showw, col, "OnLeave", CloseTooltips)
  3802.       end
  3803.     end
  3804.   end
  3805.  
  3806.   if vars.db.Tooltip.TrackSkills or showall then
  3807.     local show = false
  3808.     for toon, t in cpairs(vars.db.Toons, true) do
  3809.       if t.Skills and next(t.Skills) then
  3810.         show = true
  3811.         addColumns(columns, toon, tooltip)
  3812.       end
  3813.     end
  3814.     if show then
  3815.       if not firstcategory and vars.db.Tooltip.CategorySpaces then
  3816.         addsep()
  3817.       end
  3818.       show = tooltip:AddLine(YELLOWFONT .. L["Trade Skill Cooldowns"] .. FONTEND)
  3819.     end
  3820.     for toon, t in cpairs(vars.db.Toons, true) do
  3821.       local cnt = 0
  3822.       if t.Skills then
  3823.         for _ in pairs(t.Skills) do cnt = cnt + 1 end
  3824.       end
  3825.       if cnt > 0 then
  3826.         local col = columns[toon..1]
  3827.         tooltip:SetCell(show, col, ClassColorise(t.Class,cnt), "CENTER",maxcol)
  3828.         tooltip:SetCellScript(show, col, "OnEnter", ShowSkillTooltip, {toon, cnt})
  3829.         tooltip:SetCellScript(show, col, "OnLeave", CloseTooltips)
  3830.       end
  3831.     end
  3832.   end
  3833.  
  3834.   if vars.db.Tooltip.TrackFarm or showall then
  3835.     local toonfarm = localarr("toonfarm")
  3836.     local show
  3837.     for toon, t in cpairs(vars.db.Toons, true) do
  3838.       if (t.FarmPlanted or 0) > 0 or (t.FarmHarvested or 0) > 0 or
  3839.         (t.FarmCropReady and next(t.FarmCropReady)) then
  3840.         toonfarm[toon] = (t.FarmHarvested or 0).."/"..(t.FarmPlanted or 0)
  3841.         show = true
  3842.         addColumns(columns, toon, tooltip)
  3843.       end
  3844.     end
  3845.     if show then
  3846.       if not firstcategory and vars.db.Tooltip.CategorySpaces then
  3847.         addsep()
  3848.       end
  3849.       show = tooltip:AddLine(YELLOWFONT .. L["Farm Crops"] .. FONTEND)
  3850.     end
  3851.     for toon, t in cpairs(vars.db.Toons, true) do
  3852.       if toonfarm[toon] then
  3853.         local col = columns[toon..1]
  3854.         tooltip:SetCell(show, col, ClassColorise(t.Class,toonfarm[toon]), "CENTER",maxcol)
  3855.         tooltip:SetCellScript(show, col, "OnEnter", ShowFarmTooltip, toon)
  3856.         tooltip:SetCellScript(show, col, "OnLeave", CloseTooltips)
  3857.       end
  3858.     end
  3859.   end
  3860.  
  3861.   if vars.db.Tooltip.TrackBonus or showall then
  3862.     local show
  3863.     local toonbonus = localarr("toonbonus")
  3864.     for toon, t in cpairs(vars.db.Toons, true) do
  3865.       if t.BonusRoll and t.BonusRoll[1] then
  3866.         local gold = 0
  3867.         for _,roll in ipairs(t.BonusRoll) do
  3868.           if roll.money then
  3869.             gold = gold + 1
  3870.           else
  3871.             break
  3872.           end
  3873.         end
  3874.         toonbonus[toon] = gold
  3875.         show = true
  3876.         addColumns(columns, toon, tooltip)
  3877.       end
  3878.     end
  3879.     if show then
  3880.       if not firstcategory and vars.db.Tooltip.CategorySpaces then
  3881.         addsep()
  3882.       end
  3883.       show = tooltip:AddLine(YELLOWFONT .. L["Roll Bonus"] .. FONTEND)
  3884.     end
  3885.     for toon, t in cpairs(vars.db.Toons, true) do
  3886.       if toonbonus[toon] then
  3887.         local col = columns[toon..1]
  3888.         local str = toonbonus[toon]
  3889.         if str > 0 then str = "+"..str end
  3890.         tooltip:SetCell(show, col, ClassColorise(t.Class,str), "CENTER",maxcol)
  3891.         tooltip:SetCellScript(show, col, "OnEnter", ShowBonusTooltip, toon)
  3892.         tooltip:SetCellScript(show, col, "OnLeave", CloseTooltips)
  3893.       end
  3894.     end
  3895.   end
  3896.  
  3897.   local firstcurrency = true
  3898.   for _,idx in ipairs(currency) do
  3899.     local setting = vars.db.Tooltip["Currency"..idx]
  3900.     if setting or showall then
  3901.       local show
  3902.       for toon, t in cpairs(vars.db.Toons, true) do
  3903.         -- ci.name, ci.amount, ci.earnedThisWeek, ci.weeklyMax, ci.totalMax
  3904.         local ci = t.currency and t.currency[idx]
  3905.         local gotsome
  3906.         if ci then
  3907.           gotsome = ((ci.earnedThisWeek or 0) > 0 and (ci.weeklyMax or 0) > 0) or
  3908.             ((ci.amount or 0) > 0 and showall)
  3909.           -- or ((ci.amount or 0) > 0 and ci.weeklyMax == 0 and t.Level == maxlvl)
  3910.         end
  3911.         if ci and gotsome then
  3912.           addColumns(columns, toon, tooltip)
  3913.         end
  3914.         if ci and (gotsome or (ci.amount or 0) > 0) and columns[toon..1] then
  3915.           local name,_,tex = GetCurrencyInfo(idx)
  3916.           show = string.format(" \124T%s:0\124t%s",tex,name)
  3917.         end
  3918.       end
  3919.       local currLine
  3920.       if show then
  3921.         if not firstcategory and vars.db.Tooltip.CategorySpaces and firstcurrency then
  3922.           addsep()
  3923.           firstcurrency = false
  3924.         end
  3925.         currLine = tooltip:AddLine(YELLOWFONT .. show .. FONTEND)
  3926.         tooltip:SetLineScript(currLine, "OnMouseDown", OpenCurrency)
  3927.         tooltip:SetCellScript(currLine, 1, "OnEnter", ShowCurrencySummary, idx)
  3928.         tooltip:SetCellScript(currLine, 1, "OnLeave", CloseTooltips)
  3929.         tooltip:SetCellScript(currLine, 1, "OnMouseDown", OpenCurrency)
  3930.  
  3931.         for toon, t in cpairs(vars.db.Toons, true) do
  3932.           local ci = t.currency and t.currency[idx]
  3933.           local col = columns[toon..1]
  3934.           if ci and col then
  3935.             local earned, weeklymax, totalmax = "","",""
  3936.             if vars.db.Tooltip.CurrencyMax then
  3937.               if (ci.weeklyMax or 0) > 0 then
  3938.                 weeklymax = "/"..addon:formatNumber(ci.weeklyMax)
  3939.               end
  3940.               if (ci.totalMax or 0) > 0 then
  3941.                 totalmax = "/"..addon:formatNumber(ci.totalMax)
  3942.               end
  3943.             end
  3944.             if vars.db.Tooltip.CurrencyEarned or showall then
  3945.               earned = CurrencyColor(ci.amount,ci.totalMax)..totalmax
  3946.             end
  3947.             local str
  3948.             if (ci.amount or 0) > 0 or (ci.earnedThisWeek or 0) > 0 then
  3949.               if (ci.weeklyMax or 0) > 0 then
  3950.                 str = earned.." ("..CurrencyColor(ci.earnedThisWeek,ci.weeklyMax)..weeklymax..")"
  3951.               elseif (ci.amount or 0) > 0 then
  3952.                 str = CurrencyColor(ci.amount,ci.totalMax)..totalmax
  3953.               end
  3954.             end
  3955.             if str then
  3956.               if not vars.db.Tooltip.CurrencyValueColor then
  3957.                 str = ClassColorise(t.Class,str)
  3958.               end
  3959.               tooltip:SetCell(currLine, col, str, "CENTER",maxcol)
  3960.               tooltip:SetCellScript(currLine, col, "OnEnter", ShowCurrencyTooltip, {toon, idx, ci})
  3961.               tooltip:SetCellScript(currLine, col, "OnLeave", CloseTooltips)
  3962.               tooltip:SetCellScript(currLine, col, "OnMouseDown", OpenCurrency)
  3963.             end
  3964.           end
  3965.         end
  3966.       end
  3967.     end
  3968.   end
  3969.  
  3970.   -- toon names
  3971.   for toondiff, col in pairs(columns) do
  3972.     local toon = strsub(toondiff, 1, #toondiff-1)
  3973.     local diff = strsub(toondiff, #toondiff, #toondiff)
  3974.     if diff == "1" then
  3975.       local toonname, toonserver = toon:match('^(.*) [-] (.*)$')
  3976.       local toonstr = toonname
  3977.       if db.Tooltip.ShowServer then
  3978.         toonstr = toonstr .. "\n" .. toonserver
  3979.       end
  3980.       tooltip:SetCell(headLine, col, ClassColorise(vars.db.Toons[toon].Class, toonstr),
  3981.         tooltip:GetHeaderFont(), "CENTER", maxcol)
  3982.       tooltip:SetCellScript(headLine, col, "OnEnter", ShowToonTooltip, toon)
  3983.       tooltip:SetCellScript(headLine, col, "OnLeave", CloseTooltips)
  3984.       --[[
  3985.       tooltip:SetCellScript(headLine, col, "OnEnter", function()
  3986.         for i=0,3 do
  3987.           tooltip:SetColumnColor(col+i,0.5,0.5,0.5)
  3988.         end
  3989.       end)
  3990.       tooltip:SetCellScript(headLine, col, "OnLeave", function()
  3991.         for i=0,3 do
  3992.           tooltip:SetColumnColor(col,0,0,0)
  3993.         end
  3994.       end)
  3995.       --]]
  3996.     end
  3997.   end
  3998.   -- we now know enough to put in the category names where necessary
  3999.   if vars.db.Tooltip.ShowCategories then
  4000.     for category, row in pairs(categoryrow) do
  4001.       if (categories > 1 or vars.db.Tooltip.ShowSoloCategory) and categoryshown[category] then
  4002.         tooltip:SetCell(row, 1, YELLOWFONT .. vars.Categories[category] .. FONTEND, "LEFT", tooltip:GetColumnCount())
  4003.       end
  4004.     end
  4005.   end
  4006.  
  4007.   local hi = true
  4008.   for i=2,tooltip:GetLineCount() do -- row highlighting
  4009.     tooltip:SetLineScript(i, "OnEnter", DoNothing)
  4010.     tooltip:SetLineScript(i, "OnLeave", DoNothing)
  4011.  
  4012.     if hi and not blankrow[i] then
  4013.       tooltip:SetLineColor(i, 1,1,1, db.Tooltip.RowHighlight)
  4014.       hi = false
  4015.     else
  4016.       tooltip:SetLineColor(i, 0,0,0, 0)
  4017.       hi = true
  4018.     end
  4019.   end
  4020.  
  4021.   -- finishing up, with hints
  4022.   if TableLen(instancerow) == 0 then
  4023.     local noneLine = tooltip:AddLine()
  4024.     tooltip:SetCell(noneLine, 1, GRAYFONT .. NO_RAID_INSTANCES_SAVED .. FONTEND, "LEFT", tooltip:GetColumnCount())
  4025.   end
  4026.   if vars.db.Tooltip.ShowHints then
  4027.     tooltip:AddSeparator(8,0,0,0,0)
  4028.     local hintLine, hintCol
  4029.     if not addon:IsDetached() then
  4030.       hintLine, hintCol = tooltip:AddLine()
  4031.       tooltip:SetCell(hintLine, hintCol, L["|cffffff00Left-click|r to detach tooltip"], "LEFT", tooltip:GetColumnCount())
  4032.       hintLine, hintCol = tooltip:AddLine()
  4033.       tooltip:SetCell(hintLine, hintCol, L["|cffffff00Middle-click|r to show Blizzard's Raid Information"], "LEFT", tooltip:GetColumnCount())
  4034.       hintLine, hintCol = tooltip:AddLine()
  4035.       tooltip:SetCell(hintLine, hintCol, L["|cffffff00Right-click|r to configure SavedInstances"], "LEFT", tooltip:GetColumnCount())
  4036.     end
  4037.     hintLine, hintCol = tooltip:AddLine()
  4038.     tooltip:SetCell(hintLine, hintCol, L["Hover mouse on indicator for details"], "LEFT", tooltip:GetColumnCount())
  4039.     if not showall then
  4040.       hintLine, hintCol = tooltip:AddLine()
  4041.       tooltip:SetCell(hintLine, hintCol, L["Hold Alt to show all data"], "LEFT", math.max(1,tooltip:GetColumnCount()-maxcol))
  4042.       if tooltip:GetColumnCount() < maxcol+1 then
  4043.         tooltip:AddLine(addonName.." version "..addon.version)
  4044.       else
  4045.         tooltip:SetCell(hintLine, tooltip:GetColumnCount()-maxcol+1, addon.version, "RIGHT", maxcol)
  4046.       end
  4047.     end
  4048.   end
  4049.  
  4050.   -- cache check
  4051.   local fail = false
  4052.   local maxidx = 0
  4053.   for toon,val in cpairs(columnCache[showall]) do
  4054.     if not val then -- remove stale column
  4055.       columnCache[showall][toon] = nil
  4056.       fail = true
  4057.     else
  4058.       local thisidx = columns[toon..1]
  4059.       if thisidx < maxidx then -- sort failure caused by new middle-insertion
  4060.         fail = true
  4061.       end
  4062.       maxidx = thisidx
  4063.     end
  4064.   end
  4065.   if fail then -- retry with corrected cache
  4066.     debug("Tooltip cache miss")
  4067.     addon.scaleCache[showall] = nil
  4068.     --core:ShowTooltip(anchorframe)
  4069.     -- reschedule continuation to reduce time-slice exceeded errors in combat
  4070.     core:ScheduleTimer("ShowTooltip", 0, anchorframe)
  4071.   else -- render it
  4072.     addon:SkinFrame(tooltip,"SavedInstancesTooltip")
  4073.     if addon:IsDetached() then
  4074.       --[[
  4075.           tooltip.anchorframe = UIParent
  4076.           tooltip:SmartAnchorTo(UIParent)
  4077.     tooltip:SetAutoHideDelay(nil, UIParent)
  4078.     --]]
  4079.       --tooltip:UpdateScrolling(100000)
  4080.       tooltip:Show()
  4081.       QTip.layoutCleaner:CleanupLayouts()
  4082.       tooltip:ClearAllPoints()
  4083.       tooltip:SetPoint("BOTTOMLEFT",addon.detachframe)
  4084.       tooltip:SetFrameLevel(addon.detachframe:GetFrameLevel()+1)
  4085.     else
  4086.       tooltip:SmartAnchorTo(anchorframe)
  4087.       tooltip:SetAutoHideDelay(0.1, anchorframe)
  4088.       tooltip:Show()
  4089.     end
  4090.     tooltip.OnRelease = function() -- extra-safety: update our variable on auto-release
  4091.       tooltip:ClearAllPoints()
  4092.       tooltip = nil
  4093.     end
  4094.     if db.Tooltip.FitToScreen then
  4095.       -- scale check
  4096.       QTip.layoutCleaner:CleanupLayouts()
  4097.       local scale = tooltip:GetScale()
  4098.       local w,h = tooltip:GetSize()
  4099.       local sw,sh = UIParent:GetSize()
  4100.       w = w*scale;
  4101.       h = h*scale;
  4102.       if w > sw or h > sh then
  4103.         scale = scale / math.max(w/sw, h/sh)
  4104.         scale = scale*0.95 -- 5% slop to speed convergeance
  4105.         debug("Downscaling to %.4f",scale)
  4106.         tooltip:SetScale(scale)
  4107.         tooltip:Hide()
  4108.         addon.scaleCache[showall] = scale
  4109.         core:ScheduleTimer("ShowTooltip", 0, anchorframe) -- re-render fonts
  4110.       end
  4111.     end
  4112.   end
  4113.   starttime = debugprofilestop()-starttime
  4114.   debug("ShowTooltip(): completed in %.3fms", starttime)
  4115. end
  4116.  
  4117. local function ResetConfirmed()
  4118.   debug("Resetting characters")
  4119.   if addon:IsDetached() then
  4120.     addon:HideDetached()
  4121.   end
  4122.   -- clear saves
  4123.   for instance, i in pairs(vars.db.Instances) do
  4124.     for toon, t in pairs(vars.db.Toons) do
  4125.       i[toon] = nil
  4126.     end
  4127.   end
  4128.   wipe(vars.db.Toons) -- clear toon db
  4129.   addon.PlayedTime = nil -- reset played cache
  4130.   core:toonInit() -- rebuild thisToon
  4131.   core:Refresh()
  4132.   vars.config:BuildOptions() -- refresh config table
  4133.   vars.config:ReopenConfigDisplay(vars.config.ftoon)
  4134. end
  4135.  
  4136.  
  4137. StaticPopupDialogs["SAVEDINSTANCES_RESET"] = {
  4138.   preferredIndex = 3, -- reduce the chance of UI taint
  4139.   text = L["Are you sure you want to reset the SavedInstances character database? Characters will be re-populated as you log into them."],
  4140.   button1 = OKAY,
  4141.   button2 = CANCEL,
  4142.   OnAccept = ResetConfirmed,
  4143.   timeout = 0,
  4144.   whileDead = true,
  4145.   hideOnEscape = true,
  4146.   enterClicksFirstButton = false,
  4147.   showAlert = true,
  4148. }
  4149.  
  4150. local function DeleteCharacter(toon)
  4151.   if toon == thisToon or not vars.db.Toons[toon] then
  4152.     chatMsg("ERROR: Failed to delete "..toon..". Character is active or does not exist.")
  4153.     return
  4154.   end
  4155.   debug("Deleting character: "..toon)
  4156.   if addon:IsDetached() then
  4157.     addon:HideDetached()
  4158.   end
  4159.   -- clear saves
  4160.   for instance, i in pairs(vars.db.Instances) do
  4161.     i[toon] = nil
  4162.   end
  4163.   vars.db.Toons[toon] = nil
  4164.   vars.config:BuildOptions() -- refresh config table
  4165.   vars.config:ReopenConfigDisplay(vars.config.ftoon)
  4166. end
  4167.  
  4168. StaticPopupDialogs["SAVEDINSTANCES_DELETE_CHARACTER"] = {
  4169.   preferredIndex = 3, -- reduce the chance of UI taint
  4170.   text = string.format(L["Are you sure you want to remove %s from the SavedInstances character database?"],"\n\n%s%s\n\n").."\n\n"..
  4171.   L["This should only be used for characters who have been renamed or deleted, as characters will be re-populated when you log into them."],
  4172.   button1 = OKAY,
  4173.   button2 = CANCEL,
  4174.   OnAccept = function(self,data) DeleteCharacter(data) end,
  4175.   timeout = 0,
  4176.   whileDead = true,
  4177.   hideOnEscape = true,
  4178.   enterClicksFirstButton = false,
  4179.   showAlert = true,
  4180. }
  4181.  
  4182. local trade_spells = {
  4183.   -- Alchemy
  4184.   -- Vanilla
  4185.   [11479] = "xmute",  -- Transmute: Iron to Gold
  4186.   [11480] = "xmute",  -- Transmute: Mithril to Truesilver
  4187.   [17559] = "xmute",  -- Transmute: Air to Fire
  4188.   [17566] = "xmute",  -- Transmute: Earth to Life
  4189.   [17561] = "xmute",  -- Transmute: Earth to Water
  4190.   [17560] = "xmute",  -- Transmute: Fire to Earth
  4191.   [17565] = "xmute",  -- Transmute: Life to Earth
  4192.   [17563] = "xmute",  -- Transmute: Undeath to Water
  4193.   [17562] = "xmute",  -- Transmute: Water to Air
  4194.   [17564] = "xmute",  -- Transmute: Water to Undeath
  4195.   -- BC
  4196.   [28566] = "xmute",  -- Transmute: Primal Air to Fire
  4197.   [28585] = "xmute",  -- Transmute: Primal Earth to Life
  4198.   [28567] = "xmute",  -- Transmute: Primal Earth to Water
  4199.   [28568] = "xmute",  -- Transmute: Primal Fire to Earth
  4200.   [28583] = "xmute",  -- Transmute: Primal Fire to Mana
  4201.   [28584] = "xmute",  -- Transmute: Primal Life to Earth
  4202.   [28582] = "xmute",  -- Transmute: Primal Mana to Fire
  4203.   [28580] = "xmute",  -- Transmute: Primal Shadow to Water
  4204.   [28569] = "xmute",  -- Transmute: Primal Water to Air
  4205.   [28581] = "xmute",  -- Transmute: Primal Water to Shadow
  4206.   -- WotLK
  4207.   [60893] = 3,    -- Northrend Alchemy Research: 3 days
  4208.   [53777] = "xmute",  -- Transmute: Eternal Air to Earth
  4209.   [53776] = "xmute",  -- Transmute: Eternal Air to Water
  4210.   [53781] = "xmute",  -- Transmute: Eternal Earth to Air
  4211.   [53782] = "xmute",  -- Transmute: Eternal Earth to Shadow
  4212.   [53775] = "xmute",  -- Transmute: Eternal Fire to Life
  4213.   [53774] = "xmute",  -- Transmute: Eternal Fire to Water
  4214.   [53773] = "xmute",  -- Transmute: Eternal Life to Fire
  4215.   [53771] = "xmute",  -- Transmute: Eternal Life to Shadow
  4216.   [54020] = "xmute",  -- Transmute: Eternal Might
  4217.   [53779] = "xmute",  -- Transmute: Eternal Shadow to Earth
  4218.   [53780] = "xmute",  -- Transmute: Eternal Shadow to Life
  4219.   [53783] = "xmute",  -- Transmute: Eternal Water to Air
  4220.   [53784] = "xmute",  -- Transmute: Eternal Water to Fire
  4221.   [66658] = "xmute",  -- Transmute: Ametrine
  4222.   [66659] = "xmute",  -- Transmute: Cardinal Ruby
  4223.   [66660] = "xmute",  -- Transmute: King's Amber
  4224.   [66662] = "xmute",  -- Transmute: Dreadstone
  4225.   [66663] = "xmute",  -- Transmute: Majestic Zircon
  4226.   [66664] = "xmute",  -- Transmute: Eye of Zul
  4227.   -- Cata
  4228.   [78866] = "xmute",  -- Transmute: Living Elements
  4229.   --[80243] = "xmute",  -- Transmute: Truegold, cd removed (5.2.0 verified)
  4230.   [80244] = "xmute",  -- Transmute: Pyrium Bar
  4231.   -- MoP
  4232.   [114780] = "xmute",   -- Transmute: Living Steel
  4233.   -- WoD
  4234.   [175880] = true,  -- Secrets of Draenor
  4235.   [156587] = true,  -- Alchemical Catalyst (4)
  4236.   [168042] = true,  -- Alchemical Catalyst (10), 3 charges w/ 24hr recharge
  4237.   [181643] = "xmute", -- Transmute: Savage Blood
  4238.  
  4239.  
  4240.   -- Enchanting
  4241.   [28027] = "sphere",   -- Prismatic Sphere (2-day shared, 5.2.0 verified)
  4242.   [28028] = "sphere",   -- Void Sphere (2-day shared, 5.2.0 verified)
  4243.   [116499] = true,  -- Sha Crystal
  4244.   [177043] = true,  -- Secrets of Draenor
  4245.   [169092] = true,  -- Temporal Crystal
  4246.  
  4247.   -- Jewelcrafting
  4248.   [47280] = true,   -- Brilliant Glass, still has a cd (5.2.0 verified)
  4249.   --[62242] = true,   -- Icy Prism, cd removed (5.2.0 verified)
  4250.   [73478] = true,   -- Fire Prism, still has a cd (5.2.0 verified)
  4251.   [131691] = "facet",   -- Imperial Amethyst/Facets of Research
  4252.   [131686] = "facet",   -- Primordial Ruby/Facets of Research
  4253.   [131593] = "facet",   -- River's Heart/Facets of Research
  4254.   [131695] = "facet",   -- Sun's Radiance/Facets of Research
  4255.   [131690] = "facet",   -- Vermilion Onyx/Facets of Research
  4256.   [131688] = "facet",   -- Wild Jade/Facets of Research
  4257.   [140050] = true,  -- Serpent's Heart
  4258.   [176087] = true,  -- Secrets of Draenor
  4259.   [170700] = true,  -- Taladite Crystal
  4260.  
  4261.   -- Tailoring
  4262.   [143011] = true,  -- Celestial Cloth
  4263.   [125557] = true,  -- Imperial Silk
  4264.   [56005] = 7,    -- Glacial Bag (5.2.0 verified)
  4265.   [176058] = true,  -- Secrets of Draenor
  4266.   [168835] = true,  -- Hexweave Cloth
  4267.   -- Dreamcloth
  4268.   [75141] = 7,    -- Dream of Skywall
  4269.   [75145] = 7,    -- Dream of Ragnaros
  4270.   [75144] = 7,    -- Dream of Hyjal
  4271.   [75142] = 7,    -- Dream of Deepholm
  4272.   [75146] = 7,    -- Dream of Azshara
  4273.   --[18560] = true, -- Mooncloth, cd removed (5.2.0 verified, tooltip is wrong)
  4274.  
  4275.   -- Inscription
  4276.   [61288] = true,   -- Minor Inscription Research
  4277.   [61177] = true,   -- Northrend Inscription Research
  4278.   [86654] = true,   -- Horde Forged Documents
  4279.   [89244] = true,   -- Alliance Forged Documents
  4280.   [112996] = true,  -- Scroll of Wisdom
  4281.   [169081] = true,  -- War Paints
  4282.   [177045] = true,  -- Secrets of Draenor
  4283.   [176513] = true,  -- Draenor Merchant Order
  4284.  
  4285.   -- Blacksmithing
  4286.   [138646] = true,  -- Lightning Steel Ingot
  4287.   [143255] = true,  -- Balanced Trillium Ingot
  4288.   [171690] = true,  -- Truesteel Ingot
  4289.   [176090] = true,  -- Secrets of Draenor
  4290.  
  4291.   -- Leatherworking
  4292.   [140040] = "magni",   -- Magnificence of Leather
  4293.   [140041] = "magni", -- Magnificence of Scales
  4294.   [142976] = true,  -- Hardened Magnificent Hide
  4295.   [171391] = true,  -- Burnished Leather
  4296.   [176089] = true,  -- Secrets of Draenor
  4297.  
  4298.   -- Engineering
  4299.   [139176] = true,  -- Stabilized Lightning Source
  4300.   [169080] = true,  -- Gearspring Parts
  4301.   [177054] = true,  -- Secrets of Draenor
  4302.  
  4303.   [126459] = "item",  -- Blingtron 4000
  4304.   [161414] = "item",  -- Blingtron 5000
  4305.   [54710]  = "item",  -- MOLL-E
  4306.   [67826]  = "item",  -- Jeeves
  4307.  
  4308.   [67833] = "item", -- Wormhole Generator: Northrend
  4309.   [126755] = "item",  -- Wormhole Generator: Pandaria
  4310.   [163830] = "item",  -- Wormhole Centrifuge
  4311.   [23453] = "item",   -- Ultrasafe Transporter: Gadgetzhan
  4312.   [36941] = "item", -- Ultrasafe Transporter: Toshley's Station
  4313. }
  4314.  
  4315. local cdname = {
  4316.   ["xmute"] =  GetSpellInfo(2259).. ": "..L["Transmute"],
  4317.   ["facet"] =  GetSpellInfo(25229)..": "..L["Facets of Research"],
  4318.   ["sphere"] = GetSpellInfo(7411).. ": "..GetSpellInfo(28027),
  4319.   ["magni"] =  GetSpellInfo(2108).. ": "..GetSpellInfo(140040)
  4320. }
  4321.  
  4322. local itemcds = { -- [itemid] = spellid
  4323.   [87214] = 126459,   -- Blingtron 4000
  4324.   [111821] = 161414,  -- Blingtron 5000
  4325.   [40768] = 54710,  -- MOLL-E
  4326.   [49040] = 67826,  -- Jeeves
  4327.   [112059] = 163830,  -- Wormhole Centrifuge
  4328.   [48933] = 67833,  -- Wormhole Generator: Northrend
  4329.   [87215] = 126755, -- Wormhole Generator: Pandaria
  4330.   [18986] = 23453,  -- Ultrasafe Transporter: Gadgetzhan
  4331.   [30544] = 36941,  -- Ultrasafe Transporter: Toshley's Station
  4332. }
  4333.  
  4334. function core:scan_item_cds()
  4335.   for itemid, spellid in pairs(itemcds) do
  4336.     local start, duration = GetItemCooldown(itemid)
  4337.     if start and duration and start > 0 then
  4338.       core:record_skill(spellid, GetTimeToTime(start+duration))
  4339.     end
  4340.   end
  4341. end
  4342.  
  4343. function core:record_skill(spellID, expires)
  4344.   if not spellID then return end
  4345.   local cdinfo = trade_spells[spellID]
  4346.   if not cdinfo then
  4347.     addon.skillwarned = addon.skillwarned or {}
  4348.     if expires and expires > 0 and not addon.skillwarned[spellID] then
  4349.       addon.skillwarned[spellID] = true
  4350.       bugReport("Unrecognized trade skill cd "..(GetSpellInfo(spellID) or "??").." ("..spellID..")")
  4351.     end
  4352.     return
  4353.   end
  4354.   local t = vars and vars.db.Toons[thisToon]
  4355.   if not t then return end
  4356.   local spellName = GetSpellInfo(spellID)
  4357.   t.Skills = t.Skills or {}
  4358.   local idx = spellID
  4359.   local title = spellName
  4360.   local link = nil
  4361.   if cdinfo == "item" then
  4362.     if not expires then
  4363.       core:ScheduleTimer("scan_item_cds", 2) -- theres a delay for the item to go on cd
  4364.       return
  4365.     end
  4366.     for itemid, spellid in pairs(itemcds) do
  4367.       if spellid == spellID then
  4368.         title,link = GetItemInfo(itemid) -- use item name as some item spellnames are ambiguous or wrong
  4369.         title = title or spellName
  4370.       end
  4371.     end
  4372.   elseif type(cdinfo) == "string" then
  4373.     idx = cdinfo
  4374.     title = cdname[cdinfo] or title
  4375.   elseif expires ~= 0 then
  4376.     local slink = GetSpellLink(spellID)
  4377.     if slink and #slink > 0 then  -- tt scan for the full name with profession
  4378.       link = "\124cffffd000\124Henchant:"..spellID.."\124h[X]\124h\124r"
  4379.       scantt:SetOwner(UIParent,"ANCHOR_NONE")
  4380.       scantt:SetHyperlink(link)
  4381.       local l = _G[scantt:GetName().."TextLeft1"]
  4382.       l = l and l:GetText()
  4383.       if l and #l > 0 then
  4384.         title = l
  4385.         link = link:gsub("X",l)
  4386.       else
  4387.         link = nil
  4388.       end
  4389.     end
  4390.   end
  4391.   if expires == 0 then
  4392.     if t.Skills[idx] then -- a cd ended early
  4393.       debug("Clearing Trade skill cd: %s (%s)",spellName,spellID)
  4394.     end
  4395.     t.Skills[idx] = nil
  4396.     return
  4397.   elseif not expires then
  4398.     expires = addon:GetNextDailySkillResetTime()
  4399.     if not expires then return end -- ticket 127
  4400.     if type(cdinfo) == "number" then -- over a day, make a rough guess
  4401.       expires = expires + (cdinfo-1)*24*60*60
  4402.     end
  4403.   end
  4404.   expires = math.floor(expires)
  4405.   local sinfo = t.Skills[idx] or {}
  4406.   t.Skills[idx] = sinfo
  4407.   local change = expires - (sinfo.Expires or 0)
  4408.   if math.abs(change) > 180 and addon.db.dbg then -- updating expiration guess (more than 3 min update lag)
  4409.     debug("Trade skill cd: "..(link or title).." ("..spellID..") "..
  4410.       (sinfo.Expires and string.format("%d",change).." sec" or "(new)")..
  4411.       " Local time: "..date("%c",expires))
  4412.   end
  4413.   sinfo.Title = title
  4414.   sinfo.Link = link
  4415.   sinfo.Expires = expires
  4416.   return true
  4417. end
  4418.  
  4419. function core:TradeSkillRescan(spellid)
  4420.   local scan = core:TRADE_SKILL_LIST_UPDATE()
  4421.   if TradeSkillFrame and TradeSkillFrame.filterTbl and
  4422.     (scan == 0 or not addon.seencds or not addon.seencds[spellid]) then
  4423.     -- scan failed, probably because the skill is hidden - try again
  4424.     addon.filtertmp = wipe(addon.filtertmp or {})
  4425.     for k,v in pairs(TradeSkillFrame.filterTbl) do addon.filtertmp[k] = v end
  4426.     TradeSkillOnlyShowMakeable(false)
  4427.     TradeSkillOnlyShowSkillUps(false)
  4428.     SetTradeSkillCategoryFilter(-1)
  4429.     SetTradeSkillInvSlotFilter(-1, 1, 1)
  4430.     ExpandTradeSkillSubClass(0)
  4431.     local rescan = core:TRADE_SKILL_LIST_UPDATE()
  4432.     debug("Rescan: "..(rescan==scan and "Failed" or "Success"))
  4433.     TradeSkillOnlyShowMakeable(addon.filtertmp.hasMaterials);
  4434.     TradeSkillOnlyShowSkillUps(addon.filtertmp.hasSkillUp);
  4435.     SetTradeSkillCategoryFilter(addon.filtertmp.subClassValue or -1)
  4436.     SetTradeSkillInvSlotFilter(addon.filtertmp.slotValue or -1, 1, 1)
  4437.   end
  4438. end
  4439.  
  4440. function core:TRADE_SKILL_LIST_UPDATE()
  4441.   local cnt = 0
  4442.   if C_TradeSkillUI.IsTradeSkillLinked() or C_TradeSkillUI.IsTradeSkillGuild() then return end
  4443.   local recipeids = C_TradeSkillUI.GetFilteredRecipeIDs()
  4444.   for _, spellid in ipairs(recipeids) do
  4445.     local cd, daily = C_TradeSkillUI.GetRecipeCooldown(spellid)
  4446.     if cd and daily -- GetTradeSkillCooldown often returns WRONG answers for daily cds
  4447.       and not tonumber(trade_spells[spellid]) then -- daily flag incorrectly set for some multi-day cds (Northrend Alchemy Research)
  4448.       cd = addon:GetNextDailySkillResetTime()
  4449.     elseif cd then
  4450.       cd = time() + cd  -- on cd
  4451.     else
  4452.       cd = 0 -- off cd or no cd
  4453.     end
  4454.     core:record_skill(spellid, cd)
  4455.     if cd then
  4456.       addon.seencds = addon.seencds or {}
  4457.       addon.seencds[spellid] = true
  4458.       cnt = cnt + 1
  4459.     end
  4460.   end
  4461.   return cnt
  4462. end
  4463.  
  4464. local farm_spells = {
  4465.  
  4466.     [111102]="plant", -- Plant Green Cabbage
  4467.     [123361]="plant", -- Plant Juicycrunch Carrot
  4468.     [123388]="plant", -- Plant Scallions
  4469.     [123485]="plant", -- Plant Mogu Pumpkin
  4470.     [123535]="plant", -- Plant Red Blossom Leek
  4471.     [123565]="plant", -- Plant Pink Turnip
  4472.     [123568]="plant", -- Plant White Turnip
  4473.     [123771]="plant", -- Plant Golden Seed
  4474.     [123772]="plant", -- Plant Seed of Harmony
  4475.     [123773]="plant", -- Plant Snakeroot Seed
  4476.     [123774]="plant", -- Plant Enigma Seed
  4477.     [123775]="plant", -- Plant Magebulb Seed
  4478.     [123776]="plant", -- Plant Soybean Seed
  4479.     [123777]="plant", -- Plant Ominous Seed
  4480.     [123892]="plant", -- Plant Autumn Blossom Sapling
  4481.     [123893]="plant", -- Plant Spring Blossom Seed
  4482.     [123894]="plant", -- Plant Winter Blossom Sapling
  4483.     [123895]="plant", -- Plant Kyparite Seed
  4484.     [129623]="plant", -- Plant Windshear Cactus Seed
  4485.     [129628]="plant", -- Plant Raptorleaf Seed
  4486.     [129863]="plant", -- Plant Songbell Seed
  4487.     [129974]="plant", -- Plant Witchberries
  4488.     [129976]="plant", -- Plant Jade Squash
  4489.     [129978]="plant", -- Plant Striped Melon
  4490.     [130170]="plant", -- Plant Spring Blossom Sapling
  4491.     [133036]="plant", -- Plant Unstable Portal Shard
  4492.  
  4493.     [116356]="throw", -- Throw Green Cabbage Seeds
  4494.     [123362]="throw", -- Throw Juicycrunch Carrot Seeds
  4495.     [123389]="throw", -- Throw Scallion Seeds
  4496.     [123486]="throw", -- Throw Mogu Pumpkin Seeds
  4497.     [123537]="throw", -- Throw Red Blossom Leek Seeds
  4498.     [123566]="throw", -- Throw Pink Turnip Seeds
  4499.     [123567]="throw", -- Throw White Turnip Seeds
  4500.     [131093]="throw", -- Throw Witchberry Seeds
  4501.     [131094]="throw", -- Throw Jade Squash Seeds
  4502.     [131095]="throw", -- Throw Striped Melon Seeds
  4503.     [139975]="throw", -- Throw Songbell Seeds
  4504.     [139977]="throw", -- Throw Snakeroot Seeds
  4505.     [139978]="throw", -- Throw Enigma Seeds
  4506.     [139981]="throw", -- Throw Magebulb Seeds
  4507.     [139983]="throw", -- Throw Windshear Cactus Seeds
  4508.     [139986]="throw", -- Throw Raptorleaf Seeds
  4509.  
  4510.     [111123]="harvest", -- Harvest Green Cabbage
  4511.     [115063]="harvest", -- Harvest EZ-Gro Green Cabbage
  4512.     [123353]="harvest", -- Harvest Juicycrunch Carrot
  4513.     [123355]="harvest", -- Harvest Plump Green Cabbage
  4514.     [123356]="harvest", -- Harvest Plump Juicycrunch Carrot
  4515.     [123375]="harvest", -- Harvest Scallions
  4516.     [123380]="harvest", -- Harvest Plump Scallions
  4517.     [123445]="harvest", -- Harvest Mogu Pumpkin
  4518.     [123451]="harvest", -- Harvest Plump Mogu Pumpkin
  4519.     [123516]="harvest", -- Harvest Winter Blossom Tree
  4520.     [123522]="harvest", -- Harvest Plump Red Blossom Leek
  4521.     [123524]="harvest", -- Harvest Red Blossom Leek
  4522.     [123548]="harvest", -- Harvest Pink Turnip
  4523.     [123549]="harvest", -- Harvest Plump Pink Turnip
  4524.     [123570]="harvest", -- Harvest White Turnip
  4525.     [123571]="harvest", -- Harvest Plump White Turnip
  4526.     [129673]="harvest", -- Harvest Golden Lotus
  4527.     [129674]="harvest", -- Harvest Fool\'s Cap
  4528.     [129675]="harvest", -- Harvest Snow Lily
  4529.     [129676]="harvest", -- Harvest Silkweed
  4530.     [129687]="harvest", -- Harvest Green Tea Leaf
  4531.     [129705]="harvest", -- Harvest Rain Poppy
  4532.     [129757]="harvest", -- Harvest Snakeroot
  4533.     [129796]="harvest", -- Harvest Magebulb
  4534.     [129814]="harvest", -- Harvest Windshear Cactus
  4535.     [129843]="harvest", -- Harvest Raptorleaf
  4536.     [129887]="harvest", -- Harvest Songbell
  4537.     [129983]="harvest", -- Harvest Witchberries
  4538.     [129984]="harvest", -- Harvest Plump Witchberries
  4539.     [130025]="harvest", -- Harvest Jade Squash
  4540.     [130026]="harvest", -- Harvest Plump Jade Squash
  4541.     [130042]="harvest", -- Harvest Striped Melon
  4542.     [130043]="harvest", -- Harvest Plump Striped Melon
  4543.     [130109]="harvest", -- Harvest Terrible Turnip
  4544.     [130140]="harvest", -- Harvest Autumn Blossom Tree
  4545.     [130168]="harvest", -- Harvest Spring Blossom Tree
  4546.     [133106]="harvest", -- Harvest Portal Shard
  4547.  
  4548. }
  4549.  
  4550. function core:record_farm(spellID)
  4551.   local ft = farm_spells[spellID]
  4552.   if not ft then return end
  4553.   local t = vars and vars.db.Toons[thisToon]
  4554.   if not t then return end
  4555.   if ft == "plant" or ft == "throw" then
  4556.     local amt = (ft == "plant" and 1 or 4)
  4557.     t.FarmPlanted = (t.FarmPlanted or 0) + amt
  4558.     t.FarmCropPlanted = t.FarmCropPlanted or {}
  4559.     t.FarmCropPlanted[spellID] = (t.FarmCropPlanted[spellID] or 0) + amt
  4560.   elseif ft == "harvest" then
  4561.     if t.FarmExpires and time() + 60 > t.FarmExpires then -- assume this is a fresh day
  4562.       t.FarmExpires = t.FarmExpires - 60
  4563.       addon:UpdateToonData() -- ticket 132: ensure refresh if we're harvesting right after reset
  4564.     end
  4565.     t.FarmHarvested = (t.FarmHarvested or 0) + 1
  4566.     t.FarmCropReady = nil
  4567.   end
  4568.   t.FarmExpires = addon:GetNextDailySkillResetTime()
  4569.   debug("Farm "..ft..": planted="..(t.FarmPlanted or 0)..
  4570.     " harvested="..(t.FarmHarvested or 0).." expires="..date("%c",t.FarmExpires or 0))
  4571. end
  4572.  
  4573. function core:UNIT_SPELLCAST_SUCCEEDED(evt, unit, spellName, rank, lineID, spellID)
  4574.   if unit ~= "player" then return end
  4575.   if trade_spells[spellID] then
  4576.     debug("UNIT_SPELLCAST_SUCCEEDED: %s (%s)",GetSpellLink(spellID),spellID)
  4577.     if not core:record_skill(spellID) then return end
  4578.     core:ScheduleTimer("TradeSkillRescan", 0.5, spellID)
  4579.   elseif farm_spells[spellID] then
  4580.     debug("UNIT_SPELLCAST_SUCCEEDED: %s (%s)",GetSpellLink(spellID),spellID)
  4581.     core:record_farm(spellID)
  4582.   end
  4583. end
  4584.  
  4585. function core:BonusRollResult(event, rewardType, rewardLink, rewardQuantity, rewardSpecID)
  4586.   local t = vars.db.Toons[thisToon]
  4587.   debug("BonusRollResult:%s:%s:%s:%s (boss=%s|%s)",
  4588.     tostring(rewardType), tostring(rewardLink), tostring(rewardQuantity), tostring(rewardSpecID),
  4589.     tostring(t and t.lastboss), tostring(t and t.lastbossyell))
  4590.   if not t then return end
  4591.   if not rewardType then return end -- sometimes get a bogus message, ignore it
  4592.   t.BonusRoll = t.BonusRoll or {}
  4593.   --local rewardstr = _G["BONUS_ROLL_REWARD_"..string.upper(rewardType)]
  4594.   local now = time()
  4595.   local bossname = t.lastboss
  4596.   if now > (t.lastbosstime or 0) + 3*60 then -- user rolled before lastboss was updated, ignore the stale one. Roll timeout is 3 min.
  4597.     bossname = nil
  4598.   end
  4599.   if not bossname and t.lastbossyell and now < (t.lastbossyelltime or 0) + 10*60 then
  4600.     bossname = t.lastbossyell -- yell fallback
  4601.   end
  4602.   if not bossname then
  4603.     bossname = GetSubZoneText() or GetRealZoneText() -- zone fallback
  4604.   end
  4605.   local roll = { name = bossname, time = now, currencyID = BonusRollFrame.currencyID }
  4606.   if rewardType == "money" then
  4607.     roll.money = rewardQuantity
  4608.   elseif rewardType == "item" then
  4609.     roll.item = rewardLink
  4610.   end
  4611.   table.insert(t.BonusRoll, 1, roll)
  4612.   local limit = 25
  4613.   for i=limit+1, table.maxn(t.BonusRoll) do
  4614.     t.BonusRoll[i] = nil
  4615.   end
  4616. end
  4617.  
  4618. function addon.BonusRollShow()
  4619.   local t = vars.db.Toons[thisToon]
  4620.   if not t or not BonusRollFrame then return end
  4621.   local binfo = t.BonusRoll
  4622.   local frame = addon.BonusFrame
  4623.   if not binfo or #binfo == 0 or not vars.db.Tooltip.AugmentBonus then
  4624.     if frame then frame:Hide() end
  4625.     return
  4626.   end
  4627.   if not frame then
  4628.     frame = CreateFrame("Button", "SavedInstancesBonusRollFrame", BonusRollFrame, "SpellBookSkillLineTabTemplate")
  4629.     addon.BonusFrame = frame
  4630.     --frame:SetSize(BonusRollFrame:GetHeight(), BonusRollFrame:GetHeight())
  4631.     frame:SetPoint("LEFT", BonusRollFrame, "RIGHT",0,8)
  4632.     frame.text = addon.BonusFrame:CreateFontString(nil, "OVERLAY","GameFontNormal")
  4633.     frame.text:SetPoint("CENTER")
  4634.     frame:SetScript("OnEnter", function() ShowBonusTooltip(nil, { thisToon, frame }) end )
  4635.     frame:SetScript("OnLeave", CloseTooltips)
  4636.     frame:SetScript("OnClick", nil)
  4637.     frame.text:Show()
  4638.   end
  4639.   local bonus = 0
  4640.   for _,rinfo in ipairs(binfo) do
  4641.     if rinfo.money then
  4642.       bonus = bonus + 1
  4643.     else
  4644.       break
  4645.     end
  4646.   end
  4647.   frame.text:SetText((bonus > 0 and "+" or "")..bonus)
  4648.   frame:Show()
  4649. end
  4650.  
  4651. hooksecurefunc("BonusRollFrame_StartBonusRoll", addon.BonusRollShow)
Advertisement
Add Comment
Please, Sign In to add comment