vanilla-wow-addons – Blame information for rev 1

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 --[[
2  
3 Utility Class Version 1.02
4 Command Class Version 1.03
5 Timer Class Version 1.03
6  
7 Mairelon's Utility Classes
8 Place me in your mod directory.
9 In your XML file, load me prior to any scripts.
10  
11 Version checking occurs - if there is a later
12 version of one of these classes alread available,
13 it won't overwrite it.
14  
15 Utility_Class just provides 2 methods at the
16 moment (And isn't technically an object as there is no
17 state maintained). Echo displays a message over your
18 character's head, Print displays the message in the
19 chat box.
20  
21 Command_Class simplifies the addition of sophisticated
22 slash commands. It replaces large if-then-elseif-else-end
23 constructs with a callback function system. It also
24 encapsulates basic usage messages and limited parameter
25 parsing.
26  
27 Timer_Class encapsulates a basic timer, capable of displaying
28 end of timer messages, playing sounds or calling a callback.
29 Timers can be one-shot or recurring, be paused, restarted and
30 reset.
31  
32 Last Modified
33 01/13/2005
34 Fixed mac crash in Timer_Class
35 01/16/2005
36 added parameter checking to command class
37 03/31/2005
38 added ability to specify command text to command class
39 02/06/2005
40 Added stack class 1.0
41 04/14/2005
42 Fixed bug in timer class that was eating memory
43 08/21/2005
44 Fixed potential bug - %%qt changed to %&qt in GetParameters for consistancy
45 --]]
46 -- Class declarations
47  
48 -- Utility class provides print (to the chat box) and echo (displays over your character's head).
49 -- Instantiate it and use the colon syntax.
50 -- Color is an optional argument. You can either use one of 7 named colors
51 -- "red", "green", "blue", "yellow", "cyan", "magenta", "white" or
52 -- a table with the r, g, b values.
53 -- IE foo:Print("some text", {r = 1.0, g=1.0, b=.5})
54  
55 -- Version 1.02 has a new table copy function.
56  
57 -- Since the class is global, ensure that we have the latest version available. If you make changes
58 -- to this class RENAME IT. Do not change the interface and leave the name the same, as that
59 -- can break other mods using the class.
60 if not Utility_Class or (not Utility_Class.version) or (Utility_Class.version < 1.02) then
61 Utility_Class = {};
62 Utility_Class.version = 1.02
63 function Utility_Class:New ()
64 local o = {} -- create object
65 setmetatable(o, self)
66 self.__index = self
67 return o
68 end
69  
70 function Utility_Class:Print(msg, color)
71 -- the work for these is done in get-color, otherwise it's just adding
72 -- the text to the chat frame.
73 if msg == nil then return end;
74 local r, g, b;
75 if msg == nil then return; end
76 if color == nil then color = "white"; end
77 r, g, b = self:GetColor(color);
78  
79 if( DEFAULT_CHAT_FRAME ) then
80 DEFAULT_CHAT_FRAME:AddMessage(msg,r,g,b);
81 end
82  
83 end
84  
85 function Utility_Class:Echo(msg, color)
86 -- the work for these is done in get-color, otherwise it's just adding
87 -- the text to the UIERRORS frame.
88 if msg == nil then return end;
89 local r, g, b;
90 if msg == nil then return; end
91 if color == nil then color = "white"; end
92 r, g, b = self:GetColor(color);
93  
94 UIErrorsFrame:AddMessage(msg, r, g, b, 1.0, UIERRORS_HOLD_TIME);
95  
96 end
97  
98 function Utility_Class:GetColor(color)
99 -- Turn any string color into its rgb, check any table arg for
100 -- being in-bounds, return the appropriate RGB values.
101 if color == nil then color = self end
102 if color == nil then return 1, 1, 1 end
103  
104 if type(color) == "string" then
105 color = Utility_Class.ColorList[string.lower(color)];
106 end
107  
108 if type(color) == "table" then
109 if color.r == nil then color.r = 0.0 end
110 if color.g == nil then color.g = 0.0 end
111 if color.b == nil then color.b = 0.0 end
112 else
113 return 1, 1, 1
114 end
115  
116 if color.r < 0 then color.r = 0.0 end
117 if color.g < 0 then color.g = 0.0 end
118 if color.b < 0 then color.g = 0.0 end
119  
120 if color.r > 1 then color.r = 1.0 end
121 if color.g > 1 then color.g = 1.0 end
122 if color.b > 1 then color.g = 1.0 end
123  
124 return color.r, color.g, color.b
125  
126 end
127  
128 -- Straight forward list of primary/complement colors and their r, g, b values.
129 Utility_Class.ColorList = {}
130 Utility_Class.ColorList["red"] = { r = 1.0, g = 0.0, b = 0.0 }
131 Utility_Class.ColorList["green"] = { r = 0.0, g = 1.0, b = 0.0 }
132 Utility_Class.ColorList["blue"] = { r = 0.0, g = 0.0, b = 1.0 }
133 Utility_Class.ColorList["white"] = { r = 1.0, g = 1.0, b = 1.0 }
134 Utility_Class.ColorList["magenta"] = { r = 1.0, g = 0.0, b = 1.0 }
135 Utility_Class.ColorList["yellow"] = { r = 1.0, g = 1.0, b = 0.0 }
136 Utility_Class.ColorList["cyan"] = { r = 0.0, g = 1.0, b = 1.0 }
137  
138 -- Recursive table copy function. Copies by value
139 function Utility_Class:TableCopy(table1)
140 local table2 = {};
141 if table1 == nil then return table2 end
142 local index, value
143 for index, value in pairs(table1) do
144 local text = index .. " "
145 if type(value) == "table" then
146 table2[index] = Utility_Class:TableCopy(value)
147 else
148 table2[index] = value
149 end
150 end
151 return table2;
152 end
153  
154 end
155  
156  
157 -- Command_Class provides an easy way to handle slash commands and their associated
158 -- sub-commands, parameters and usage statements.
159 -- Instantiate a Command_Class variable with Command_Class:New(modname) instead of
160 -- SlashCmdList[modname] = Slash_Command_Callback.
161 -- After that, use :AddCommand("command", callback, "group", "usage") to add new
162 -- sub-commands.
163  
164 -- If the command on the command line does not match one of the added commands,
165 -- displayusage() will be called for the main group. Use seperate groups to partition
166 -- your usage statements into manageable sets.
167  
168 -- Version 1.01 - fixed error in dispatch code.
169 -- Version 1.02 - add parameter checking methods
170 -- Version 1.03 - added command text
171  
172 -- Since the class is global, ensure that we have the latest version available. If you make changes
173 -- to this class RENAME IT. Do not change the interface and leave the name the same, as that
174 -- can break other mods using the class.
175  
176 if (not Command_Class) or (not Command_Class.version) or (Command_Class.version < 1.03) then
177 Command_Class = {}
178 Command_Class.version=1.03
179  
180 -- Instantiate the new object - for more on OO in Lua see the book
181 -- Programming in Lua at Lua.org.
182 function Command_Class:New(modname,command)
183 if (modname == nil) then return nil end
184 if (command == nil) then command = "/" .. string.lower(modname) end
185 local o = {}
186 o.CommandList = {}
187 o.UsageList = {}
188 o.UsageList.n=0
189 o.GroupList = {}
190 o.ParamList = {}
191 SlashCmdList[modname .. "COMMAND"] = function(msg) o:Dispatch(msg) end
192 setglobal("SLASH_" .. modname .. "COMMAND1", command)
193 setmetatable(o, self)
194 self.__index = self
195 return o
196 end
197  
198 -- This the instance method that will be assigned to the slash command.
199 -- this replaces the huge if/then/elseif structures common to my mods so far.
200 -- msg is the text passed to the command by the WoW environment
201 -- command is the text of the command we are looking for
202 -- dispatch is the function to call on that command.
203 function Command_Class:Dispatch(msg)
204 -- first, pull off the first token to compare against the command text
205 if msg == nil then msg = "" end
206 local token
207 local firsti, lasti = string.find(msg, " ")
208 if firsti then
209 token = string.sub(msg, 1, firsti-1)
210 else
211 token = msg
212 end
213 -- the command function will expect the remainder of msg as an argument
214 if lasti then
215 msg = string.sub(msg, lasti+1)
216 else
217 msg = ""
218 end
219 -- ensure it gets the empty string rather than nil
220 if msg == nil then msg = "" end;
221 -- if command exists - dispatch it.
222 if self.CommandList[string.lower(token)] then
223 local dispatch = self.CommandList[string.lower(token)]
224 dispatch(msg)
225 else
226 -- no match found - display the usage message.
227 self:DisplayUsage()
228 end
229 end
230  
231 -- Method to display the optional usage message attached to the command
232 -- if no group is specified, the main group usage is displayed. To partition
233 -- large command lists, seperate them into groups and add commands to
234 -- display non-main groups.
235 function Command_Class:DisplayUsage(group)
236 -- Simply iterate through and display the usage line for each command in group
237 local util = Utility_Class:New()
238 local index, usage
239 if group == nil then group = "main" end
240 for index, usage in ipairs(self.UsageList) do
241 if self.GroupList[index] == group then
242 util:Print(self.UsageList[index])
243 end
244 end
245 end
246  
247 -- Method to add commands to the recoginized command list.
248 -- Arguments are:
249 -- command: The text we are searching for in the slash command line
250 -- (case insensitive)
251 -- dispatch: The name of the function that will handle this command
252 -- group: Optional (will default to main) used in display usage
253 -- usage Optional (will default to command) used in display usage
254 function Command_Class:AddCommand(command, dispatch, group, usage)
255 self.CommandList[command]=dispatch
256 if usage == nil then usage =command end
257 self.UsageList.n = self.UsageList.n + 1
258 self.UsageList[self.UsageList.n]=usage
259 if group == nil then group = "main" end
260 group = string.lower(group)
261 self.GroupList[self.UsageList.n]=group
262 self.ParamList[command]={}
263 end
264  
265 -- Method to parse msg for name=value pairs
266 -- handles the following types:
267 -- name=<number> where <number> is any whole number
268 -- returns <number> in return["name"]
269  
270 -- name=<number>-<number> where number1 and number2 are any whole number
271 -- returns a table in return["name"] containing all the numbers from
272 -- number1 to number 2 (inclusive)
273  
274 -- name=[<number> <number> .... <number>] where number is a whole number.
275 -- returns the table with all the numbers in return["name"]
276  
277 -- name='<string with spaces>' returns the arbitrary string in return["name"], all
278 -- characters allowed except '
279  
280  
281 function Command_Class:GetParameters(msg)
282 if msg == nil then return {} end
283 -- pattern to return name=
284 msg = string.gsub(msg,"\\'","%&qt")
285 local pattern = "([%a_][%w_]*)="
286 local index = 1
287 local params = {}
288 local firsti, lasti, capture = string.find(msg,pattern)
289 -- while we have a name=, process the info after it.
290 while capture do
291 local varname=string.lower(capture)
292 index = index+lasti
293 firsti, lasti, capture = string.find(string.sub(msg, index),pattern)
294 if not firsti then firsti=string.len(msg) else firsti=firsti-2 end
295 local str = string.sub(msg,index, index+firsti)
296 -- if the start is ', we have a string
297 if string.sub(str,1,1) == "'" then
298 local location = string.find(string.sub(str,2),"'")
299 if location and (location>1) then
300 params[varname]=string.gsub(string.sub(str,2,location),"%&qt","'")
301 end
302 -- we have a table
303 elseif string.sub(str,1,1) == "[" then
304 local table1={}
305 local element
306 local index = 1
307 for element in string.gfind(str,"'([^']+)'") do
308 table1[index] = string.gsub(element,"%&qt","'")
309 index = index+1
310 end
311 if not (index > 1) then
312 -- no strings in table - look for numbers
313 for element in string.gfind(str,"([-]?%d+)") do
314 table1[index] = element+0
315 index = index+1
316 end
317 end
318 if index > 1 then
319 params[varname] = table1
320 end
321 -- we have a range of values
322 elseif string.find(str,"([-]?%d+)-([-]?%d+)") then
323 local firsti, lasti, startrange, endrange = string.find(str,"([-]?%d+)-([-]?%d+)")
324 local index = 1
325 local table = {}
326 local value;
327 if firsti then
328 for value = startrange+0, endrange+0 do
329 table[index]=value
330 index=index+1
331 end
332 end
333 if index>1 then
334 params[varname]=table
335 end
336 -- Not a string, range or table, so extract a number from it
337 else
338 local firsti, lasti, value = string.find(str,"([-]?%d+)")
339 if value then
340 params[varname]=value+0
341 end
342 end
343 end
344 return params
345 end
346  
347 -- Method to add a parameter to a command for automatic checking.
348 -- Inputs:
349 -- command = the command this parameter is for (string)
350 -- param = the parameter name (string)
351 -- required = whether the parameter is required or optional (true or nil)
352 -- strict = if strict is true, error if values are not in allowed values and return false
353 -- = otherwise show a warning on values not in allowed range and return true
354 -- types = what types are allowed for this param - needs to be a table of strings.
355 -- if "string" is in the table then type string is allowed
356 -- if "number" is in the table then type number is allowed
357 -- if "table" is in the table then tables are allowed BUT - if either of the
358 -- previous 2 are specified, only tables of that type.
359 -- EG {"string"} allows only strings
360 -- {"number"} allows only numbers
361 -- {"table"} allows only tables (no restriction on contents)
362 -- {"string" "table"} allows strings or tables of strings
363 -- {"number" "table"} allows numbers or tables of numbers
364 -- {"number" "string" "table"} allows any value my command class can parse,
365 -- equivalent to {} or nil
366 -- allowed Values that are allowed for this parameter. Must be a table.
367 -- if lowerbound or upperbound are set, it will check numeric values against them:
368 -- eg { lowerbound=5, upperbound=10 } will only allow values that are >= 5 and <=10
369 -- but will be ignored if the value is non-numeric. Enforce that with types
370 -- if lowerbound and upperbound are both omitted, it is simply a list of allowed values
371 -- eg { 'true', 'false' } will only allow those 2 strings and IS case sensitive
372 -- default default value to assign in the case this item is optional and nil
373  
374 function Command_Class:AddParam(command, param, required, strict, types, allowed, default)
375 if not self.CommandList[command] then return end
376 self.ParamList[command][param] = {}
377 local p = self.ParamList[command][param]
378  
379 p["required"] = required
380 p["strict"] = strict
381 p["types"] = types
382 p["default"] = default
383 p["allowed"] = allowed
384 end
385  
386 -- Method to actually verify the params against the paramlist
387 -- return true if they are valid, false otherwise
388 function Command_Class:CheckParameters(command, args)
389 local util = Utility_Class:New()
390 local plist = self.ParamList[command]
391 local index, param
392 -- first check for existence. If required and nil, raise error
393 -- if optional and nil assign default value
394 for index, param in pairs(plist) do
395 if not args[index] and param["required"] then
396 util:Print("Error: " .. index .. " is required")
397 return false
398 elseif not args[index] and not param["required"] then
399 args[index] = param["default"]
400 end
401  
402 -- next - check types
403 if param["types"] and args[index] then
404 local types={} -- used to simplify type checking
405 local index2
406 for index2=1,3 do
407 if param["types"][index2] then
408 types[param["types"][index2]]=true
409 end
410 end
411  
412 -- basic type checking
413 if not types[type(args[index])] then
414 util:Print("Error: " .. index .. " type mismatch " .. type(args[index]) .. " not allowed")
415 return false
416 end
417  
418 -- if the argument was a table, and tables are allowed then check each value in the table.
419 -- since tables of tables will not be parsed by Command_Class we can explicitly check one level down
420 -- without needing recursion
421 if type(args[index]) == "table" then
422 local index3, value
423 for index3, value in ipairs(args[index]) do
424 if not types[type(value)] then
425 util:Print("Error: " .. index .. " type mismatch " .. type(value) .. " not allowed")
426 return false
427 end
428 end
429 end
430 end
431  
432 -- if allowed values were specified, check against them
433 if param["allowed"] and (args[index]) then
434 local vals = param["allowed"]
435 -- first, check for lower and upper bounds
436 if vals.lowerbound or vals.upperbound then
437 -- simple bounds checking for non-table types
438 if type(args[index]) == "number" then
439 if vals.lowerbound and args[index] < vals.lowerbound then
440 if param["strict"] then
441 util:Print("Error: " .. index .. " must be higher than (or equal to)" .. vals.lowerbound)
442 else
443 util:Print("Warning: " .. index .. " should be higher than (or equal to)" .. vals.lowerbound)
444 end
445 elseif vals.upperbound and args[index] > vals.upperbound then
446 if param["strict"] then
447 util:Print("Error: " .. index .. " must be lower than (or equal to)" .. vals.upperbound)
448 return false
449 else
450 util:Print("Warning: " .. index .. " should be lower than (or equal to) " .. vals.upperbound)
451 end
452 end
453 -- bounds checking for each individual value for table types
454 elseif (type(args[index]) == "table" and type(args[index][1]) == "number") then
455 local index5, value
456 for index5, value in ipairs(args[index]) do
457 if vals.lowerbound and value < vals.lowerbound then
458 if param["strict"] then
459 util:Print("Error: " .. index .. " must be higher than (or equal to)" .. vals.lowerbound)
460 else
461 util:Print("Warning: " .. index .. " should be higher than (or equal to)" .. vals.lowerbound)
462 end
463 elseif vals.upperbound and value > vals.upperbound then
464 if param["strict"] then
465 util:Print("Error: " .. index .. " must be lower than (or equal to)" .. vals.upperbound)
466 return false
467 else
468 util:Print("Warning: " .. index .. " should be lower than (or equal to) " .. vals.upperbound)
469 end
470 end
471 end
472 end
473 else
474 -- without bounds, we just check against each individual element of allowed
475 local valid = false
476 -- check arg against each element in allowed
477 if type(args[index]) ~= "table" then
478 local index6, allowedvalue
479 for index6, allowedvalue in ipairs(vals) do
480 if args[index] == allowedvalue then
481 valid = true
482 end
483 end
484 else
485 -- if it's a table, we compare each element of the table against the elements of allowed
486 local index7, value
487 for index7, value in ipairs(args[index]) do
488 local index6, allowedvalue
489 for index6, allowedvalue in ipairs(vals) do
490 if value == allowedvalue then
491 valid = true
492 end
493 end
494 end
495 end
496 if (not valid) and param["strict"] then
497 util:Print("Error: " .. index .. " not in range of allowed values")
498 return false
499 elseif (not valid) then
500 util:Print("Warning: " .. index .. " has an unrecognized value -- errors may result")
501 end
502 end
503 end
504 end
505 return true
506 end
507 end
508  
509 -- A simple timer class. Timers can be one shot or recurring, they can be paused, reset and restarted
510 -- Timers can echo a message over head, play a sound file or execute a function at the end of their
511 -- run
512  
513 -- Version 1.01 fixed Mac crash.
514 -- Version 1.03 altered code so the timer is disposed of at the end of it's run
515  
516 -- Since the class is global, ensure that we have the latest version available. If you make changes
517 -- to this class RENAME IT. Do not change the interface and leave the name the same, as that
518 -- can break other mods using the class.
519  
520 if (not Timer_Class) or (not Timer_Class.version) or (Timer_Class.version < 1.03) then
521 Timer_Class = {}
522 Timer_Class.version=1.03
523 Timer_Class.Util = Utility_Class:New()
524 -- Create a new timer. Arguments are:
525 -- duration How long (in seconds) the timer should run)
526 -- recurring Whether the timer should reset after finishing
527 -- message What message to display overhead at the end of the timer
528 -- sound Soundfile to play at the end of the timer*****
529 -- **** The soundfile MUST be in your World of Warcraft/Data directory BEFORE UI load
530 -- callback A function to be called at the end of the timer.
531  
532 -- Duration must exist and be non-negative, or it returns nil
533 -- at least one of message, sound, callback must be non-nil
534 -- the timer defaults to running. If you need it paused, call timer:Pause() after creating it.
535  
536 function Timer_Class:New (duration, recurring, message, sound, callback)
537 if duration == nil or type(duration) ~= "number" or duration < 0 then return nil end
538 if message == nil and sound == nil and callback == nil then return nil end
539 -- if message == nil then message = ""; end
540 -- if sound == nil then sound = ""; end
541 -- if callback == nil then callback = function() end end
542 local o = {} -- create object
543 setmetatable(o, self)
544 self.__index = self
545 o.duration = duration
546 o.message = message
547 o.recurring = recurring
548 o.sound = sound
549 o.callback = callback
550 o.running = true
551 o.currenttime = o.duration
552 return o
553 end
554  
555 -- Method to call in the mod's OnUpdate function. Pass OnUpdate's arg1 to the method.
556 -- The timer is disposed of when it finishes it's run
557  
558 function Timer_Class:Update(elapsed)
559 if self.running then
560 self.currenttime = self.currenttime - elapsed
561 if self.currenttime <= 0 then
562 if self.recurring then
563 self.currenttime = self.duration
564 else
565 self.running = false
566 end
567 if self.message then Timer_Class.Util:Echo(self.message) end
568 if self.sound then PlaySoundFile(self.sound) end
569 if self.callback then self.callback() end
570 end
571 end
572 end
573  
574 -- Helper methods to pause a running timer, start a paused timer and reset the timer.
575 function Timer_Class:Pause()
576 self.running = nil
577 end
578  
579 function Timer_Class:Start()
580 self.running = true;
581 end
582  
583 function Timer_Class:Reset()
584 self.currenttime = self.duration
585 end
586  
587 -- Helper methods to return time currently left on the timer, max duration and running state.
588 function Timer_Class:GetTimeLeft()
589 return self.currenttime;
590 end
591  
592 function Timer_Class:GetDuration()
593 return self.duration
594 end
595  
596 function Timer_Class:GetRunning()
597 if self.running then return true else return false end
598 end
599 end
600  
601  
602 -- A simple Stack class.
603  
604 -- Since the class is global, ensure that we have the latest version available. If you make changes
605 -- to this class RENAME IT. Do not change the interface and leave the name the same, as that
606 -- can break other mods using the class.
607  
608 if (not Stack_Class) or (not Stack_Class.version) or (Stack_Class.version < 1.02) then
609 Stack_Class = {}
610 Stack_Class.version=1.02
611  
612 function Stack_Class:New ()
613 local o = {} -- create object
614 setmetatable(o, self)
615 self.__index = self
616 o.n = 0
617 return o
618 end
619  
620 function Stack_Class:IsEmpty()
621 if self.n == 0 then
622 return true
623 else
624 return false
625 end
626 end
627  
628 function Stack_Class:Top()
629 if not self:IsEmpty() then
630 return self[self.n]
631 else
632 return nil
633 end
634 end
635  
636 function Stack_Class:Pop()
637 if not self:IsEmpty() then
638 local value = self:Top()
639 self.n = self.n - 1
640 return value
641 else
642 return nil
643 end
644 end
645  
646 function Stack_Class:Push(value)
647 if value ~= nil then
648 self.n = self.n + 1
649 self[self.n] = value
650 end
651 end
652 end