view WorldMap.lua @ 113:03e4a8b93012 v7.3.0-2

7.3 Updates - Added some map frame adjustments to keep the filter bar from obstructing edge of zone clicks KNOWN ISSUES: - Argus quests can only be filtered when viewing Argus maps - Main Argus map in the WorldMapFrame has misaligned pins; this is due to it using a different map texture from what is used in the teleporter map, so all the POI coordinates are based on that map in the client info
author Nenue
date Tue, 05 Sep 2017 03:14:34 -0400
parents e8b6c5433128
children a4dfdd4f1cf3
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
      -- client-side quest completion status isn't updated until the next event stack
      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)
  local transformFlags = (db.currentMapID == 1184) and Enum.MapTransform.IsForFlightMap or nil
  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, transformFlags)

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

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


end

-- Attempt to display the pins for quests in taskInfo
function Module:UpdateQuestsForMap(taskInfo, mapID, transformFlags)
  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

    -- rough workaround for argus map; flightmap and world map images are different, so not completely accurate
    if transformFlags and x and y then
      print(x,y)
      x = x / 2 + .25
      y = y / 2 + .30
      print(x,y)
    end


    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