vanilla-wow-addons – Rev 1

Subversion Repositories:
Rev:
--[[ 
        
        Utility Class Version           1.02
        Command Class Version   1.03
        Timer Class Version             1.03

        Mairelon's Utility Classes
        Place me in your mod directory.
        In your XML file, load me prior to any scripts.
        
        Version checking occurs - if there is a later
        version of one of these classes alread available,
        it won't overwrite it.

        Utility_Class just provides 2 methods at the 
        moment (And isn't technically an object as there is no
        state maintained).  Echo displays a message over your
        character's head, Print displays the message in the
        chat box.
        
        Command_Class simplifies the addition of sophisticated
        slash commands.  It replaces large if-then-elseif-else-end
        constructs with a callback function system.  It also 
        encapsulates basic usage messages and limited parameter
        parsing.
        
        Timer_Class encapsulates a basic timer, capable of displaying
        end of timer messages, playing sounds or calling a callback.
        Timers can be one-shot or recurring, be paused, restarted and
        reset.
        
        Last Modified
                01/13/2005
                        Fixed mac crash in Timer_Class
                01/16/2005
                        added parameter checking to command class
                03/31/2005
                        added ability to specify command text to command class
                02/06/2005
                        Added stack class 1.0
                04/14/2005
                        Fixed bug in timer class that was eating memory
                08/21/2005
                        Fixed potential bug - %%qt changed to %&qt in GetParameters for consistancy
--]]
-- Class declarations

-- Utility class provides print (to the chat box) and echo (displays over your character's head).
-- Instantiate it and use the colon syntax.
-- Color is an optional argument.  You can either use one of 7 named colors
-- "red", "green", "blue", "yellow", "cyan", "magenta", "white" or
-- a table with the r, g, b values.
-- IE foo:Print("some text", {r = 1.0, g=1.0, b=.5})

-- Version 1.02 has a new table copy function.

-- Since the class is global, ensure that we have the latest version available.  If you make changes
-- to this class RENAME IT.  Do not change the interface and leave the name the same, as that 
-- can break other mods using the class.
if not Utility_Class or (not Utility_Class.version) or (Utility_Class.version < 1.02) then
        Utility_Class = {};
        Utility_Class.version = 1.02
        function Utility_Class:New ()
                local o = {}   -- create object
                setmetatable(o, self)
                self.__index = self
                return o
        end
        
        function Utility_Class:Print(msg, color)
        -- the work for these is done in get-color, otherwise it's just adding
        -- the text to the chat frame.
        if msg == nil then return end;
        local r, g, b;
                if msg == nil then return; end
                if color == nil then color = "white"; end
                r, g, b = self:GetColor(color);
                
                if( DEFAULT_CHAT_FRAME ) then
                        DEFAULT_CHAT_FRAME:AddMessage(msg,r,g,b);
                end
                
        end
        
        function Utility_Class:Echo(msg, color)
        -- the work for these is done in get-color, otherwise it's just adding
        -- the text to the UIERRORS frame.
        if msg == nil then return end;
        local r, g, b;
                if msg == nil then return; end
                if color == nil then color = "white"; end
                r, g, b = self:GetColor(color);
                
                UIErrorsFrame:AddMessage(msg, r, g, b, 1.0, UIERRORS_HOLD_TIME);
                
        end
        
        function Utility_Class:GetColor(color)
        -- Turn any string color into its rgb, check any table arg for
        -- being in-bounds, return the appropriate RGB values.
                if color == nil then color = self end
                if color == nil then return 1, 1, 1 end
        
                if type(color) == "string" then 
                        color = Utility_Class.ColorList[string.lower(color)];
                end
                
                if type(color) == "table" then
                        if color.r == nil then color.r = 0.0 end
                        if color.g == nil then color.g = 0.0 end
                        if color.b == nil then color.b = 0.0 end
                else
                        return 1, 1, 1 
                end
        
                if color.r < 0 then color.r = 0.0 end
                if color.g < 0 then color.g = 0.0 end
                if color.b < 0 then color.g = 0.0 end
        
                if color.r > 1 then color.r = 1.0 end
                if color.g > 1 then color.g = 1.0 end
                if color.b > 1 then color.g = 1.0 end
                
                return color.r, color.g, color.b
                
        end
        
        -- Straight forward list of primary/complement colors and their r, g, b values.
        Utility_Class.ColorList = {}
        Utility_Class.ColorList["red"] = { r = 1.0, g = 0.0, b = 0.0 }
        Utility_Class.ColorList["green"] = { r = 0.0, g = 1.0, b = 0.0 }
        Utility_Class.ColorList["blue"] = { r = 0.0, g = 0.0, b = 1.0 }
        Utility_Class.ColorList["white"] = { r = 1.0, g = 1.0, b = 1.0 }
        Utility_Class.ColorList["magenta"] = { r = 1.0, g = 0.0, b = 1.0 }
        Utility_Class.ColorList["yellow"] = { r = 1.0, g = 1.0, b = 0.0 }
        Utility_Class.ColorList["cyan"] = { r = 0.0, g = 1.0, b = 1.0 }

-- Recursive table copy function.  Copies by value
        function Utility_Class:TableCopy(table1)
        local table2 = {};
        if table1 == nil then return table2 end
        local index, value
                for index, value in pairs(table1) do
                local text = index .. " "
                        if type(value) == "table" then
                                table2[index] = Utility_Class:TableCopy(value)
                        else
                                table2[index] = value
                        end
                end
                return table2;
        end

end


-- Command_Class provides an easy way to handle slash commands and their associated
-- sub-commands, parameters and usage statements.
-- Instantiate a Command_Class variable with Command_Class:New(modname) instead of
-- SlashCmdList[modname] = Slash_Command_Callback.
-- After that, use :AddCommand("command", callback, "group", "usage") to add new
-- sub-commands.

-- If the command on the command line does not match one of the added commands, 
-- displayusage() will be called for the main group.  Use seperate groups to partition
-- your usage statements into manageable sets.

-- Version 1.01 - fixed error in dispatch code.
-- Version 1.02 - add parameter checking methods
-- Version 1.03 - added command text

-- Since the class is global, ensure that we have the latest version available.  If you make changes
-- to this class RENAME IT.  Do not change the interface and leave the name the same, as that 
-- can break other mods using the class.

if (not Command_Class) or (not Command_Class.version) or (Command_Class.version < 1.03) then
    Command_Class = {}
    Command_Class.version=1.03
    
--  Instantiate the new object  - for more on OO in Lua see the book
--  Programming in Lua at Lua.org.
    function Command_Class:New(modname,command)
                if (modname == nil) then return nil end
                if (command == nil) then command = "/" .. string.lower(modname) end
                local o = {}
        o.CommandList = {}
        o.UsageList = {}
                o.UsageList.n=0
        o.GroupList = {}
                o.ParamList = {}
                SlashCmdList[modname .. "COMMAND"] = function(msg) o:Dispatch(msg) end
                setglobal("SLASH_" .. modname .. "COMMAND1", command)
        setmetatable(o, self)
        self.__index = self
        return o
    end

--  This the instance method that will be assigned to the slash command.
--  this replaces the huge if/then/elseif structures common to my mods so far.    
--  msg             is the text passed to the command by the WoW environment
--  command             is the text of the command we are looking for
--  dispatch            is the function to call on that command.
    function Command_Class:Dispatch(msg)
    --  first, pull off the first token to compare against the command text
                if msg == nil then msg = "" end
                local token
        local firsti, lasti = string.find(msg, " ")
        if firsti then
            token = string.sub(msg, 1, firsti-1)
        else
            token = msg
        end
    -- the command function will expect the remainder of msg as an argument
        if lasti then
            msg = string.sub(msg, lasti+1)
        else
            msg = ""
        end
    -- ensure it gets the empty string rather than nil
        if msg == nil then msg = "" end;
    --  if command exists - dispatch it.
                if self.CommandList[string.lower(token)] then
                        local dispatch = self.CommandList[string.lower(token)]
                        dispatch(msg)
                else
                --  no match found - display the usage message.
                        self:DisplayUsage()
                end
    end

--  Method to display the optional usage message attached to the command
--  if no group is specified, the main group usage is displayed.  To partition
--  large command lists, seperate them into groups and add commands to 
--  display non-main groups.
    function Command_Class:DisplayUsage(group)
    --  Simply iterate through and display the usage line for each command in group
                local util = Utility_Class:New()
        local index, usage
        if group == nil then group = "main" end
        for index, usage in ipairs(self.UsageList) do
            if self.GroupList[index] == group then
                util:Print(self.UsageList[index])
            end
        end
    end

--  Method to add commands to the recoginized command list.
--  Arguments are:
--  command:    The text we are searching for in the slash command line 
--                      (case insensitive)
--  dispatch:      The name of the function that will handle this command
--  group:           Optional (will default to main) used in display usage
--  usage           Optional (will default to command) used in display usage
    function Command_Class:AddCommand(command, dispatch, group, usage)
        self.CommandList[command]=dispatch
        if usage == nil then usage =command end
                self.UsageList.n = self.UsageList.n + 1
        self.UsageList[self.UsageList.n]=usage
        if group == nil then group = "main" end
        group = string.lower(group)
        self.GroupList[self.UsageList.n]=group
                self.ParamList[command]={}
    end

-- Method to parse msg for name=value pairs
-- handles the following types:
-- name=<number>  where <number> is any whole number
-- returns <number> in return["name"]

-- name=<number>-<number> where number1 and number2 are any whole number
-- returns a table in return["name"] containing all the numbers from
-- number1 to number 2 (inclusive)

-- name=[<number> <number> .... <number>]  where number is a whole number.
-- returns the table with all the numbers in return["name"]

-- name='<string with spaces>'  returns the arbitrary string in return["name"], all
-- characters allowed except '


        function Command_Class:GetParameters(msg)
        if msg == nil then return {} end
        -- pattern to return name=
        msg = string.gsub(msg,"\\'","%&qt")
        local pattern = "([%a_][%w_]*)="
        local index = 1
        local params = {}
                local firsti, lasti, capture = string.find(msg,pattern)
                -- while we have a name=, process the info after it.
                while capture do
                        local varname=string.lower(capture)
                        index = index+lasti
                        firsti, lasti, capture = string.find(string.sub(msg, index),pattern)
                        if not firsti then firsti=string.len(msg) else firsti=firsti-2 end
                        local str = string.sub(msg,index, index+firsti)
                        -- if the start is ', we have a string
                        if string.sub(str,1,1) == "'" then
                                local location = string.find(string.sub(str,2),"'")
                                if location and (location>1) then
                                        params[varname]=string.gsub(string.sub(str,2,location),"%&qt","'")
                                end
                        -- we have a table
                        elseif string.sub(str,1,1) == "[" then
                                local table1={}
                                local element
                                local index = 1
                                for element in string.gfind(str,"'([^']+)'") do
                                        table1[index] = string.gsub(element,"%&qt","'")
                                        index = index+1
                                end
                                if not (index > 1) then
                                -- no strings in table - look for numbers
                                        for element in string.gfind(str,"([-]?%d+)") do
                                                table1[index] = element+0
                                                index = index+1
                                        end
                                end
                                if index > 1 then
                                        params[varname] = table1
                                end
                        -- we have a range of values
                        elseif string.find(str,"([-]?%d+)-([-]?%d+)") then
                                local firsti, lasti, startrange, endrange = string.find(str,"([-]?%d+)-([-]?%d+)")
                                local index = 1
                                local table = {}
                                local value;
                                if firsti then
                                        for value = startrange+0, endrange+0 do
                                                table[index]=value
                                                index=index+1
                                        end
                                end
                                if index>1 then
                                        params[varname]=table
                                end
                        -- Not a string, range or table, so extract a number from it
                        else
                                local firsti, lasti, value = string.find(str,"([-]?%d+)")
                                if value then
                                        params[varname]=value+0
                                end
                        end
                end
                return params
        end     
        
-- Method to add a parameter to a command for automatic checking.
-- Inputs:
-- command      = the command this parameter is for (string)
-- param                = the parameter name (string)
-- required     = whether the parameter is required or optional (true or nil)
-- strict               = if strict is true, error if values are not in allowed values and return false
--                      = otherwise show a warning on values not in allowed range and return true
-- types                = what types are allowed for this param - needs to be a table of strings.
--                      if "string" is in the table then type string is allowed
--                      if "number" is in the table then type number is allowed
--                      if "table" is in the table then tables are allowed BUT - if either of the
--                      previous 2 are specified, only tables of that type.
--                      EG      {"string"}  allows only strings
--                              {"number"} allows only numbers
--                              {"table"} allows only tables (no restriction on contents)
--                              {"string" "table"} allows strings or tables of strings
--                              {"number" "table"} allows numbers or tables of numbers
--                              {"number" "string" "table"} allows  any value my command class can parse,
--                              equivalent to {} or nil
-- allowed              Values that are allowed for this parameter.  Must be a table.
--                      if lowerbound or upperbound are set, it will check numeric values against them:
--                      eg { lowerbound=5, upperbound=10 } will only allow values that are >= 5 and <=10
--                      but will be ignored if the value is non-numeric.  Enforce that with types
--                      if lowerbound and upperbound are both omitted, it is simply a list of allowed values
--                      eg { 'true', 'false' } will only allow those 2 strings and IS case sensitive
-- default              default value to assign in the case this item is optional and nil

        function Command_Class:AddParam(command, param, required, strict, types, allowed, default)
                if not self.CommandList[command] then return end
                self.ParamList[command][param] = {}
                local p = self.ParamList[command][param]
                
                p["required"] = required
                p["strict"] = strict
                p["types"] = types
                p["default"] = default
                p["allowed"] = allowed
        end

-- Method to actually verify the params against the paramlist
-- return true if they are valid, false otherwise
        function Command_Class:CheckParameters(command, args)
                local util = Utility_Class:New()
                local plist = self.ParamList[command]
                local index, param
                -- first check for existence.  If required and nil, raise error
                -- if optional and nil assign default value
                for index, param in pairs(plist) do
                        if not args[index] and param["required"] then
                                util:Print("Error: " .. index .. " is required")
                                return false
                        elseif not args[index] and not param["required"] then
                                args[index] = param["default"]
                        end
                        
                        -- next - check types
                        if param["types"] and args[index] then
                                local types={}  -- used to simplify type checking
                                local index2
                                for index2=1,3 do
                                        if param["types"][index2] then
                                                types[param["types"][index2]]=true
                                        end
                                end
                                
                                -- basic type checking 
                                if not types[type(args[index])] then 
                                        util:Print("Error: " .. index .. " type mismatch " .. type(args[index]) .. " not allowed")
                                        return false
                                end
                                
                                -- if the argument was a table, and tables are allowed then check each value in the table.
                                -- since tables of tables will not be parsed by Command_Class we can explicitly check one level down
                                -- without needing recursion
                                if type(args[index]) == "table" then
                                        local index3, value
                                        for index3, value in ipairs(args[index]) do
                                                if not types[type(value)] then 
                                                        util:Print("Error: " .. index .. " type mismatch " .. type(value) .. " not allowed")
                                                        return false
                                                end
                                        end
                                end
                        end

                        -- if allowed values were specified, check against them
                        if param["allowed"] and (args[index]) then
                                local vals = param["allowed"]
                                -- first, check for lower and upper bounds
                                if vals.lowerbound or vals.upperbound then
                                        -- simple bounds checking for non-table types
                                        if type(args[index]) == "number" then
                                                if vals.lowerbound and args[index] < vals.lowerbound then 
                                                        if param["strict"] then
                                                                util:Print("Error: " .. index .. " must be higher than (or equal to)" .. vals.lowerbound)
                                                        else
                                                                util:Print("Warning: " .. index .. " should be higher than (or equal to)" .. vals.lowerbound)
                                                        end
                                                elseif  vals.upperbound and args[index] > vals.upperbound then
                                                        if param["strict"] then
                                                                util:Print("Error: " .. index .. " must be lower than (or equal to)" .. vals.upperbound)
                                                                return false
                                                        else
                                                                util:Print("Warning: " .. index .. " should be lower than (or equal to) " .. vals.upperbound) 
                                                        end
                                                end
                                        -- bounds checking for each individual value for table types
                                        elseif (type(args[index]) == "table" and type(args[index][1]) == "number") then
                                                local index5, value
                                                for index5, value in ipairs(args[index]) do
                                                        if vals.lowerbound and value < vals.lowerbound then 
                                                                if param["strict"] then
                                                                        util:Print("Error: " .. index .. " must be higher than (or equal to)" .. vals.lowerbound)
                                                                else
                                                                        util:Print("Warning: " .. index .. " should be higher than (or equal to)" .. vals.lowerbound)
                                                                end
                                                        elseif  vals.upperbound and value > vals.upperbound then
                                                                if param["strict"] then
                                                                        util:Print("Error: " .. index .. " must be lower than (or equal to)" .. vals.upperbound)
                                                                        return false
                                                                else
                                                                        util:Print("Warning: " .. index .. " should be lower than (or equal to) " .. vals.upperbound) 
                                                                end
                                                        end
                                                end
                                        end
                                else
                                        -- without bounds, we just check against each individual element of allowed
                                        local valid = false
                                        -- check arg against each element in allowed
                                        if type(args[index]) ~= "table" then
                                                local index6, allowedvalue
                                                for index6, allowedvalue in ipairs(vals) do
                                                        if args[index] == allowedvalue then
                                                                valid = true
                                                        end
                                                end
                                        else
                                        -- if it's a table, we compare each element of the table against the elements of allowed
                                                local index7, value                                             
                                                for index7, value in ipairs(args[index]) do
                                                        local index6, allowedvalue
                                                        for index6, allowedvalue in ipairs(vals) do
                                                                if value == allowedvalue then
                                                                        valid = true
                                                                end
                                                        end
                                                end
                                        end
                                        if (not valid) and param["strict"] then
                                                util:Print("Error: " .. index .. " not in range of allowed values")
                                                return false
                                        elseif (not valid) then
                                                util:Print("Warning: " .. index .. " has an unrecognized value -- errors may result")
                                        end
                                end
                        end
                end
                return true
        end
end

-- A simple timer class.  Timers can be one shot or recurring, they can be paused, reset and restarted
-- Timers can echo a message over head, play a sound file or execute a function at the end of their
-- run

-- Version 1.01 fixed Mac crash.
-- Version 1.03 altered code so the timer is disposed of at the end of it's run

-- Since the class is global, ensure that we have the latest version available.  If you make changes
-- to this class RENAME IT.  Do not change the interface and leave the name the same, as that 
-- can break other mods using the class.

if (not Timer_Class) or (not Timer_Class.version) or (Timer_Class.version < 1.03) then
    Timer_Class = {}
    Timer_Class.version=1.03
        Timer_Class.Util = Utility_Class:New()
-- Create a new timer.  Arguments are:
-- duration     How long (in seconds) the timer should run)
-- recurring    Whether the timer should reset after finishing
-- message      What message to display overhead at the end of the timer
-- sound                Soundfile to play at the end of the timer*****
--              ****    The soundfile MUST be in your World of Warcraft/Data directory BEFORE UI load
-- callback     A function to be called at the end of the timer.

-- Duration must exist and be non-negative, or it returns nil
-- at least one of message, sound, callback must be non-nil
-- the timer defaults to running.  If you need it paused, call timer:Pause() after creating it.

        function Timer_Class:New (duration, recurring, message, sound, callback)
                if duration == nil or type(duration) ~= "number" or duration < 0 then return nil end
                if message == nil and sound == nil and callback == nil then return nil end
--              if message == nil then message = ""; end
--              if sound == nil then sound = ""; end
--              if callback == nil then callback = function() end end
                local o = {}   -- create object
                setmetatable(o, self)
                self.__index = self
                o.duration = duration
                o.message = message
                o.recurring = recurring
                o.sound = sound
                o.callback = callback
                o.running = true
                o.currenttime = o.duration
                return o
        end
        
-- Method to call in the mod's OnUpdate function.  Pass OnUpdate's arg1 to the method.
-- The timer is disposed of when it finishes it's run

        function Timer_Class:Update(elapsed)
                if self.running then
                        self.currenttime = self.currenttime - elapsed
                        if self.currenttime <= 0 then
                                if self.recurring then
                                        self.currenttime = self.duration
                                else
                                        self.running = false
                                end
                                if self.message then Timer_Class.Util:Echo(self.message) end
                                if self.sound then PlaySoundFile(self.sound) end
                                if self.callback then self.callback() end
                        end
                end
        end     

-- Helper methods to pause a running timer, start a paused timer and reset the timer.   
        function Timer_Class:Pause()
                self.running = nil
        end
        
        function Timer_Class:Start()
                self.running = true;
        end
        
        function Timer_Class:Reset()
                self.currenttime = self.duration
        end

-- Helper methods to return time currently left on the timer, max duration and running state.
        function Timer_Class:GetTimeLeft()
                return self.currenttime;
        end
        
        function Timer_Class:GetDuration()
                return self.duration
        end
        
        function Timer_Class:GetRunning()
                if self.running then return true else return false end
        end
end


-- A simple Stack class.  

-- Since the class is global, ensure that we have the latest version available.  If you make changes
-- to this class RENAME IT.  Do not change the interface and leave the name the same, as that 
-- can break other mods using the class.

if (not Stack_Class) or (not Stack_Class.version) or (Stack_Class.version < 1.02) then
    Stack_Class = {}
    Stack_Class.version=1.02

        function Stack_Class:New ()
                local o = {}   -- create object
                setmetatable(o, self)
                self.__index = self
                o.n = 0
                return o
        end
        
        function Stack_Class:IsEmpty()
                if self.n == 0 then
                        return true
                else
                        return false
                end
        end
        
        function Stack_Class:Top()
                if not self:IsEmpty() then
                        return self[self.n]
                else
                        return nil
                end
        end
        
        function Stack_Class:Pop()
                if not self:IsEmpty() then
                        local value = self:Top()
                        self.n = self.n - 1
                        return value
                else
                        return nil
                end
        end
        
        function Stack_Class:Push(value)
                if value ~= nil then
                        self.n = self.n + 1
                        self[self.n] = value
                end
        end
end