--[[ 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= where is any whole number -- returns in return["name"] -- name=- 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=[ .... ] where number is a whole number. -- returns the table with all the numbers in return["name"] -- name='' 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