view modules/ReAction_State/ReAction_State.lua @ 62:f9cdb920470a

Added first cut on State module. Menu system only, it doesn't do anything. The menu system and data storage will change substantially when the implementation takes shape.
author Flick <flickerstreak@gmail.com>
date Tue, 13 May 2008 16:42:52 +0000
parents 21bcaf8215ff
children 2000f4f4c6af
line wrap: on
line source
--[[
  ReAction bar state driver interface

--]]

-- local imports
local ReAction = ReAction
local L = ReAction.L
local _G = _G
local InCombatLockdown = InCombatLockdown

-- module declaration
local moduleID = "State"
local module = ReAction:NewModule( moduleID )


-- module event handlers
function module:OnInitialize()
  self.db = ReAction.db:RegisterNamespace( moduleID, 
    {
      profile = { 
        bars = { },
        presets = { }
      }
    }
  )
  self.states = { }
  self.options = setmetatable({},{__mode="k"})
end



-- ReAction module interface
function module:ApplyToBar(bar)
  self:RefreshBar(bar)
end

function module:RefreshBar(bar)
  local c = self.db.profile.bars[bar:GetName()]
  if c then
    --self:BuildStates(bar)
    --self:BuildRules(bar)
  end
end

function module:RemoveFromBar(bar)
end

function module:EraseBarConfig(barName)
  self.db.profile.bars[barName] = nil
end

function module:RenameBarConfig(oldname, newname)
  local b = self.db.profile.bars
  bars[newname], bars[oldname] = bars[oldname], nil
end

function module:ApplyConfigMode(mode,bars)
  -- swap out hidestates
end




-- Private --

-- traverse a table tree by key list and fetch the result or first nil
local function tfetch(t, ...)
  for i = 1, select('#', ...) do
    t = t and t[select(i, ...)]
  end
  return t
end

-- traverse a table tree by key list and build tree as necessary
local function tbuild(t, ...)
  for i = 1, select('#', ...) do
    local key = select(i, ...)
    if not t[key] then t[key] = { } end
    t = t[key]
  end
  return t
end


local rules
local BuildRuleString
do
  local function ClassCheck(...)
    for i = 1, select('#',...) do
      local _, c = UnitClass("player")
      if c == select(i,...) then
        return false
      end
    end
    return true
  end

  -- The structure of this table is important: each row is designed to be unpack()ed
  -- into variables as defined in the header comment row.
  -- The 'fields' subtable (index 4) structure is also important: its subtables 
  -- are ordered to match appearances of '%s' (string-substitutions) in the corresponding
  -- rules[] format-string entry. Each subtable's single element's key is the storage key used
  -- in the user db, and the name of the group-element table in the config tree, so it too
  -- is important.
  --
  -- While this allows for very compact code and data storage, the scheme is admittedly obtuse,
  -- so I document it here for my own sanity.
  --
  rules = {
    --  rule         name                 hidden                          fields
    { "stance",  L["Warrior Stance"], ClassCheck("WARRIOR"),          { {battle = L["Battle Stance"]}, {defensive = L["Defensive Stance"]}, {berserker = L["Berserker Stance"]} } },
    { "form",    L["Druid Form"],     ClassCheck("DRUID"),            { {bear = L["Bear"]}, {cat = L["Cat"]}, {treeOrMoonkin = L["Tree/Moonkin"]}, {caster = L["Normal"]},  } },
    { "stealth", L["Stealth"],        ClassCheck("ROGUE","DRUID"),    { {stealth = L["Stealth"]}, {normal = L["Normal"]} } },
    { "shadow",  L["Shadowform"],     ClassCheck("PRIEST"),           { {shadowform = L["Shadowform"]}, {normal = L["Normal"]} } },
    { "pet",     L["Pet"],            ClassCheck("HUNTER","WARLOCK"), { {pet = L["With Pet"]}, {nopet = L["Without Pet"]} } },
    { "target",  L["Target"],         false,                          { {hostile = L["Hostile Target"]}, {friendly = L["Friendly Target"]}, {none = L["No Target"]} } },
    { "focus",   L["Focus"],          false,                          { {hostile = L["Hostile Focus"]}, {friendly = L["Friendly Focus"]}, {none = L["No Focus"]} } },
    { "group",   L["In Group"],       false,                          { {raid = L["Raid"]}, {party = L["Party"]}, {solo = L["Solo"]} } },
    { "key",     L["Key Press"],      false,                          { {state = L["On Key Press"]}, {keybinding = false} } },  -- keybinding has no state-selector, it's implemented elsewhere
    { "combat",  L["Combat"],         false,                          { {combat = L["In Combat"]}, {nocombat = L["Out of Combat"]} } },
    { "custom",  L["Custom"],         false,                          { {rule = false} } }, -- custom has no state-selector, it's implemented elsewhere
  }

  do
    local forms = { }
    for i = 1, GetNumShapeshiftForms() do
      local icon, name = GetShapeshiftFormInfo(i)
      -- TODO: need to find out if name is localized, it probably is
      forms[name] = i;
    end
    local dStance = forms["Defensive Stance"] or 2
    local zStance = forms["Berserker Stance"] or 3
    local bForm   = forms["Dire Bear Form"] or forms["Bear Form"] or 1
    local cForm   = forms["Cat Form"] or 3
    local tForm   = forms["Tree of Life"] or forms["Moonkin Form"] or 5

    -- TODO: do the macro conditional strings need to be localized?
    local ruleformats = { 
      stance  = ("[stance:1] %%s; [stance:%d] %%s; [stance:%d] %%s"):format(dStance,zStance),
      form    = ("[form:%d] %%s; [form:%d] %%s; [form:%d] %%s; %%s"):format(bForm,cForm,tForm),
      stealth = "[stealth] %s; %s",
      shadow  = "[stance:1] %s; %s",
      pet     = "[pet] %s; %s",
      target  = "[harm] %s; [help] %s; %s",
      focus   = "[target=focus,harm] %s; [target=focus,help] %s; %s",
      group   = "[group:raid] %s; [group] %s; %s",
      combat  = "[combat] %s; %s",
      custom  = "%s",
    }

    local fieldmap = { }
    for i = 1, #rules do
      fieldmap[rules[i][1]] = rules[i][4]
    end

    local _scratch = { }
    function BuildRuleString(bar, name)
      local rule, data = module:GetRule(bar,name)
      if not rule then return "" end
      local fields = fieldmap[rule]
      for i = 1, #fields do
        _scratch[i] = data[next(fields[i])] -- TODO: insert default state here
      end
      for i = #fields+1, #_scratch do
        _scratch[i] = nil
      end
      local success, value = pcall(string.format, ruleformats[rule], unpack(_scratch))
      return success and value or "<error>"
    end
  end


end



-- API --

function module:BuildStates( bar )
  local c = tfetch(self.db.profile.bars, bar:GetName(), "states")
  if c then
    for name, s in pairs(c) do
      -- TODO: new state here
    end
  end
end

function module:CreateState( bar, name )
  local c = tbuild(self.db.profile.bars, bar:GetName(), "states")
  if c[name] then
    ReAction:UserError(L["State named '%s' already exists"]:format(name))
  else
    c[name] = { }
    -- TODO: new state here
  end
end

function module:DeleteState( bar, name )
  local c = tfetch(self.db.profile.bars, bar:GetName(), "states")
  if c[name] then
    -- TODO: delete state
    c[name] = nil
  end
end

function module:BuildRules( bar )
  for bar, c in pairs(self.db.profile.bars) do
    if c.rules then
      for name, t in pairs(c.rules) do
        local rule, config = next(t)
        self:SetRule(bar, name, rule, config)
      end
    end
  end
end

function module:CreateRule( bar, name, rule, config )
  local c = tbuild(self.db.profile.bars, bar:GetName(), "rules")
  if c[name] then
    ReAction:UserError(L["Rule named '%s' already exists"]:format(name))
  else
    tbuild(self.db.profile.bars, bar:GetName(), "rules", name)
    if rule then
      self:SetRule(bar,name,rule,config)
    end
  end
end

function module:DeleteRule( bar, name )
  local c = tfetch(self.db.profile.bars, bar:GetName(), "rules")
  if c[name] then
    local f = bar:GetFrame()
    -- TODO: delete rule
    c[name] = nil
  end
end

function module:UpdateRule( bar, name )
  local rule, c = self:GetRule(bar,name)
  -- TODO: remove all relevant outdated attributes
  -- TODO: set new attributes
end

function module:GetRule(bar, name)
  local c = tfetch(self.db.profile.bars, bar:GetName(), "rules", name)
  if c then
    return next(c) -- returns key, value (= rulename, configtable)
  end
end

function module:SetRule(bar, name, rule, config)
  tbuild(self.db.profile.bars, bar:GetName(), "rules")[name] = { [rule] = (config or {}) }
  self:UpdateRule(bar,name)
end


-- options --
local CreateBarOptions
do
  local function GetRuleConfig(bar, name, rule, field)
    return tfetch(module.db.profile.bars, bar:GetName(), "rules", name, rule, field)
  end

  local function SetRuleConfig( bar, name, rule, field, value )
    tbuild(module.db.profile.bars, bar:GetName(), "rules", name, rule)[field] = value
  end

  local function CreateStateOptions(bar, name)
    return {
      type = "group",
      name = name,
      args = {
        -- show/hide would go here
        -- page # would go here
        -- anchoring would go here
        __delete__ = {
          type = "execute",
          name = L["Delete this State"],
          func = function(info) 
              module:DeleteState(bar,name) 
              module.options[bar].args.states.args[name] = nil
            end,
          order = -1
        },
      }
    }
  end


  -- display rule string setting is shared between all rule opts and is transient
  -- (mostly used for debugging)
  local display = { show = false }

  local function CreateRuleOptions(bar, name)
    local function get(info)
      local rule  = info[#info-1]
      local field = info[#info]
      return GetRuleConfig(bar,name,rule,field) or ""
    end

    local function set(info, value)
      local rule  = info[#info-1]
      local field = info[#info]
      SetRuleConfig(bar,name,rule,field,value)
      module:UpdateRule(bar,name)
    end

    local opts = { 
      type = "group",
      name = name,
      childGroups = "inline",
      args = {
        __select__ = {
          type = "select",
          name = L["Select Rule Type"],
          get  = function(info) return module:GetRule(bar,name) or "" end,
          set  = function(info,value) module:SetRule(bar,name,value) end, -- TODO: get default rule config and pass as final value
          values = function()
              local v = { }
              for i = 1, #rules do
                local rule, name, hidden = unpack(rules[i])
                if not hidden then
                  v[rule] = name
                end
              end
              return v
            end,
          order = 1,
        },
        __delete__ = {
          type = "execute",
          name = L["Delete this Rule"],
          func = function(info) 
              module:DeleteRule(bar,name) 
              module.options[bar].args.rules.args[name] = nil
            end,
          order = -3
        },
        --
        -- rule selection groups will be inserted here
        --
        __show__ = {
          type = "toggle",
          name = L["Show Rule String"],
          desc = L["Toggles display of the raw rule string"],
          get  = function() return display.show end,
          set  = function(info,value) display.show = value end,
          order = -2
        },
        __rule__ = {
          type = "input",
          name = L["Rule String"],
          disabled = true,
          multiline = true,
          width = "double",
          hidden = function() return not display.show end,
          get = function() return BuildRuleString(bar,name) end,
          set = function() end,
          order = -1
        }
      }
    }

    -- unpack rules table
    for i = 1, #rules do
      local rule, label, _, fields = unpack(rules[i])
      local hidden = function() 
        return module:GetRule(bar,name) ~= rule 
      end
      opts.args[rule] = {
        type = "group",
        name = label,
        hidden = hidden,
        disabled = hidden,
        inline = true,
        args = { }
      }
      for j = 1, #fields do
        local field, label = next(fields[j]) -- extract from table of single key-value pair
        if field and label then
          opts.args[rule].args[field] = {
            type = "select",
            name = L["Select State (%s):"]:format(label),
            values = function ()
                local states = tfetch(module.db.profile.bars,bar:GetName(),"states")
                local v = { }
                if states then
                  for k in pairs(states) do
                    v[k] = k
                  end
                end
                return v
              end,
            set  = set,
            get  = get,
            order = 100 + j
          }
        end
      end
    end

    -- set up special entry for keybinding
    opts.args.key.args.binding = {
      type = "keybinding",
      name = L["Key Binding"],
      get = get,
      set = set,
      order = -1
    }

    -- set up special entry for custom
    opts.args.custom.args.rule = {
      type = "input",
      name = L["Rule"],
      desc = L["Syntax like macro conditions: see preset rules for examples"],
      get  = get,
      set  = set,
      validate = function (info, rule)
          local s = rule:gsub("%s","") -- remove all spaces
          if s:match(";$") then -- can't end with semicolon
            return L["Invalid custom rule '%s': Rule cannot end with ';'"]:format(rule)
          end
          if s == "" then
            return true
          end
          -- unfortunately %b and captures don't support the '+' notation, or this would be considerably simpler
          repeat
            repeat
              local c, r = s:match("(%b[])(.*)")
              if r then s = r end
            until c == nil
            local state, s = s:match("(%w+)(.*)")
            if not state then
              return L["Invalid custom rule '%s': Each expression must have a state"]:format(rule)
            end
            if not tfetch(module.db.profile.bars,bar:GetName(),"states",state) then
              return L["Invalid custom rule '%s': '%s' is not a state"]:format(rule,state)
            end
            if s:match("^[^;]") then
              return L["Invalid custom rule '%s': Expressions must be separated by ';'"]:format(rule)
            end
          until #s == 0
          return true
        end,
      multiline = true,
      order = 1,
    }

    return opts
  end

  CreateBarOptions = function(bar)
    local private = { }
    local options = {
      type = "group",
      name = L["Dynamic State"],
      childGroups = "tab",
      disabled = InCombatLockdown,
      args = {
        states = {
          type = "group",
          name = L["States"],
          childGroups = "tree",
          order = 1,
          args = {
            __new__ = {
              type = "group",
              name = L["New State..."],
              order = 1,
              args = {
                name = {
                  type = "input",
                  name = L["State Name"],
                  desc = L["Set a name for the new state"],
                  get = function() return private.newstatename or "" end,
                  set = function(info,value) private.newstatename = value end,
                  pattern = "^%w*$",
                  usage = L["State names must be alphanumeric without spaces"],
                  order = 1
                },
                create = {
                  type = "execute",
                  name = L["Create State"],
                  func = function ()
                      local name = private.newstatename
                      module:CreateState(bar,name) -- TODO: select default state options and pass as final argument
                      module.options[bar].args.states.args[name] = CreateStateOptions(bar,name)
                      private.newstatename = ""
                    end,
                  disabled = function()
                      local name = private.newstatename or ""
                      return #name == 0 or name:find("%W")
                    end,
                  order = 2,
                }
              }
            }
          }
        },
        rules = {
          type = "group",
          name = L["Transition Rules"],
          childGroups = "tree",
          order = 2,
          args = {
            __new__ = {
              type = "group",
              name = L["New Rule..."],
              order = 1,
              args = {
                name = {
                  type = "input",
                  name = L["Rule Name"],
                  desc = L["Set a name for the new transition rule"],
                  get = function() return private.newtransname or "" end,
                  set = function(info,value) private.newtransname = value end,
                  pattern = "^%w*$",
                  usage = L["Rule names must be alphanumeric without spaces"],
                  order = 1
                },
                create = {
                  type = "execute",
                  name = L["Create Rule"],
                  func = function ()
                      local name = private.newtransname
                      module:CreateRule(bar,name) -- TODO: select default rule and add as final argument
                      module.options[bar].args.rules.args[name] = CreateRuleOptions(bar,name)
                      private.newtransname = ""
                    end,
                  disabled = function ()
                      local name = private.newtransname or ""
                      return #name == 0 or name:find("%W")
                    end,
                  order = 2,
                },
              }
            }
          }
        },
        presets = {
          type = "group",
          name = L["Presets"],
          order = 3,
          args = {
            desc = {
              type = "description",
              name = L["Presets are canned sets of states and transitions. You can create your own presets to add to ReAction's built in defaults."],
              order = 1,
            },
            select = {
              type = "select",
              name = L["Select Preset"],
              set  = function(info,value) end,
              get  = function() return "" end,
              values = function() return { } end,
              order = 2,
            },
            load = {
              type = "execute",
              name = L["Load"],
              func = function() end,
              width = "half",
              order = 3,
            },
            delete = {
              type = "execute",
              name = L["Delete"],
              disabled = function() return false end,
              func = function() end,
              width = "half",
              order = 4,
            },
            hdr = {
              type = "header",
              name = " ",
              order = 5,
            },
            save = {
              type = "input",
              name = L["Save As..."],
              get  = function() return "" end,
              set  = function(info,name) end,
              order = 6,
            },
          },
        },
      }
    }
    local states = tfetch(module.db.profile.bars, bar:GetName(), "states")
    if states then
      for name, config in pairs(states) do
        options.args.states.args[name] = CreateStateOptions(bar,name)
      end
    end
    local rules = tfetch(module.db.profile.bars, bar:GetName(), "rules")
    if rules then
      for name, config in pairs(rules) do
        options.args.rules.args[name] = CreateRuleOptions(bar,name)
      end
    end
    return options
  end
end

function module:GetBarOptions(bar)
  if not self.options[bar] then
    self.options[bar] = CreateBarOptions(bar)
  end
  return self.options[bar]
end