Fix stupid bug introduced with the new debugging messages. switch to kind_of? instead...
[rbot] / lib / rbot / timer.rb
1 module Timer
2
3   # timer event, something to do and when/how often to do it
4   class Action
5     
6     # when this action is due next (updated by tick())
7     attr_reader :in
8     
9     # is this action blocked? if so it won't be run
10     attr_accessor :blocked
11
12     # period:: how often (seconds) to run the action
13     # data::   optional data to pass to the proc
14     # once::   optional, if true, this action will be run once then removed
15     # func::   associate a block to be called to perform the action
16     # 
17     # create a new action
18     def initialize(period, data=nil, once=false, &func)
19       @blocked = false
20       @period = period
21       @in = period
22       @func = func
23       @data = data
24       @once = once
25       @last_tick = Time.new
26     end
27
28     def tick
29       diff = Time.new - @last_tick
30       @in -= diff
31       @last_tick = Time.new
32     end
33
34     def inspect 
35       "#<#{self.class}:#{@period}s:#{@once ? 'once' : 'repeat'}>"
36     end
37
38     def due?
39       @in <= 0
40     end
41
42     # run the action by calling its proc
43     def run
44       @in += @period
45       # really short duration timers can overrun and leave @in negative,
46       # for these we set @in to @period
47       @in = @period if @in <= 0
48       if(@data)
49         @func.call(@data)
50       else
51         @func.call
52       end
53       return @once
54     end
55   end
56   
57   # timer handler, manage multiple Action objects, calling them when required.
58   # The timer must be ticked by whatever controls it, i.e. regular calls to
59   # tick() at whatever granularity suits your application's needs.
60   # 
61   # Alternatively you can call run(), and the timer will spawn a thread and
62   # tick itself, intelligently shutting down the thread if there are no
63   # pending actions.
64   class Timer
65     def initialize(granularity = 0.1)
66       @granularity = granularity
67       @timers = Hash.new
68       @handle = 0
69       @lasttime = 0
70       @should_be_running = false
71       @thread = false
72       @next_action_time = 0
73     end
74     
75     # period:: how often (seconds) to run the action
76     # data::   optional data to pass to the action's proc
77     # func::   associate a block with add() to perform the action
78     # 
79     # add an action to the timer
80     def add(period, data=nil, &func)
81       debug "adding timer, period #{period}"
82       @handle += 1
83       @timers[@handle] = Action.new(period, data, &func)
84       start_on_add
85       return @handle
86     end
87
88     # period:: how long (seconds) until the action is run
89     # data::   optional data to pass to the action's proc
90     # func::   associate a block with add() to perform the action
91     # 
92     # add an action to the timer which will be run just once, after +period+
93     def add_once(period, data=nil, &func)
94       debug "adding one-off timer, period #{period}"
95       @handle += 1
96       @timers[@handle] = Action.new(period, data, true, &func)
97       start_on_add
98       return @handle
99     end
100
101     # remove action with handle +handle+ from the timer
102     def remove(handle)
103       @timers.delete(handle)
104     end
105     
106     # block action with handle +handle+
107     def block(handle)
108       @timers[handle].blocked = true
109     end
110
111     # unblock action with handle +handle+
112     def unblock(handle)
113       @timers[handle].blocked = false
114     end
115
116     # you can call this when you know you're idle, or you can split off a
117     # thread and call the run() method to do it for you.
118     def tick 
119       if(@lasttime == 0)
120         # don't do anything on the first tick
121         @lasttime = Time.now
122         return
123       end
124       @next_action_time = 0
125       diff = (Time.now - @lasttime).to_f
126       @lasttime = Time.now
127       @timers.each { |key,timer|
128         timer.tick
129         next if timer.blocked
130         if(timer.due?)
131           if(timer.run)
132             # run once
133             @timers.delete(key)
134           end
135         end
136         if @next_action_time == 0 || timer.in < @next_action_time
137           @next_action_time = timer.in
138         end
139       }
140       #debug "ticked. now #{@timers.length} timers remain"
141       #debug "next timer due at #{@next_action_time}"
142     end
143
144     # for backwards compat - this is a bit primitive
145     def run(granularity=0.1)
146       while(true)
147         sleep(granularity)
148         tick
149       end
150     end
151
152     def running?
153       @thread && @thread.alive?
154     end
155
156     # return the number of seconds until the next action is due, or 0 if
157     # none are outstanding - will only be accurate immediately after a
158     # tick()
159     def next_action_time
160       @next_action_time
161     end
162
163     # start the timer, it spawns a thread to tick the timer, intelligently
164     # shutting down if no events remain and starting again when needed.
165     def start
166       return if running?
167       @should_be_running = true
168       start_thread unless @timers.empty?
169     end
170
171     # stop the timer from ticking
172     def stop
173       @should_be_running = false
174       stop_thread
175     end
176     
177     private
178     
179     def start_on_add
180       if running?
181         stop_thread
182         start_thread
183       elsif @should_be_running
184         start_thread
185       end
186     end
187     
188     def stop_thread
189       return unless running?
190       @thread.kill
191     end
192     
193     def start_thread
194       return if running?
195       @thread = Thread.new do
196         while(true)
197           tick
198           exit if @timers.empty?
199           sleep(@next_action_time)
200         end
201       end
202     end
203
204   end
205 end