view Turok/Modules/Timer/Timer.lua @ 9:9400a0ff8540

Ugh Timer: - container update directionality - talent update iterates over a non-volatile table to carry out updates - index management steps organized - talentRow status implemented, returns the spell associated with the talent chosen from that row CombatLog: - sort out font controls and unbork arguments
author Nenue
date Sun, 21 Feb 2016 13:08:30 -0500
parents a9b8b0866ece
children
line wrap: on
line source
--- Turok - Timer/Timer.lua
-- @file-author@
-- @project-revision@ @project-hash@
-- @file-revision@ @file-hash@
--- Defines common elements for the various timer HUDs
local ADDON, _A = ...
local _G, CreateFrame, tconcat, GetInventoryItemsForSlot, GetInventoryItemID = _G, CreateFrame, table.concat, GetInventoryItemsForSlot, GetInventoryItemID
local T, F, tostring, type, max, tinsert, unpack, UIParent, loadstring, rawset = _A.Addon, _A.LibFog, tostring, type, max, table.insert, unpack, _G.UIParent, loadstring, rawset
local mod = T.modules.TimerControl
local P = mod.prototype
local db

local pairs, ipairs, gsub, sub, setmetatable, wipe = pairs, ipairs, string.gsub, string.sub, setmetatable, wipe
local INVTYPE_FINGER, INVSLOT_FINGER1, INVSLOT_FINGER2, INVTYPE_TRINKET, INVSLOT_TRINKET1, INVSLOT_TRINKET2 =
INVTYPE_FINGER, INVSLOT_FINGER1, INVSLOT_FINGER2, INVTYPE_TRINKET, INVSLOT_TRINKET1, INVSLOT_TRINKET2
--@debug@
local DEBUG = true
--@end-debug@
local cType, cText, cNum, cWord, cKey, cPink, cBool = cType, cText, cNum, cWord, cKey, cPink, cBool
local print = function(...)
  if not DEBUG then return end
  if _G.Devian and _G.DevianDB.workspace ~= 1 then
    _G.print('Timer', ...)
  end
end
local teprint = function(...)
  if not DEBUG then return end
  if _G.Devian and _G.DevianDB.workspace ~= 1 then
    _G.print('TimerEvent',...)
  end
end
local tfprint = function(...)
  if not DEBUG then return end
  if _G.Devian and _G.DevianDB.workspace ~= 1 then
    _G.print('TimerFocus',...)
  end
end

local Timer_GetPrintHandler = function(self)
  if self.trace then
  return function(...)
    print(...)
    tfprint(...)
  end else
    return print
  end
end
local pb_suppressed = {}

function mod:OnInitialize()

  --@debug@
  TurokData.spirit.timers = Turok.defaults.spirit.timers
  --@end-debug@
  self.db = TurokData.spirit
  db = self.db
  self.active_cooldowns = {}
  self.cast_units = {}
  self.buff_units = {}
  self.loaded_types = {}
  self.loaded_triggers = {}
  self.equipped = {}
  self.containers = {}
  self.timers = {} -- active timers
  self.empty_frames = {} -- foster table for frames released by talent change


  T:RegisterChatCommand("tsp", self.Import_Open)
  T:RegisterChatCommand("tka", self.Dialog_Command)
  T:RegisterChatCommand("tki", self.CreateIndex)
  --T:Print("/tsp to import spells. /tka to open create dialog")
  -- suppress cacophony from all cooldowns activating at login
  self.quiet = true
  --self:ScheduleTimer(function() self:Dialog_Command() end, 4)

end

local mt_single = {
  __mode = "v",
  __newindex = function(t,k,v)
    rawset(t,k,v)
    --_G.print('DB', 'TCMeta: adding leaf', k, '=', v)
  end}
local mt_double = {
  __index = function(t,k)
    t[k] = setmetatable({}, mt_single)
    --_G.print('DB', 'TCMeta: add layer', k, '=', t[k])
    return t[k]
  end,
  __newindex = function(t,k,v)
    rawset(t,k,v)
    --_G.print('DB', 'TCMeta: adding to top layer', k, '=', v)
  end
}
local mt_error = {
  __call =function (t, txt)
    t.disable = true
    tinsert(t, txt)
  end
}

--- Sets and cleans up index data used by event handlers
local Timer_UpdateIndex = function(self, key)

  -- Is there already an entry for this key/value?
  if self.frames[key] then
    local lim = #mod.frames[key]
    --[[
    for i = self.frames[key]+1, lim, 1 do
      mod.frames[key][i] = mod.frames[key+1]
    end]]
    --self.frames[key] = nil
    print('     ', cText('mod.frames.')..cWord(key), '=', #mod.frames[key])
    print('     ', cText('self.frames.')..cWord(key), '=', cNum(self.frames[key]))
  end

  if key then
    local i = #mod.frames[key]+1
    --mod.frames[key][i] = self
    self.frames[key] = i
    print('     ', cText('self.frames.')..cWord(key), '=', #mod.frames[key])
  end
  mod.loaded_types[key] = (#mod.frames[key] == 0) and nil or true
  print('     ',cText(key..'_is_loaded'), '=', cBool(mod.loaded_types[key]))
end

--- Loading initators
function mod:OnEnable()
  mod.LoadPresets()
  mod.GetIndex()
  -- setup indexes, use nested weak table for status since they implicitly have a key variable
  mod.frames = {}
  for class, p in pairs(mod.prototype.status) do
    print('nested index table', class)
    mod.frames[class] = setmetatable({}, mt_double)
  end
  mod.frames.spellName = setmetatable({}, mt_double)
  for class, p in pairs(mod.prototype.display) do
    mod.frames[class] = setmetatable({}, mt_single)
  end
  for class, p in pairs(mod.prototype.trigger) do
    mod.frames[class] = setmetatable({}, mt_single)
  end

  local srcIndex = mod.timers
  if T.playerClass and mod.index[T.playerClass] then
    srcIndex = mod.index[T.playerClass]
    print('*** Found index for '..tostring(T.playerClass)..', using that.')
  else
    print(cWord('*** Using global index.'))
  end
  mod.activeSpec = T.specID

  --- go through that list
  for id, timer in pairs(srcIndex) do
    local result, message = mod:EnableTimer(id, timer)
  end

  mod.InitTimers()
  --- Delay sound activations so there isn't a giant cacophony on load
  mod:ScheduleTimer(function()
    self.quiet = nil
  end, db.audio_delay or 2)
end

function mod:EnableTimer(id, dvars)
  local print = Timer_GetPrintHandler(dvars)
  print('-{', cPink(dvars.name))
  if not dvars then
    if not mod.index.global[id] then
      return false,  "Unable to resolve dvars table."
    end
    dvars = mod.index.global[id]
  end
  if dvars.virtual then
    return
  end

  local spirit, newFrame = mod:GetTimer(id, dvars)
  if not spirit then return spirit, newFrame end

  local cvars = spirit.cvars
  local dvars = spirit.dvars
  local trigger = P.trigger[cvars.type]
  local display = P.display[cvars.display]
  local cvars = spirit.cvars
  local index = mod.frames
  local print = Timer_GetPrintHandler(cvars)

  if spirit.disable then
    return false, "Manually disabled." -- nothing to do, nothing to say
  end

  --- Interpret STATUS vars
  print(cText('  *** Merging Status Data'))
  spirit.disable = dvars.disable
  local pcount = 1
  for k, handler in pairs(P.status) do
    if cvars[k] then
      if handler.Init then
        print(cWord('  * Firing ')..cKey(k)..cWord('.Init'), cNum(cvars[k]))
        handler.Init(spirit, cvars[k])
      else
        print('   ', cText('skipped'), cKey(k))
      end
      pcount = pcount + 1
    end
  end

  spirit.Event = trigger.Event
  spirit.Value = trigger.Value
  spirit.SetText = mod.SetText
  spirit.LoadText = mod.LoadText
  spirit.Query = trigger.Query
  spirit.Set = trigger.Set

  --- Display handler init
  if display.Init then
    print(cText('  * Display Init:'), cKey(dvars.display))
    display.Init(spirit)
  end

  --- Trigger handler and events Load()
  print(cText('  * Trigger Init:'), cKey(dvars.type))
  trigger.Init(spirit)


  if C_PetBattles.IsInBattle() then
    spirit.disable = true
    spirit.debug_info("Hidden for pet battle")
    pb_suppressed[id] = true
  end


  if spirit.disable then
    spirit:UnregisterAllEvents()
    spirit.displayState = nil
    spirit.prevState = nil
    spirit:Hide()
    return false, tconcat(spirit.debug_info,"\n")
  else
    print('--', self.disable and cPink('DISABLED') or cNum('ENABLED'), #spirit.debug_info > 0 and tconcat(spirit.debug_info,"\n"), '}')
    return true, tconcat(spirit.debug_info,"\n")
  end
end

function mod:GetTimer(id, dvars)
  local print = Timer_GetPrintHandler(dvars)
  local newFrame
  if not mod.timers[id] then
    print(cKey('  [[CreateTimer'))
    newFrame = true
    --- Compile the cvar table from the various config layers:
     -- Start with timer dvars, overwritten by any container settings, then a disable check, then merge in prototype values
    local cvars = T.Config_Push({}, dvars, nil, cKey('['..id..']')..'.'..cWord('cvars'))
    cvars.name = dvars.name -- push function ignores name keys

    if dvars.container and db.containers[dvars.container] then
      print(cText('    * Merging Container overrides'))
      T.Config_Push(cvars, db.containers[dvars.container], cvars, cKey('['..id..']')..'.'..cWord('cvars'))
    end

    --- Stop here if disabled via SavedVars
    if cvars.disable then
      return false, "Manually disabled"
    end

    --- Localize the stuff we are going to loop over
    local display = P.display[cvars.display]
    local trigger = P.trigger[cvars.type]
    local displayType = cvars.display
    local triggerType = cvars.type
    if not (display and trigger) then
      return nil, "Missing prototype data. Summary: "..tostring(displayType).."="..(display and 'OK' or 'MISSING') ..
          " "..tostring(triggerType).."="..(trigger and 'OK' or 'MISSING')
    end

    --- Establish the order in which values are merged
    print(cText('    * Merging object CVars'))
    local cvar_class = {cWord('db.'..displayType), cWord('db.'..triggerType), cWord('db.global')}
    local cvar_array = {
      db[displayType],
      db[triggerType],
      db.global,
    }
    local override_class = {cWord('trigger.'..cvars.type), cWord('display.'.. cvars.display)}
    local override_array = {
      display.cvars,
      trigger.cvars }

    --- Table merge user settings
    for i, p in pairs(cvar_array) do
      print('    '..cNum(i)..' merge ['..cvar_class[i]..']')
      T.Config_Merge(cvars, p, cvars, cKey('['..id..']')..'.'..cWord('cvars'))
    end

    --- Overwrite with anything defined by the prototype structure because it's important
    local _, odiff
    for i, p in ipairs(override_array) do
      _, odiff = T.Config_Push(cvars, p, cvars, cKey('['..id..']')..'.'..cWord('cvars'))
    end
    local print = Timer_GetPrintHandler(cvars)

    --- Create the UI frame and seed it with the data we just composed
    local spirit =  CreateFrame('Frame', 'TurokTimerFrame'..gsub(dvars.name, "[^%a%d]", ''), UIParent, display.inherits)
    spirit.trace = cvars.trace
    spirit.timerID = id
    spirit.timerName = dvars.name
    spirit.container = dvars.container
    spirit.cvars = cvars
    spirit.dvars = dvars
    spirit.Update = display.Update
    spirit.SetState = display.SetState
    spirit.Report = mod.Report
    spirit.Stats = trigger.Stats

    --- Set Layout Statics
    T.SetFrameLayout(spirit, cvars)

    --- Create troubleshooting collection
    spirit.debug_info = setmetatable({}, mt_error)

    --- Add the frame to corresponding prototype indexes
    spirit.frames = {}
    spirit.events = {}

    if spirit.display ~= displayType then
      spirit.display = displayType
      Timer_UpdateIndex(spirit, displayType)
    end
    if spirit.type ~= triggerType then
      spirit.type = triggerType
      Timer_UpdateIndex(spirit, triggerType)
    end
    --- Add the frame to global index
    mod.timers[id] = spirit
  end

  return mod.timers[id], newFrame
end

function mod.InitTimers()
  teprint('INIT TIMERS ====================')
  for id, spirit in pairs(mod.timers) do
    if spirit.disable then
      teprint(id, 'disabled:', tconcat(spirit.debug_info or {}, ', '))
    else

    teprint(cText('init'), cNum(id), cWord(spirit.name))
    --- Throw a dry event to initialize values
    teprint(cText(' *'), cWord('prototype.'..cKey(spirit.dvars.type)..'.'..cWord('Load')))
    P.trigger[spirit.dvars.type].Event(spirit)

    --- Set loose
    teprint(cText(' *'), cWord('prototype')..'.'..cKey('events')..'.'..cWord('Load'))
    mod.UpdateEvents(spirit, P.trigger[spirit.dvars.type].events)
    end
  end
  teprint('INIT DONE =========================')
end

function mod:DisableTimer(name, timer)
  local timer_frame = mod.db.timers[name]
  if timer_frame and not timer_frame.disable then
    timer_frame.disable = true
    timer_frame:UnregisterAllEvents()
    timer_frame:Hide()
  end
end

function mod.UpdateEvents(self, events)
  local print = Timer_GetPrintHandler(self)

  self:SetScript('OnEvent', nil)
  self:UnregisterAllEvents()

  local proxy, listen = {}, {}
  for event, handler in pairs(events) do
    if mod[event] then
      tinsert(proxy, cNum(event))
    else
      tinsert(listen, cWord(event))
      self:RegisterEvent(event)
    end
    self.events[event] = handler
  end

  if #proxy > 0 then
    print( '  -', cKey(self.name), cWord('receiving'), tconcat(proxy, ', '))
  end
  if #listen > 0 then
    print( '  -', cKey(self.name), cText('listening'), tconcat(listen, ', '))
  end

  self:SetScript('OnEvent', self.Event)
end

local match_sub = {
  {'%%c', "' .. tostring(t.caster).. '"},
  {'%%h', "' .. tostring((t.valueFull >= 60) and (math.floor(t.valueFull/60)) or t.value) .. '"},
  {'%%i', "' .. tostring((t.valueFull >= 60) and (math.floor(t.valueFull/60) .. ':' .. ((t.value %% 60) < 10 and '0' or '').. (t.value %% 60)) or ((t.valueFull < 6) and (t.value .. '.' .. math.floor((t.valueFull * 10) %% 10)) or t.value)) .. '"},
  {'%%n', "' .. tostring(t.spellName) .. '"},
  {'%%p', "' .. tostring(t.value) .. '"},
  {'%%d', "' .. tostring(t.chargeDuration or t.duration) .. '"},
  {'%%%.p', "' .. string.sub(tostring((t.valueFull %% 1) * 100),0,1) .. '"},
  {"%%s", "' .. (t.stacks or t.charges or '') .. '"},
}

-- dot syntax implies use as embedded method
function mod.LoadText(self)
  print(cKey('parsing textRegions for'), self.timerName, self.timerID)
  self.textTypes = {}
  self.textValues = {}
  for name, region in pairs(self.textRegions) do
    print('  ', cWord('textRegions')..'["'.. cType(self.timerName)..'"].'..cType(name))
    if self.cvars[name..'Text'] then

      -- todo: collect match counts and index the text fields by match types
      local str = self.cvars[name..'Text']
      for i, args in ipairs(match_sub) do
        if str:match(args[1]) then
          if not self.textTypes[args[1]] then
            self.textTypes[args[1]] = {}
          end
          tinsert(self.textTypes[args[1]], region)
          str = str:gsub(args[1], args[2])
        end
      end
      str = "local t = _G.Turok.modules.TimerControl.timers["..self.timerID.."]\n"
          .. "\n return '" .. str .. "'"
      local func = assert(loadstring(str))
      self.textValues[name] = func
    end
  end

  --mod.SetText(self)
end

--- generic text setter
local HIDDEN, PASSIVE, ACTIVE = 0, 1, 2
mod.SetText = function(self)
  if self.displayState ~= ACTIVE then
    for name, region in pairs(self.textRegions) do
      region:SetText(nil)
    end
    return
  end

  if not self.textValues then
    self.textValues = {}
    mod.LoadText(self, self.cvars)
  end

  -- hide when above a certain number

  if self.spiral and self.spiral.subCounter then
    if self.valueFull > 6 then
      if self.textValues.subCounter then
        --print('hiding milliseconds')
        self.textRegions.subCounter:Hide()
        self.textRegionsSub = self.textRegions.subCounter
        self.textValuesSub = self.textValues.subCounter
        self.textRegions.subCounter = nil
        self.textValues.subCounter = nil
      end
    else
      if not self.textValues.subCounter then
        --print('showing milliseconds')
        self.textValues.subCounter = self.textValuesSub
        self.textRegions.subCounter = self.textRegionsSub
        self.textRegions.subCounter:Show()
      end
    end
  end

  for name, region in pairs(self.textRegions) do
    --print(name)
    --print(name, self.timerName, self.textValues[name](self))
    region:SetText(self.textValues[name](self))
  end
end


-------------------------------------------------------------------------
--- Second-tier handlers to cut down on the number of Status:Event() polls

--- UNIT_SPELLCAST_*** use args to filter out the number of full handler runs
function mod:UNIT_SPELLCAST_SUCCEEDED (e, unit, spellName, rank, castID, spellID)
  if not mod.frames.unit[unit] then
    return
  end

  if #mod.frames.spellName[spellName] > 0 then
    print('spellName-ID relation detected:', cWord(spellName), cNum(spellID))
    for i, frame in pairs(mod.frames.spellName[spellName]) do
      if not frame.frames.spellID then
        frame.frames.spellID = {}
      end
      if not frame.frames.spellID[spellID] then

        tinsert(mod.frames.spellID[spellID], frame)
        frame.frames.spellID[spellID] = #mod.frames.spellID[spellID]
        print(cText('  updating'), cKey(frame.timerName))
      end
    end
    mod.frames.spellName[spellName] = nil
  end



  if mod.frames.spellID[spellID] then
    for i, timer_frame in pairs(mod.frames.spellID[spellID]) do
      print(cText('caught spell'), cWord(spellName), 'for', timer_frame:GetName())
      timer_frame:Event(e, unit, spellName, rank, castID, spellID)
    end
  end
end
mod.UNIT_SPELLCAST_CHANNEL_START = mod.UNIT_SPELLCAST_SUCCEEDED

--- Fire a dry event to force status updates on units with changing GUID's
function mod:PLAYER_TARGET_CHANGED(e, unit)
  print('doing a target swap thing')
  for k, v in pairs( self.frames.unit.target) do
    print(k, v)
    v:Event(nil, 'target')
  end
end

--- Same thing but for talent/spec-driven
local update_queue = {}
function mod.ResetTimers(heading)
  print(cText('*** Flushing update queue for'), cWord(heading))
  for id, frame in pairs(update_queue) do
    print('  ', cNum(id), cKey(frame.timerName))
    frame.disable = nil
    wipe(frame.debug_info)
    local res, msg = mod:EnableTimer(id, frame.dvars)
  end
  wipe(update_queue)
end

function mod:PLAYER_TALENT_UPDATE(e, unit)
  print('')
  print('')
  print(cText(e), T.specPage, T.specName)

  for _, k in ipairs({'talentID', 'talentRow', 'specPage'}) do
    for value, frameSet in pairs(mod.frames.talentID) do
      for id, frame in ipairs(frameSet) do
        print(frame.timerID, frame.timerName)
        update_queue[frame.timerID] = frame
      end
    end
  end
  mod.resetTimers('Talent')
end

function mod:PLAYER_EQUIPMENT_CHANGED(e, slot, hasItem)
  print(e, slot, hasItem)
  if mod.frames.inventoryID and mod.frames.inventoryID[slot] then
    print('  Inventory slot:', cNum(slot))
    for i, slotFrame in ipairs(mod.frames.inventoryID[slot]) do
      print('   ', cNum(i), cText(slotFrame.timerName))
      update_queue[slotFrame.timerID] = slotFrame
      if mod.frames.itemID then
        local itemsForSlot = GetInventoryItemsForSlot(slot, {}, false)
        for _, itemID in pairs(itemsForSlot) do
          if mod.frames.itemID[itemID] then
            print('    Frames for equippable item:', cNum(itemID))
            for j, itemFrame in ipairs(mod.frames.itemID[itemID]) do
              print('     ', cNum(j), cText(itemFrame.timerName))
              update_queue[itemFrame.timerID] = itemFrame
            end
          end
        end
      end
    end
  end
  mod.ResetTimers('Equipment')
end
function mod:PET_BATTLE_OPENING_START ()
  for i, v in pairs(mod.timers) do
    if not v.disable then
      print('suppressing', v:GetName())
      v.disable = true
      v:Hide()
      pb_suppressed[i] = true
    end
  end
end
function mod:PET_BATTLE_CLOSE()
  for id, v in pairs(mod.timers) do
    if pb_suppressed[id] then
      print('restoring', v:GetName())
      mod:EnableTimer(id)
      pb_suppressed[id] = nil
    end
  end
end