]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/timer.rb
Fix casemap/server mismatch problems when moving the bots between servers with differ...
[user/henk/code/ruby/rbot.git] / 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
56     # reschedule the Action to change its period
57     def reschedule(new_period)
58       @period = new_period
59       @in = new_period
60     end
61   end
62
63   # timer handler, manage multiple Action objects, calling them when required.
64   # The timer must be ticked by whatever controls it, i.e. regular calls to
65   # tick() at whatever granularity suits your application's needs.
66   #
67   # Alternatively you can call run(), and the timer will spawn a thread and
68   # tick itself, intelligently shutting down the thread if there are no
69   # pending actions.
70   class Timer
71     def initialize(granularity = 0.1)
72       @granularity = granularity
73       @timers = Hash.new
74       @handle = 0
75       @lasttime = 0
76       @should_be_running = false
77       @thread = false
78       @next_action_time = 0
79     end
80
81     # period:: how often (seconds) to run the action
82     # data::   optional data to pass to the action's proc
83     # func::   associate a block with add() to perform the action
84     #
85     # add an action to the timer
86     def add(period, data=nil, &func)
87       debug "adding timer, period #{period}"
88       @handle += 1
89       @timers[@handle] = Action.new(period, data, &func)
90       start_on_add
91       return @handle
92     end
93
94     # period:: how long (seconds) until the action is run
95     # data::   optional data to pass to the action's proc
96     # func::   associate a block with add() to perform the action
97     #
98     # add an action to the timer which will be run just once, after +period+
99     def add_once(period, data=nil, &func)
100       debug "adding one-off timer, period #{period}"
101       @handle += 1
102       @timers[@handle] = Action.new(period, data, true, &func)
103       start_on_add
104       return @handle
105     end
106
107     # remove action with handle +handle+ from the timer
108     def remove(handle)
109       @timers.delete(handle)
110     end
111
112     # block action with handle +handle+
113     def block(handle)
114       @timers[handle].blocked = true
115     end
116
117     # unblock action with handle +handle+
118     def unblock(handle)
119       @timers[handle].blocked = false
120     end
121
122     # reschedule action with handle +handle+ to change its period
123     def reschedule(handle, period)
124       @timers[handle].reschedule(period)
125     end
126
127     # you can call this when you know you're idle, or you can split off a
128     # thread and call the run() method to do it for you.
129     def tick
130       if(@lasttime == 0)
131         # don't do anything on the first tick
132         @lasttime = Time.now
133         return
134       end
135       @next_action_time = 0
136       diff = (Time.now - @lasttime).to_f
137       @lasttime = Time.now
138       @timers.each { |key,timer|
139         timer.tick
140         next if timer.blocked
141         if(timer.due?)
142           if(timer.run)
143             # run once
144             @timers.delete(key)
145           end
146         end
147         if @next_action_time == 0 || timer.in < @next_action_time
148           @next_action_time = timer.in
149         end
150       }
151       #debug "ticked. now #{@timers.length} timers remain"
152       #debug "next timer due at #{@next_action_time}"
153     end
154
155     # for backwards compat - this is a bit primitive
156     def run(granularity=0.1)
157       while(true)
158         sleep(granularity)
159         tick
160       end
161     end
162
163     def running?
164       @thread && @thread.alive?
165     end
166
167     # return the number of seconds until the next action is due, or 0 if
168     # none are outstanding - will only be accurate immediately after a
169     # tick()
170     def next_action_time
171       @next_action_time
172     end
173
174     # start the timer, it spawns a thread to tick the timer, intelligently
175     # shutting down if no events remain and starting again when needed.
176     def start
177       return if running?
178       @should_be_running = true
179       start_thread unless @timers.empty?
180     end
181
182     # stop the timer from ticking
183     def stop
184       @should_be_running = false
185       stop_thread
186     end
187
188     private
189
190     def start_on_add
191       if running?
192         stop_thread
193         start_thread
194       elsif @should_be_running
195         start_thread
196       end
197     end
198
199     def stop_thread
200       return unless running?
201       @thread.kill
202     end
203
204     def start_thread
205       return if running?
206       @thread = Thread.new do
207         while(true)
208           tick
209           exit if @timers.empty?
210           sleep(@next_action_time)
211         end
212       end
213     end
214
215   end
216 end