view WorldMap.lua @ 109:caa482329919

POI optimization
author Nenue
date Mon, 10 Jul 2017 18:34:11 -0400
parents WorldQuests.lua@b67ba1078824
children f6ef9a9f5476
line wrap: on
line source
-- WorldPlan
-- WorldMap.lua
-- Created: 11/2/2016 3:40 PM
-- %file-revision%

local print = DEVIAN_WORKSPACE and function(...) _G.print('WorldQuests', ...) end or nop
local rprint = DEVIAN_WORKSPACE and function(...) _G.print('WQRefresh', ...) end or nop
local qprint = DEVIAN_WORKSPACE and function(...) _G.print('POI', ...) end or nop
local wprint = DEVIAN_WORKSPACE and function(...) _G.print('WP', ...) end or nop
local mprint = DEVIAN_WORKSPACE and function(...) _G.print('Canvas', ...) end or nop
local _, db = ...
local Module = {
  UsedPositions = {},
}
WorldPlanMapMixin = Module

local _G = _G
local type, tostring, tonumber, pairs, ipairs = type, tostring, tonumber, pairs, ipairs
local MC_GetNumZones, MC_GetZoneInfo = C_MapCanvas.GetNumZones, C_MapCanvas.GetZoneInfo
local TQ_GetQuestsForPlayerByMapID = C_TaskQuest.GetQuestsForPlayerByMapID -- This function is not yet documented
local TQ_GetQuestZoneID = C_TaskQuest.GetQuestZoneID
local TQ_IsActive = C_TaskQuest.IsActive
local TQ_RequestPreloadRewardData = C_TaskQuest.RequestPreloadRewardData
local pairs, ipairs, tinsert, tremove, wipe = pairs, ipairs, tinsert, tremove, table.wipe
local GetTaskInfo, GetTasksTable, HaveQuestData = GetTaskInfo, GetTasksTable, HaveQuestData
local GetTime = GetTime
local SpellCanTargetQuest, IsQuestIDValidSpellTarget = SpellCanTargetQuest, IsQuestIDValidSpellTarget
local tonumber, abs = tonumber, math.abs
local GetQuestLogRewardInfo = GetQuestLogRewardInfo
local GetCurrentMapAreaID, GetMapInfo, GetMapNameByID = GetCurrentMapAreaID, GetMapInfo, GetMapNameByID
local GetQuestBountyInfoForMapID, GetQuestLogTitle, GetQuestLogIndexByID, IsQuestComplete = GetQuestBountyInfoForMapID, GetQuestLogTitle, GetQuestLogIndexByID, IsQuestComplete
local HaveQuestRewardData = HaveQuestRewardData
local TQ_GetQuestLocation = C_TaskQuest.GetQuestLocation
local InCombatLockdown, hooksecurefunc = InCombatLockdown, hooksecurefunc

local ToggleButton = {}
local BROKEN_ISLES_ID, DALARAN_ID, AZSUNA_ID, VALSHARAH_ID, HIGHMOUNTAIN_ID, STORMHEIM_ID, SURAMAR_ID, EOA_ID = 1007, 1014, 1015,1018, 1024, 1017, 1033, 1096
local WORLD_QUEST_MAPS = { [DALARAN_ID] = 'Dalaran70',  [AZSUNA_ID] = 'Azsuna',  [VALSHARAH_ID] = "Val'sharah",
  [HIGHMOUNTAIN_ID] = 'Highmountain', [STORMHEIM_ID] = 'Stormheim',  [SURAMAR_ID] = 'Suramar',  [EOA_ID] = 'EyeOfAszhara', }

local REWARD_CASH = WORLD_QUEST_REWARD_TYPE_FLAG_GOLD
local REWARD_ARTIFACT_POWER = WORLD_QUEST_REWARD_TYPE_FLAG_ARTIFACT_POWER
local REWARD_GEAR = WORLD_QUEST_REWARD_TYPE_FLAG_EQUIPMENT
local REWARD_CURRENCY = WORLD_QUEST_REWARD_TYPE_FLAG_ORDER_RESOURCES
local REWARD_REAGENT = WORLD_QUEST_REWARD_TYPE_FLAG_MATERIALS
local SCALE_FACTORS = { 0.25, 0.7, 1 }

local BountyBoard = WorldMapFrame.UIElementsFrame.BountyBoard
local ActionButton = WorldMapFrame.UIElementsFrame.ActionButton
local defaults = {}
local completedQuests = {}

local continentScanned
local layoutDirty = true
local bountiesDirty = true
local artifactPowerDirty = true
local hooksDirty = true
local currentScale = WorldMapDetailFrame:GetScale()
local canTargetQuests
local isDataLoaded = true
local artifactKnowledgeLevel
local superTrackedQuestID
local lastRefresh
local refreshReason

local bountyQuests = {}
local bountyInfo = {}
local bountyDisplayLocation, bountyLockedQuestID, selectedBountyIndex, selectedBountyQuestID

local totalPins = 0
local numShown = 0
local numLoaded = 0
local numOverlays = 1
local scaleConstant = 1
local pinBaseIndex = 1550
local overlayBaseIndex = 1600

local artifactKnowldegeSpells = {
  [207856] = true,
  [209203] = true,
  [209204] = true,
  [209205] = true,
  [209206] = true,
  [209207] = true,
  [209208] = true,
  [209209] = true,
  [209210] = true,
  [209211] = true,
  [209212] = true,
  [219978] = true,
  [227852] = true,
  [236477] = true,
  [236489] = true,
  [236302] = true,
  [236488] = true,
  [236490] = true,
  [240475] = true,
  [243176] = true,
  [243177] = true,
  [243178] = true,
  [243182] = true,
  [243183] = true,
  [243187] = true,
  [245133] = true,
}

--%debug%
local SetTimedCallbackForAllPins = function(seconds, callback)
  C_Timer.After(seconds, function()
    for id, pin in pairs(WorldPlanQuests.QuestsByID) do
      callback(pin)
    end
  end)
end

function Module:OnLoad()
  --print('|cFFFF4400'..self:GetName()..':OnLoad()')

  self:SetParent(WorldMapFrame.UIElementsFrame)
  WorldPlan:AddHandler(self, defaults)

  for areaID, fileName in pairs(WORLD_QUEST_MAPS) do
    db.QuestsByZone[areaID] = {}
  end

  -- WORLD_MAP_UPDATE and PLAYER_ENTERING_WORLD are passed down from a higher level
  self:RegisterEvent('WORLD_QUEST_COMPLETED_BY_SPELL')
  self:RegisterEvent('SUPER_TRACKED_QUEST_CHANGED')
  self:RegisterEvent('SKILL_LINES_CHANGED')
  self:RegisterEvent('ARTIFACT_UPDATE')
  self:RegisterEvent('QUEST_LOG_UPDATE')
  self:RegisterEvent('UNIT_SPELLCAST_STOP')
end

function Module:OnEvent (event, ...)
  print('|cFFFFFF00OnEvent() '..event..'|r', GetTime(), ...)
  if (event == 'QUEST_LOG_UPDATE') then
    self:UpdateBounties(event)
  elseif event == 'WORLD_QUEST_COMPLETED_BY_SPELL' then
    local questID = ...
    if questID and db.QuestsByID[questID] then
      completedQuests[questID] = true
      db.QuestsByID[questID]:Release()
    end
    self:Refresh(event)
  elseif event == 'SKILL_LINES_CHANGED' or event == 'CURRENT_SPELL_CAST_CHANGED' then
    self:Refresh(event)
  elseif event == 'ARTIFACT_UPDATE' then
    self:UpdateArtifactPower()
  elseif event == 'MODIFIER_STATE_CHANGED' then
    self:UpdateModifierState()
  elseif event == 'SUPER_TRACKED_QUEST_CHANGED' then
    if superTrackedQuestID and db.QuestsByID[superTrackedQuestID] then
      db.QuestsByID[superTrackedQuestID].isStale = true
    end
    local newID = GetSuperTrackedQuestID()
    if newID and db.QuestsByID[newID] then
      db.QuestsByID[newID].isStale = true
    end
  elseif event == 'UNIT_SPELLCAST_STOP' then
    local name, _, _, _, spellID = ...
    if artifactKnowldegeSpells[spellID] then
      db.log('AK spellcast ended ' .. tostring(name) .. ' ('.. tostring(spellID)..')')
      self:UpdateArtifactPower()
    end
  end
end

function Module:OnUpdate(sinceLast)
  if WorldPlanData.DebugEnabled then
    if self.refreshBenchMarkTicker then
      --print(self.refreshBenchMarkTicker)
      self.refreshBenchMarkTicker =   self.refreshBenchMarkTicker - 1

      if self.refreshBenchMarkTicker == 0 then

        self.refreshTime = floor((GetTime() - self.refreshBenchMark) * 1000)
        self.debugMessage:SetText(self.refreshTime)
        self.refreshBenchMarkTicker = nil
      end
    else
      self.refreshBenchMark = GetTime()
    end
  end

  if self.filtersDirty or self.isStale then
    self:Refresh()
  end

  if #db.UpdatedPins >= 1 then
    --print('|cFF00FF88pending update', #db.UpdatedPins)
    self:UpdateNext()
  end
end

local callbacks = {}
callbacks.ClickWorldMapActionButton = function(WorldQuests)
  WorldQuests:Refresh('CLICK_MAP_ACTION_BUTTON')
end
callbacks.WorldMap_UpdateQuestBonusObjectives = function(WorldQuests)
  WorldQuests:UpdateTaskPOIs()
end
callbacks.WorldMapFrame_UpdateMap = function(WorldQuests)
  WorldQuests:RefreshIfChanged('WMF_UPDATE')
end
callbacks.WorldMapScrollFrame_ReanchorQuestPOIs = function (WorldQuests)
  WorldQuests:RefreshIfChanged('WMF_REANCHOR')
end

callbacks[BountyBoard] = {}
callbacks[BountyBoard].SetSelectedBountyIndex = function(WorldQuests)
  WorldQuests:UpdateBounties('BOUNTY_SELECTED')
  for questID, pin in pairs(db.QuestsByID) do
    pin.checkCriteria = true
    pin:Refresh()
  end
end

callbacks[ActionButton] = {}
callbacks[ActionButton].UpdateCastingState = function(WorldQuests)
  for questID, pin in pairs(db.QuestsByID) do
    pin.checkCursor = true
    pin:Refresh()
  end
end

callbacks.UseWorldMapActionButtonSpellOnQuest = function(questID)
  local pin = db.QuestsByID[questID]
  -- calling this implies that the pin is used in some way
  if pin then
    db.log(pin.title ..  ' completed by spell?', IsQuestComplete(pin.questID))
    pin:OnFilters()
    pin.isStale = true
  end
end

function Module:SetupCallbacks()
  if InCombatLockdown() then
    return true
  end
  print('SetupCallbacks()')
  for target, arg in pairs(callbacks) do
    --print(type(target))
    if type(target) == 'table' then
      local callerName = target:GetName() or tostring(target)
      for name, method  in pairs(arg) do
        --print(callerName, arg)
        hooksecurefunc(target, name, function(...)
          self:OnSecureHook(callerName .. '.' .. name, method, ...)
        end)
      end
    else
      hooksecurefunc(target, function(...)
        self:OnSecureHook(target, arg, ...)
      end)
    end
  end
end

function Module:Setup()
  --print('|cFFFF4400'..self:GetName()..':Setup()')
  for mapID, mapName in pairs(WORLD_QUEST_MAPS) do
    db.QuestsByZone[mapID] = {}
  end

  hooksDirty = self:SetupCallbacks()

  self:SetAllPoints(WorldMapFrame.UIElementsFrame)
  self:UpdateArtifactPower()
  self:UpdateBounties('SETUP')
  self:Show()
end

function Module:OnMapInfo(isBrokenIsle, isZoomedOut, mapAreaID, isNewMap, isMapOpen)
  if isNewMap or self.isStale then
    print('|cFF0088FFOnMapInfo()|r, mapAreaID =', mapAreaID,'visible =', isMapOpen, 'changed =', isNewMap)
    layoutDirty = true
    self:Refresh('WORLD_MAP_CHANGED')
  end
end

function Module:OnConfigUpdate()
  --print('|cFFFFFF00OnConfigUpdate()|r')
  if db.Config.FadeWhileGrouped then
    db.PinAlpha = 0.15
  else
    db.PinAlpha = 1
  end

  if not db.Config.EnablePins then
    for _, pin in pairs(db.QuestsByID) do
      pin:SetShown(false)
    end
  end
end

function Module:OnSecureHook(callbackName, func, ...)
  print('|cFFFF4400'..callbackName..'|r', ...)
  func(self, ...)
end

function Module:UpdateModifierState()

end

function Module:UpdateTaskPOIs()
  canTargetQuests = SpellCanTargetQuest()
  for i = 1, NUM_WORLDMAP_TASK_POIS do
    local poiFrame = _G['WorldMapFrameTaskPOI'..i]
    if poiFrame and poiFrame.worldQuest then
      local pin = db.QuestsByID[poiFrame.questID]
      if pin and pin.used and canTargetQuests and IsQuestIDValidSpellTarget(pin.questID) then
        poiFrame:Show()
      else
        poiFrame:Hide()
      end
    end
  end
end
-- re-anchors and scales pins that have had either of these changed due to data loading delays
function Module:UpdateNext()
  --print('|cFF00FF88UpdateNext()')
  local pin = tremove(db.UpdatedPins)
  pin:OnFilters()

  local scaleFactor = SCALE_FACTORS[(pin.dataLoaded and not pin.filtered) and scaleConstant or 1]
  --print(pin.title, pin.dataLoaded  and not pin.filtered, scaleFactor)
  if pin.used then
    pin:SetShown(true)
    pin:SetAnchor(nil, pin.x, pin.y, self.hostWidth, self.hostHeight, scaleFactor)
    pin:Refresh()
  else
    print('|cFFFF4400flagging queued pin that got hidden:', pin.title)
    pin.isStale = true
  end
end

function Module:UpdateBounties(...)
  bountiesDirty = nil
  print('|cFF00FF88BountyInfo()|r', ...)
  wipe(db.BountiesByFactionID)
  wipe(db.BountiesByQuestID)

  db.selectedBounty = nil
  selectedBountyIndex = BountyBoard:GetSelectedBountyIndex()
  db.Bounties, bountyDisplayLocation, bountyLockedQuestID = GetQuestBountyInfoForMapID(db.currentMapID, db.Bounties)
  local numBounties = 0
  for index, info in ipairs(db.Bounties) do
      numBounties = numBounties + 1
      info.index = index
      info.complete =  IsQuestComplete(info.questID)
      if not info.complete then
        db.BountiesByFactionID[info.factionID] = info
        db.BountiesByQuestID[info.questID] = info
        if index == selectedBountyIndex then
          db.selectedBounty = info
          selectedBountyQuestID = info.questID
        end
        print(' ', index, info.factionID, GetQuestLogTitle(GetQuestLogIndexByID(info.questID)), info.complete, (index == selectedBountyIndex) and 'SELECTED' or '')
      end
  end
end

-- check current artifact knowledge and update pins accordingly
function Module:UpdateArtifactPower(overrideLevel)
  if InCombatLockdown() then
    artifactPowerDirty = true
    return
  end

  print('|cFF00FF88UpdateArtifactPower()|r')
  local _, akLevel = GetCurrencyInfo(1171)
  if overrideLevel then
    akLevel = overrideLevel
  end

  --db.print('current AK', akLevel)
  if akLevel and (akLevel ~= artifactKnowledgeLevel) or (not artifactKnowledgeLevel) then
    --print('new ak level', akLevel)
    db.log('AK update ' .. tostring(artifactKnowledgeLevel) .. ' to '.. tostring(akLevel))
    for _, pin in pairs(db.QuestsByID) do
      if (pin.rewardType == REWARD_ARTIFACT_POWER) then
        print(pin.title, pin.itemNumber)
        local newAP = pin:UpdateArtifactPower()
        if newAP then
          pin.itemNumber = newAP
          print(newAP)
        else
          pin.dataLoaded = nil
        end
        pin.isStale = true
      end
    end
    artifactKnowledgeLevel = akLevel
  end
  artifactPowerDirty = nil
end

local msg = '|cFF00FF88WorldQuests:Refresh()|r|cFF00FFFF'
function Module:Refresh(...)

  if hooksDirty then
    hooksDirty = self:SetupCallbacks()
  end


  if not self:IsVisible() then
    print('|cFFFF4400Refresh()|r', ...)
    return
  else
    if lastRefresh == GetTime() then
      print('|cFFFF4400multiple refreshes tried')
    end
    lastRefresh = GetTime()
    print(msg, lastRefresh, ...)
  end


  if bountiesDirty then
    self:UpdateBounties()
  end

  if not db.Config.EnablePins then
    numShown = 0
    self.refreshBenchMark = GetTime()
    self.refreshBenchMarkTicker = 2
    print('starting bench', self.refreshBenchMark)
    return
  end

  scaleConstant = db.isContinentMap and 2 or 3
  canTargetQuests = SpellCanTargetQuest()

  for index, pin in pairs(db.QuestsByID) do
    pin.used = nil
  end

  self:UpdateAnchors(...)

  if artifactPowerDirty and not InCombatLockdown() then
    self:UpdateArtifactPower()
  end
  -- calculate quests shown
  numShown = 0
  numLoaded = 0
  for questID, pin in pairs(db.QuestsByID) do
    local oV = pin:IsShown()
    if pin.used then
      print('show', pin.title)
      pin.throttle = 1
      pin:SetShown(true)
      numShown = numShown + 1
      if pin.dataLoaded then
        numLoaded = numLoaded + 1
      end

      pin.checkCriteria = true
      pin.checkFilters = true
      pin:Refresh('WORLDMAP_REFRESH ' .. GetTime())

    else
      if pin:IsShown() then
        print('|cFFFF4400need to remove', pin.title)

      end

      pin.hideReason = "Not used in map area " .. (db.currentMapID)
      pin:SetShown(false)
    end

  end


--
  self.refreshBenchMark = GetTime()
  self.refreshBenchMarkTicker = 2
  print('starting bench', self.refreshBenchMark)

--

  layoutDirty = nil
  self.isStale = nil
  self.sizesDirty = nil
  self.isZoomDirty = nil

  if WorldPlanSummary then
    WorldPlanSummary.isStale = true
  end

end

function Module:RefreshIfChanged(event)
  local scaleCheck = WorldMapDetailFrame:GetScale()
  refreshReason = nil
  if scaleCheck ~= currentScale then
    refreshReason = 'map scale updated'
    currentScale = scaleCheck
    layoutDirty = true
  elseif self.isStale or layoutDirty then
    refreshReason = 'layout is marked dirty'
  end
  if not refreshReason then
    return
  end

  if self:IsVisible() then
    print('|cFF00FFFFRefreshIfChanged()|r', refreshReason)
    self:Refresh(event)
  else
    print('|cFF00FFFFRefreshIfChanged()|r', refreshReason)
    self.isStale = true
  end
end

-- Walks the current map tree and fires updates as needed
function Module:UpdateAnchors (event)
  wipe(self.UsedPositions)
  local hostWidth, hostHeight = WorldMapPOIFrame:GetSize()

  if (hostWidth ~= self.hostWidth) or (hostHeight ~= self.hostHeight) then
    self.hostWidth, self.hostHeight = hostWidth, hostHeight
    layoutDirty = true
  end

  print('|cFF00FF00UpdateAnchors()', event)
  local mapFileName, textureHeight, textureWidth, isMicroDungeon, microDungeonMapName = GetMapInfo()
  if isMicroDungeon then
    return
  end

  isDataLoaded = true
  local taskInfo = TQ_GetQuestsForPlayerByMapID(db.currentMapID)
  if taskInfo then
    self:UpdateQuestsForMap(taskInfo, db.currentMapID)
  end
  local numZones = MC_GetNumZones(db.currentMapID)
  if numZones then
    for i = 1, numZones do
      local mapAreaID = MC_GetZoneInfo(db.currentMapID, i)
      local taskInfo = TQ_GetQuestsForPlayerByMapID(mapAreaID, db.currentMapID)

      db.QuestsByZone[mapAreaID] = db.QuestsByZone[mapAreaID] or {}

      if taskInfo then
        self:UpdateQuestsForMap(taskInfo, mapAreaID)
      end
    end
  end
end

-- Attempt to display the pins for quests in taskInfo
function Module:UpdateQuestsForMap(taskInfo, mapID)
  print('|cFF00FF00UpdateQuestsForMap()|r', GetMapNameByID(mapID), GetMapNameByID(db.currentMapID), layoutDirty)
  if db.QuestsByZone[mapID] then
    wipe(db.QuestsByZone[mapID])
  elseif db.isBrokenIsle then
    continentScanned = true
  end
  db.PinStrata = WorldMapFrame_InWindowedMode() and 'HIGH' or 'FULLSCREEN'
  print('layoutDirty =',layoutDirty)

  for index, info in pairs(taskInfo) do
    local questID, x, y = info.questId, info.x, info.y
    local pin = self:AcquirePin(info)
    if pin then
      if pin.canShow then
        pin.used = true
        print('using', pin.title, (pin.owningFrame ~= WorldMapFrame))
        if layoutDirty or (pin.owningFrame ~= WorldMapFrame) then
          local scaleFactor = SCALE_FACTORS[(not pin.filtered and scaleConstant) or 1]
          pin.owningFrame = WorldMapFrame
          pin:SetAnchor(WorldMapPOIFrame, x, y, self.hostWidth, self.hostHeight, scaleFactor)

        end
        if db.QuestsByZone[mapID] then
          db.QuestsByZone[mapID][questID] = pin
        end
      else
        print('|cFFFF4400discarding|r', pin.title)
      end
    end
  end
end

-- locates or creates a corresponding pin frame for the provided TaskInfo data
function Module:AcquirePin (info)
  local questID = info.questId
  if not (questID and QuestUtils_IsQuestWorldQuest(questID)) then
    return nil
  end
  local pin = db.QuestsByID[questID]
  -- check to avoid creating unnecessary frames
  if IsQuestComplete(questID) or completedQuests[questID] then
    completedQuests[questID] = true
    if pin then
      pin:Release()
    end
    return nil
  end

  if not pin then
    local numFree = #db.FreePins
    if numFree >= 1 then
      pin = tremove(db.FreePins, numFree)
      print('|cFF00FF00Acquire()|r Re-using', pin:GetName())
    else
      totalPins = totalPins + 1
      local name = 'WorldPlanQuestMarker' .. numOverlays
      print('|cFF00FF00Acquire()|r Creating', name)
      pin = CreateFrame('Frame', name, WorldMapPOIFrame, 'WorldPlanQuestPin')

      pin:SetID(totalPins)
      numOverlays = numOverlays + 1
      --pin.iconBorder:SetVertexColor(0,0,0,1)
    end
    pin.questID = questID
    pin.throttle = pin.updateRate
    pin.currentWidth = nil

    db.QuestsByID[questID] = pin
    tinsert(db.UsedPins, pin)
  end

  if info then
    pin.inProgress = info.inProgress
    pin.floor = info.floor
    pin.numObjectives = info.numObjectives or 0
    if info.x and info.y then
      if (info.x ~= pin.x) or (info.y ~= pin.y) then
        pin.isStale = true
        --rprint('|cFFFF4400SetCoords|r', info.x, info.y)
      end
    end
  end

  pin.x = info.x or pin.x
  pin.y = info.y or pin.y

  if not HaveQuestRewardData(questID) then
    TQ_RequestPreloadRewardData(questID);
  end

  if (not pin.dataLoaded) then
    local dataLoaded = pin:GetData()
    if dataLoaded then
      WorldPlan.dataFlush = true
    else
      isDataLoaded = false
    end
  end

  pin:OnFilters()
  pin.isActive = TQ_IsActive(questID)
  --rprint(pin:GetID(), pin.filtered, pin.used)
  return pin
end

function Module:Debug(...)
  print(...)
end