Mercurial > wow > reaction
diff State.lua @ 69:a785d6708388
moved State.lua to a top level file
author | Flick <flickerstreak@gmail.com> |
---|---|
date | Tue, 03 Jun 2008 23:05:16 +0000 |
parents | modules/ReAction_State/ReAction_State.lua@fcb5dad031f9 |
children | 2c12e2b1752e |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/State.lua Tue Jun 03 23:05:16 2008 +0000 @@ -0,0 +1,927 @@ +--[[ + 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, "AceEvent-3.0" ) + +-- Utility -- + +-- 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 + +-- PRIVATE -- + +local InitRules, ApplyStates, SetProperty, GetProperty +do + -- As far as I can tell the macro clauses are NOT locale-specific. + local ruleformats = { + stealth = "stealth", + nostealth = "nostealth", + shadowform = "form:1", + noshadowform = "noform", + pet = "pet", + nopet = "nopet", + harm = "target=target,harm", + help = "target=target,help", + notarget = "target=target,noexists", + focusharm = "target=focus,harm", + focushelp = "target=focus,help", + nofocus = "target=focus,noexists", + raid = "group:raid", + party = "group:party", + solo = "nogroup", + combat = "combat", + nocombat = "nocombat", + } + + -- Have to do these shenanigans instead of hardcoding the stances/forms because + -- the ordering varies if the character is missing a form. For warriors + -- this is rarely a problem (c'mon, who actually skips the level 10 def stance quest?) + -- but for druids it can be. Some people never bother to do the aquatic form quest + -- until well past when they get cat form, and stance 5 can be flight, tree, or moonkin + -- depending on talents. + function InitRules() + local forms = { } + -- sort by icon since it's locale-independent + for i = 1, GetNumShapeshiftForms() do + local icon = GetShapeshiftFormInfo(i) + forms[icon] = i; + end + -- use 9 if not found since 9 is never a valid stance/form + local defensive = forms["Interface\\Icons\\Ability_Warrior_DefensiveStance"] or 9 + local berserker = forms["Interface\\Icons\\Ability_Racial_Avatar"] or 9 + local bear = forms["Interface\\Icons\\Ability_Racial_BearForm"] or 9 -- bear and dire bear share the same icon + local aquatic = forms["Interface\\Icons\\Ability_Druid_AquaticForm"] or 9 + local cat = forms["Interface\\Icons\\Ability_Druid_CatForm"] or 9 + local travel = forms["Interface\\Icons\\Ability_Druid_TravelForm"] or 9 + local treekin = forms["Interface\\Icons\\Ability_Druid_TreeofLife"] or forms["Interface\\Icons\\Spell_Nature_ForceOfNature"] or 9 + local flight = forms["Interface\\Icons\\Ability_Druid_FlightForm"] or 9 -- flight and swift flight share the same icon + + ruleformats.battle = "stance:1" + ruleformats.defensive = ("stance:%d"):format(defensive) + ruleformats.berserker = ("stance:%d"):format(berserker) + ruleformats.caster = ("form:0/%d/%d/%d"):format(aquatic, travel, flight) + ruleformats.bear = ("form:%d"):format(bear) + ruleformats.cat = ("form:%d"):format(cat) + ruleformats.treeOrMoonkin = ("form:%d"):format(treekin) + end + + -- return a new array of keys of table 't', sorted by comparing + -- sub-fields (obtained via tfetch) of the table values + local function fieldsort( t, ... ) + local r = { } + for k in pairs(t) do + table.insert(r,k) + end + local dotdotdot = { ... } + table.sort(r, function(lhs, rhs) + local olhs = tfetch(t[lhs], unpack(dotdotdot)) or 0 + local orhs = tfetch(t[rhs], unpack(dotdotdot)) or 0 + return olhs < orhs + end) + return r + end + + local function BuildRuleString(states) + local s = "" + local default + local sorted = fieldsort(states, "rule", "order") + for idx, name in ipairs(sorted) do + local state = states[name] + local semi = #s > 0 and "; " or "" + local mode = tfetch(state,"rule","type") + if mode == "default" then + default = name + elseif mode == "custom" then + if state.rule.custom then + -- strip out all spaces from the custom rule + s = ("%s%s%s %s"):format(s, semi, state.rule.custom:gsub("%s",""), name) + end + elseif mode == "any" then + if state.rule.values then + local clause = "" + for key, value in pairs(state.rule.values) do + clause = ("%s[%s]"):format(clause,ruleformats[key]) + end + if #clause > 0 then + s = ("%s%s%s %s"):format(s, semi, clause, name) + end + end + elseif mode == "all" then + if state.rule.values then + local clause = "" + for key, value in pairs(state.rule.values) do + clause = ("%s%s%s"):format(clause,#clause > 0 and "," or "", ruleformats[key]) + end + if #clause > 0 then + s = ("%s%s[%s] %s"):format(s, semi, clause, name) + end + end + end + end + if default then + s = ("%s%s%s"):format(s, #s > 0 and "; " or "", default) + end + return s, default + end + + local drivers = setmetatable({},{__mode="k"}) + local propertyFuncs = { } + + function ApplyStates( bar ) + local states = tfetch(module.db.profile.bars, bar:GetName(), "states") + if states then + local frame = bar:GetFrame() + local string, default = BuildRuleString(states) + if string and #string > 0 then + drivers[bar] = true + -- register a map for each "statemap-reaction-XXX" to set 'state' to 'XXX' + -- UNLESS we're in a keybound state AND there's a default state, in which case + -- all keybound states go back to themselves. + local keybindprefix + if default then + local tmp = { } + for state, config in pairs(states) do + if tfetch(config, "rule", "type") == "keybind" then + bar:SetStateKeybind(tfetch(config,"rule","keybind"), state, tfetch(config,"rule","keybindreturn") or default or 0) + table.insert(tmp, ("%s:%s"):format(state,state)) + end + end + if #tmp > 0 then + table.insert(tmp,"") -- to get a final ';' + end + keybindprefix = table.concat(tmp,";") + end + for state in pairs(states) do + frame:SetAttribute(("statemap-reaction-%s"):format(state), ("%s%s"):format(keybindprefix or "",state)) + end + -- register a handler to set the value of attribute "state-reaction" + -- in response to events as per the rule string + RegisterStateDriver(frame, "reaction", string) + SecureStateHeader_Refresh(frame) + elseif drivers[bar] then + UnregisterStateDriver(frame, "reaction") + drivers[bar] = nil + end + for k, f in pairs(propertyFuncs) do + f(bar, states) + end + end + end + + function GetProperty( bar, state, propname ) + return tfetch(module.db.profile.bars, bar:GetName(), "states", state, propname) + end + + function SetProperty( bar, state, propname, value ) + local states = tbuild(module.db.profile.bars, bar:GetName(), "states") + tbuild(states, state)[propname] = value + local f = propertyFuncs[propname] + if f then + f(bar, states) + end + end + + -- state property functions + function propertyFuncs.hide( bar, states ) + local tmp = { } + for state, config in pairs(states) do + if config.hide then + table.insert(tmp, state) + end + end + local s = table.concat(tmp,",") + bar:SetHideStates(s) + end + + function propertyFuncs.page( bar, states ) + local map = { } + for state, config in pairs(states) do + map[state] = config.page + end + bar:SetStatePageMap(state, map) + end + + function propertyFuncs.keybindstate( bar, states ) + local map = { } + for state, config in pairs(states) do + if config.keybindstate then + table.insert(map,state) + end + end + bar:SetStateKeybindOverrideMap(map) + end + + function propertyFuncs.enableanchor( bar, states ) + + end + + function propertyFuncs.anchorPoint( bar, states ) + + end + + function propertyFuncs.anchorRelPoint( bar, states ) + + end + + function propertyFuncs.anchorX( bar, states ) + + end + + function propertyFuncs.anchorY( bar, states ) + + end + + function propertyFuncs.enablescale( bar, states ) + + end + + function propertyFuncs.scale( bar, states ) + + end + +end + + + +-- module event handlers -- + +function module:OnInitialize() + self.db = ReAction.db:RegisterNamespace( moduleID, + { + profile = { + bars = { }, + } + } + ) + + InitRules() + self:RegisterEvent("PLAYER_AURAS_CHANGED") + + ReAction:RegisterBarOptionGenerator(self, "GetBarOptions") + + ReAction.RegisterCallback(self, "OnCreateBar","OnRefreshBar") + ReAction.RegisterCallback(self, "OnRefreshBar") + ReAction.RegisterCallback(self, "OnEraseBar") + ReAction.RegisterCallback(self, "OnRenameBar") + ReAction.RegisterCallback(self, "OnConfigModeChanged") +end + +function module:PLAYER_AURAS_CHANGED() + self:UnregisterEvent("PLAYER_AURAS_CHANGED") + -- on login the number of stances is 0 until this event fires during the init sequence. + -- however if you reload just the UI the number of stances is correct immediately + -- and this event won't fire until you gain/lose buffs/debuffs, at which point you might + -- be in combat. + if not InCombatLockdown() then + InitRules() + for name, bar in ReAction:IterateBars() do + self:OnRefreshBar(nil,bar,name) + end + end +end + +function module:OnRefreshBar(event, bar, name) + local c = self.db.profile.bars[name] + if c then + ApplyStates(bar) + end +end + +function module:OnEraseBar(event, bar, name) + self.db.profile.bars[name] = nil +end + +function module:OnRenameBar(event, bar, oldname, newname) + local b = self.db.profile.bars + bars[newname], bars[oldname] = bars[oldname], nil +end + +function module:OnConfigModeChanged(event, mode) + -- TODO: unregister all state drivers (temporarily) and hidestates +end + + + +-- Options -- + +local CreateBarOptions +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 + + -- pre-sorted by the order they should appear in + local rules = { + -- rule hidden fields + { "stance", ClassCheck("WARRIOR"), { {battle = L["Battle Stance"]}, {defensive = L["Defensive Stance"]}, {berserker = L["Berserker Stance"]} } }, + { "form", ClassCheck("DRUID"), { {caster = L["Caster Form"]}, {bear = L["Bear Form"]}, {cat = L["Cat Form"]}, {treeOrMoonkin = L["Tree/Moonkin"]} } }, + { "stealth", ClassCheck("ROGUE","DRUID"), { {stealth = L["Stealth"]}, {nostealth = L["No Stealth"]} } }, + { "shadow", ClassCheck("PRIEST"), { {shadowform = L["Shadowform"]}, {noshadowform = L["No Shadowform"]} } }, + { "pet", ClassCheck("HUNTER","WARLOCK"), { {pet = L["With Pet"]}, {nopet = L["Without Pet"]} } }, + { "target", false, { {harm = L["Hostile Target"]}, {help = L["Friendly Target"]}, {notarget = L["No Target"]} } }, + { "focus", false, { {focusharm = L["Hostile Focus"]}, {focushelp = L["Friendly Focus"]}, {nofocus = L["No Focus"]} } }, + { "group", false, { {raid = L["Raid"]}, {party = L["Party"]}, {solo = L["Solo"]} } }, + { "combat", false, { {combat = L["In Combat"]}, {nocombat = L["Out of Combat"]} } }, + } + + local ruleSelect = { } + local ruleMap = { } + local optionMap = setmetatable({},{__mode="k"}) + + local pointTable = { + NONE = " ", + CENTER = L["Center"], + LEFT = L["Left"], + RIGHT = L["Right"], + TOP = L["Top"], + BOTTOM = L["Bottom"], + TOPLEFT = L["Top Left"], + TOPRIGHT = L["Top Right"], + BOTTOMLEFT = L["Bottom Left"], + BOTTOMRIGHT = L["Bottom Right"], + } + + -- unpack rules table into ruleSelect and ruleMap + for _, c in ipairs(rules) do + local rule, hidden, fields = unpack(c) + if not hidden then + for _, field in ipairs(fields) do + local key, label = next(field) + table.insert(ruleSelect, label) + table.insert(ruleMap, key) + end + end + end + + local function CreateStateOptions(bar, name) + local opts = { + type = "group", + name = name, + childGroups = "tab", + } + + local states = tbuild(module.db.profile.bars, bar:GetName(), "states") + + local function update() + ApplyStates(bar) + end + + local function setrule( key, value, ... ) + tbuild(states, opts.name, "rule", ...)[key] = value + end + + local function getrule( ... ) + return tfetch(states, opts.name, "rule", ...) + end + + local function setprop(info, value) + SetProperty(bar, opts.name, info[#info], value) + end + + local function getprop(info) + return GetProperty(bar, opts.name, info[#info]) + end + + local function fixall(setkey) + -- if multiple selections in the same group are chosen when 'all' is selected, + -- keep only one of them. If changing the mode, the first in the fields list will + -- be chosen arbitrarily. Otherwise, if selecting a new checkbox from the field-set, + -- it will be retained. + local notified = false + for _, c in ipairs(rules) do + local rule, hidden, fields = unpack(c) + local found = false + for key in ipairs(fields) do + if getrule("values",key) then + if (found or setkey) and key ~= setkey then + setrule(key,false,"values") + if not setkey and not notified then + ReAction:UserError(L["Warning: one or more incompatible rules were turned off"]) + notified = true + end + end + found = true + end + end + end + end + + local function getNeighbors() + local before, after + for k, v in pairs(states) do + local o = tonumber(tfetch(v, "rule", "order")) + if o and k ~= opts.name then + local obefore = tfetch(states,before,"rule","order") + local oafter = tfetch(states,after,"rule","order") + if o < opts.order and (not obefore or obefore < o) then + before = k + end + if o > opts.order and (not oafter or oafter > o) then + after = k + end + end + end + return before, after + end + + local function swapOrder( a, b ) + -- do options table + local args = optionMap[bar].args + args[a].order, args[b].order = args[b].order, args[a].order + -- do profile + a = tbuild(states, a, "rule") + b = tbuild(states, b, "rule") + a.order, b.order = b.order, a.order + end + + local function anchordisable() + return not GetProperty(bar, opts.name, "enableanchor") + end + + tbuild(states, name) + + opts.order = getrule("order") + if opts.order == nil then + -- add after the highest + opts.order = 100 + for _, state in pairs(states) do + local x = tonumber(tfetch(state, "rule", "order")) + if x and x >= opts.order then + opts.order = x + 1 + end + end + setrule("order",opts.order) + end + + opts.args = { + ordering = { + name = L["Info"], + order = 1, + type = "group", + args = { + delete = { + name = L["Delete this State"], + order = -1, + type = "execute", + func = function(info) + if states[opts.name] then + states[opts.name] = nil + ApplyStates(bar) + end + optionMap[bar].args[opts.name] = nil + end, + }, + rename = { + name = L["Name"], + order = 1, + type = "input", + get = function() return opts.name end, + set = function(info, value) + -- check for existing state name + if states[value] then + L["State named '%s' already exists"]:format(value) + end + local args = optionMap[bar].args + states[value], args[value], states[opts.name], args[opts.name] = states[opts.name], args[opts.name], nil, nil + opts.name = value + update() + end, + pattern = "^%w*$", + usage = L["State names must be alphanumeric without spaces"], + }, + ordering = { + name = L["Evaluation Order"], + desc = L["State transitions are evaluated in the order listed:\nMove a state up or down to change the order"], + order = 2, + type = "group", + inline = true, + args = { + up = { + name = L["Up"], + order = 1, + type = "execute", + width = "half", + func = function() + local before, after = getNeighbors() + if before then + swapOrder(before, opts.name) + update() + end + end, + }, + down = { + name = L["Down"], + order = 2, + type = "execute", + width = "half", + func = function() + local before, after = getNeighbors() + if after then + swapOrder(opts.name, after) + update() + end + end, + } + } + } + } + }, + properties = { + name = L["Properties"], + order = 2, + type = "group", + args = { + desc = { + name = L["Set the properties for the bar when in this state"], + order = 1, + type = "description" + }, + hide = { + name = L["Hide Bar"], + order = 2, + type = "toggle", + set = setprop, + get = getprop, + }, + page = { + name = L["Show Page #"], + order = 3, + type = "select", + disabled = function() + return bar:GetNumPages() < 2 + end, + hidden = function() + return bar:GetNumPages() < 2 + end, + values = function() + local pages = { none = " " } + for i = 1, bar:GetNumPages() do + pages[i] = i + end + return pages + end, + set = function(info, value) + if value == "none" then + setprop(info, nil) + else + setprop(info, value) + end + end, + get = function(info) + return getprop(info) or "none" + end, + }, + keybindstate = { + name = L["Override Keybinds"], + desc = L["Set this state to maintain its own set of keybinds which override the defaults when active"], + order = 4, + type = "toggle", + set = setprop, + get = getprop, + }, + position = { + name = L["Position"], + order = 5, + type = "group", + inline = true, + args = { + enableanchor = { + name = L["Set New Position"], + order = 1, + type = "toggle", + set = setprop, + get = getprop, + }, + anchorPoint = { + name = L["Point"], + order = 2, + type = "select", + values = pointTable, + set = function(info, value) setprop(info, value ~= "NONE" and value or nil) end, + get = function(info) return getprop(info) or "NONE" end, + disabled = anchordisable, + hidden = anchordisable, + }, + anchorRelPoint = { + name = L["Relative Point"], + order = 3, + type = "select", + values = pointTable, + set = function(info, value) setprop(info, value ~= "NONE" and value or nil) end, + get = function(info) return getprop(info) or "NONE" end, + disabled = anchordisable, + hidden = anchordisable, + }, + anchorX = { + name = L["X Offset"], + order = 4, + type = "range", + min = -100, + max = 100, + step = 1, + set = setprop, + get = getprop, + disabled = anchordisable, + hidden = anchordisable, + }, + anchorY = { + name = L["Y Offset"], + order = 5, + type = "range", + min = -100, + max = 100, + step = 1, + set = setprop, + get = getprop, + disabled = anchordisable, + hidden = anchordisable, + }, + }, + }, + scale = { + name = L["Scale"], + order = 6, + type = "group", + inline = true, + args = { + enablescale = { + name = L["Set New Scale"], + order = 1, + type = "toggle", + set = setprop, + get = getprop, + }, + scale = { + name = L["Scale"], + order = 2, + type = "range", + min = 0.1, + max = 2.5, + step = 0.05, + isPercent = true, + set = setprop, + get = function(info) return getprop(info) or 1 end, + disabled = function() return not GetProperty(bar, opts.name, "enablescale") end, + hidden = function() return not GetProperty(bar, opts.name, "enablescale") end, + }, + }, + }, + }, + }, + rules = { + name = L["Selection Rule"], + order = 3, + type = "group", + args = { + mode = { + name = L["Select this state"], + order = 2, + type = "select", + style = "radio", + values = { + default = L["by default"], + any = L["when ANY of these"], + all = L["when ALL of these"], + custom = L["via custom rule"], + keybind = L["via keybinding"], + }, + set = function( info, value ) + setrule("type", value) + fixall() + update() + end, + get = function( info ) + return getrule("type") + end, + }, + clear = { + name = L["Clear All"], + order = 3, + type = "execute", + hidden = function() + local t = getrule("type") + return t ~= "any" and t ~= "all" + end, + disabled = function() + local t = getrule("type") + return t ~= "any" and t ~= "all" + end, + func = function() + local type = getrule("type") + if type == "custom" then + setrule("custom","") + elseif type == "any" or type == "all" then + setrule("values", {}) + end + update() + end, + }, + inputs = { + name = L["Conditions"], + order = 4, + type = "multiselect", + hidden = function() + local t = getrule("type") + return t ~= "any" and t ~= "all" + end, + disabled = function() + local t = getrule("type") + return t ~= "any" and t ~= "all" + end, + values = ruleSelect, + set = function(info, key, value ) + setrule(ruleMap[key], value or nil, "values") + if value then + fixall(ruleMap[key]) + end + update() + end, + get = function(info, key) + return getrule("values", ruleMap[key]) or false + end, + }, + custom = { + name = L["Custom Rule"], + order = 5, + type = "input", + multiline = true, + hidden = function() + return getrule("type") ~= "custom" + end, + disabled = function() + return getrule("type") ~= "custom" + end, + desc = L["Syntax like macro rules: see preset rules for examples"], + set = function(info, value) + setrule("custom",value) + update() + end, + get = function(info) + return getrule("custom") or "" + end, + validate = function (info, rule) + local s = rule:gsub("%s","") -- remove all spaces + -- unfortunately %b and captures don't support the '+' notation, or this would be considerably simpler + repeat + if s == "" then + return true + end + local c, r = s:match("(%b[])(.*)") + if c == nil and s and #s > 0 then + return L["Invalid custom rule '%s': each clause must appear within [brackets]"]:format(rule) + end + s = r + until c == nil + return true + end, + }, + keybind = { + name = L["Keybinding"], + order = 6, + inline = true, + hidden = function() return getrule("type") ~= "keybind" end, + disabled = function() return getrule("type") ~= "keybind" end, + type = "group", + args = { + desc = { + name = L["Invoking a state keybind overrides all other transition rules. Toggle the keybind again to remove the override and return to the specified toggle-off state."], + order = 1, + type = "description", + }, + keybind = { + name = L["State Hotkey"], + desc = L["Define an override toggle keybind"], + order = 2, + type = "keybinding", + set = function(info, value) + setrule("keybind",value) + update() + end, + get = function() return getrule("keybind") end, + }, + default = { + name = L["Toggle Off State"], + desc = L["Select a state to return to when the keybind override is toggled off"], + order = 3, + type = "select", + values = function() + local t = { } + for k in pairs(states) do + if k ~= opts.name then + t[k] = k + end + end + return t + end, + set = function(info, value) + setrule("keybindreturn",value) + update() + end, + get = function() return getrule("keybindreturn") end, + }, + }, + }, + }, + }, + } + return opts + end + + + CreateBarOptions = function(bar) + local private = { } + local options = { + type = "group", + name = L["Dynamic State"], + childGroups = "tree", + disabled = InCombatLockdown, + args = { + __desc__ = { + name = L["States are evaluated in the order they are listed"], + order = 1, + type = "description", + }, + __new__ = { + name = L["New State..."], + order = 2, + type = "group", + args = { + name = { + name = L["State Name"], + desc = L["Set a name for the new state"], + order = 1, + type = "input", + 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"], + }, + create = { + name = L["Create State"], + order = 2, + type = "execute", + func = function () + local name = private.newstatename + if states[name] then + ReAction:UserError(L["State named '%s' already exists"]:format(name)) + else + -- TODO: select default state options and pass as final argument + states[name] = { } + optionMap[bar].args[name] = CreateStateOptions(bar,name) + private.newstatename = "" + end + end, + disabled = function() + local name = private.newstatename or "" + return #name == 0 or name:find("%W") + end, + } + } + } + } + } + local states = tfetch(module.db.profile.bars, bar:GetName(), "states") + if states then + for name, config in pairs(states) do + options.args[name] = CreateStateOptions(bar,name) + end + end + optionMap[bar] = options + return options + end +end + +function module:GetBarOptions(bar) + return CreateBarOptions(bar) +end