]> git.xonotic.org Git - xonotic/xonotic.git/blob - server/rcon2irc/rcon2irc.pl
Update cvars in mapdownload.txt
[xonotic/xonotic.git] / server / rcon2irc / rcon2irc.pl
1 #!/usr/bin/perl
2
3 our $VERSION = '0.4.2 svn $Revision$';
4
5 # Copyright (c) 2008 Rudolf "divVerent" Polzer
6
7 # Permission is hereby granted, free of charge, to any person
8 # obtaining a copy of this software and associated documentation
9 # files (the "Software"), to deal in the Software without
10 # restriction, including without limitation the rights to use,
11 # copy, modify, merge, publish, distribute, sublicense, and/or sell
12 # copies of the Software, and to permit persons to whom the
13 # Software is furnished to do so, subject to the following
14 # conditions:
15
16 # The above copyright notice and this permission notice shall be
17 # included in all copies or substantial portions of the Software.
18
19 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 # OTHER DEALINGS IN THE SOFTWARE.
27
28 # MISC STRING UTILITY ROUTINES to convert between DarkPlaces and IRC conventions
29
30 # convert mIRC color codes to DP color codes
31 our $color_utf8_enable = 1;
32 our @color_irc2dp_table = (7, 0, 4, 2, 1, 1, 6, 1, 3, 2, 5, 5, 4, 6, 7, 7);
33 our @color_dp2irc_table = (-1, 4, 9, 8, 12, 11, 13, -1, -1, -1); # not accurate, but legible
34 our @color_dp2ansi_table = ("m", "1;31m", "1;32m", "1;33m", "1;34m", "1;36m", "1;35m", "m", "1m", "1m"); # not accurate, but legible
35 our %color_team2dp_table = (5 => 1, 14 => 4, 13 => 3, 10 => 6);
36 our %color_team2irc_table = (5 => 4, 14 => 12, 13 => 8, 10 => 13);
37 sub color_irc2dp($)
38 {
39         my ($message) = @_;
40         $message =~ s/\^/^^/g;
41         my $color = 7;
42         $message =~ s{\003(\d\d?)(?:,(\d?\d?))?|(\017)}{
43                 # $1 is FG, $2 is BG, but let's ignore BG
44                 my $oldcolor = $color;
45                 if($3)
46                 {
47                         $color = 7;
48                 }
49                 else
50                 {
51                         $color = $color_irc2dp_table[$1];
52                         $color = $oldcolor if not defined $color;
53                 }
54                 ($color == $oldcolor) ? '' : '^' . $color;
55         }esg;
56         $message =~ s{[\000-\037]}{}gs; # kill bold etc. for now
57         return $message;
58 }
59
60 our @text_qfont_table = ( # ripped from DP console.c qfont_table
61     '',   '#',  '#',  '#',  '#',  '.',  '#',  '#',
62     '#',  9,    10,   '#',  ' ',  13,   '.',  '.',
63     '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
64     '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
65     ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
66     '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
67     '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
68     '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
69     '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
70     'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
71     'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
72     'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
73     '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
74     'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
75     'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
76     'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<',
77     '<',  '=',  '>',  '#',  '#',  '.',  '#',  '#',
78     '#',  '#',  ' ',  '#',  ' ',  '>',  '.',  '.',
79     '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
80     '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
81     ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
82     '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
83     '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
84     '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
85     '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
86     'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
87     'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
88     'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
89     '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
90     'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
91     'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
92     'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<'
93 );
94 sub text_qfont_table($)
95 {
96         my ($char) = @_;
97         my $o = ord $char;
98         if($color_utf8_enable)
99         {
100                 return (($o & 0xFF00) == 0xE000) ? $text_qfont_table[$o & 0xFF] : $char;
101         }
102         else
103         {
104                 return $text_qfont_table[$o];
105         }
106 }
107 sub text_dp2ascii($)
108 {
109         my ($message) = @_;
110         $message = join '', map { text_qfont_table $_ } split //, $message;
111 }
112
113 sub color_dp_transform(&$)
114 {
115         my ($block, $message) = @_;
116         $message =~ s{(?:(\^\^)|\^x([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])|\^([0-9])|(.))(?=([0-9,]?))}{
117                 defined $1 ? $block->(char => '^', $7) :
118                 defined $2 ? $block->(rgb => [hex $2, hex $3, hex $4], $7) :
119                 defined $5 ? $block->(color => $5, $7) :
120                 defined $6 ? $block->(char => $6, $7) :
121                         die "Invalid match";
122         }esg;
123
124         return $message;
125 }
126
127 sub color_dp2none($)
128 {
129         my ($message) = @_;
130
131         return color_dp_transform
132         {
133                 my ($type, $data, $next) = @_;
134                 $type eq 'char'
135                         ? text_qfont_table $data
136                         : "";
137         }
138         $message;
139 }
140
141 sub color_rgb2basic($)
142 {
143         my ($data) = @_;
144         my ($R, $G, $B) = @$data;
145         my $min = [sort { $a <=> $b } ($R, $G, $B)]->[0];
146         my $max = [sort { $a <=> $b } ($R, $G, $B)]->[-1];
147
148         my $v = $max / 15;
149         my $s = ($max == $min) ? 0 : 1 - $min/$max;
150
151         if($s < 0.2)
152         {
153                 return 0 if $v < 0.5;
154                 return 7;
155         }
156
157         my $h;
158         if($max == $min)
159         {
160                 $h = 0;
161         }
162         elsif($max == $R)
163         {
164                 $h = (60 * ($G - $B) / ($max - $min)) % 360;
165         }
166         elsif($max == $G)
167         {
168                 $h = (60 * ($B - $R) / ($max - $min)) + 120;
169         }
170         elsif($max == $B)
171         {
172                 $h = (60 * ($R - $G) / ($max - $min)) + 240;
173         }
174
175         return 1 if $h < 36;
176         return 3 if $h < 80;
177         return 2 if $h < 150;
178         return 5 if $h < 200;
179         return 4 if $h < 270;
180         return 6 if $h < 330;
181         return 1;
182 }
183
184 sub color_dp_rgb2basic($)
185 {
186         my ($message) = @_;
187         return color_dp_transform
188         {
189                 my ($type, $data, $next) = @_;
190                 $type eq 'char'  ? ($data eq '^' ? '^^' : $data) :
191                 $type eq 'color' ? "^$data" :
192                 $type eq 'rgb'   ? "^" . color_rgb2basic $data :
193                         die "Invalid type";
194         }
195         $message;
196 }
197
198 sub color_dp2irc($)
199 {
200         my ($message) = @_;
201         my $color = -1;
202         return color_dp_transform
203         {
204                 my ($type, $data, $next) = @_;
205
206                 if($type eq 'rgb')
207                 {
208                         $type = 'color';
209                         $data = color_rgb2basic $data;
210                 }
211
212                 $type eq 'char'  ? text_qfont_table $data :
213                 $type eq 'color' ? do {
214                         my $oldcolor = $color;
215                         $color = $color_dp2irc_table[$data];
216
217                         $color == $oldcolor               ? '' :
218                         $color < 0                        ? "\017" :
219                         (index '0123456789,', $next) >= 0 ? "\003$color\002\002" :
220                                                             "\003$color";
221                 } :
222                         die "Invalid type";
223         }
224         $message;
225 }
226
227 sub color_dp2ansi($)
228 {
229         my ($message) = @_;
230         my $color = -1;
231         return color_dp_transform
232         {
233                 my ($type, $data, $next) = @_;
234
235                 if($type eq 'rgb')
236                 {
237                         $type = 'color';
238                         $data = color_rgb2basic $data;
239                 }
240
241                 $type eq 'char'  ? text_qfont_table $data :
242                 $type eq 'color' ? do {
243                         my $oldcolor = $color;
244                         $color = $color_dp2ansi_table[$data];
245
246                         $color eq $oldcolor ? '' :
247                                               "\033[${color}"
248                 } :
249                         die "Invalid type";
250         }
251         $message;
252 }
253
254 sub color_dpfix($)
255 {
256         my ($message) = @_;
257         # if the message ends with an odd number of ^, kill one
258         chop $message if $message =~ /(?:^|[^\^])\^(\^\^)*$/;
259         return $message;
260 }
261
262
263
264
265 # Interfaces:
266 #   Connection:
267 #     $conn->sockname() returns a connection type specific representation
268 #       string of the local address, or undef if not applicable.
269 #     $conn->peername() returns a connection type specific representation
270 #       string of the remote address, or undef if not applicable.
271 #     $conn->send("string") sends something over the connection.
272 #     $conn->recv() receives a string from the connection, or returns "" if no
273 #       data is available.
274 #     $conn->fds() returns all file descriptors used by the connection, so one
275 #       can use select() on them.
276 #   Channel:
277 #     Usually wraps around a connection and implements a command based
278 #     structure over it. It usually is constructed using new
279 #     ChannelType($connection, someparameters...)
280 #     @cmds = $chan->join_commands(@cmds) joins multiple commands to a single
281 #       command string if the protocol supports it, or does nothing and leaves
282 #       @cmds unchanged if the protocol does not support that usage (this is
283 #       meant to save send() invocations).
284 #     $chan->send($command, $nothrottle) sends a command over the channel. If
285 #       $nothrottle is sent, the command must not be left out even if the channel
286 #       is saturated (for example, because of IRC's flood control mechanism).
287 #     $chan->quote($str) returns a string in a quoted form so it can safely be
288 #       inserted as a substring into a command, or returns $str as is if not
289 #       applicable. It is assumed that the result of the quote method is used
290 #       as part of a quoted string, if the protocol supports that.
291 #     $chan->recv() returns a list of received commands from the channel, or
292 #       the empty list if none are available.
293 #     $conn->fds() returns all file descriptors used by the channel's
294 #       connections, so one can use select() on them.
295
296
297
298
299
300
301
302 # Socket connection.
303 # Represents a connection over a socket.
304 # Mainly used to wrap a channel around it for, in this case, line based or rcon-like operation.
305 package Connection::Socket;
306 use strict;
307 use warnings;
308 use IO::Socket::INET;
309 use IO::Handle;
310
311 # Constructor:
312 #   my $conn = new Connection::Socket(tcp => "localaddress" => "remoteaddress" => 6667);
313 # If the remote address does not contain a port number, the numeric port is
314 # used (it serves as a default port).
315 sub new($$)
316 {
317         my ($class, $proto, $local, $remote, $defaultport) = @_;
318         my $sock = IO::Socket::INET->new(
319                 Proto => $proto,
320                 (length($local) ? (LocalAddr => $local) : ()),
321                 PeerAddr => $remote,
322                 PeerPort => $defaultport
323         ) or die "socket $proto/$local/$remote/$defaultport: $!";
324         binmode $sock;
325         $sock->blocking(0);
326         my $you = {
327                 # Mortal fool! Release me from this wretched tomb! I must be set free
328                 # or I will haunt you forever! I will hide your keys beneath the
329                 # cushions of your upholstered furniture... and NEVERMORE will you be
330                 # able to find socks that match!
331                 sock => $sock,
332                 # My demonic powers have made me OMNIPOTENT! Bwahahahahahahaha!
333         };
334         return
335                 bless $you, 'Connection::Socket';
336 }
337
338 # $sock->sockname() returns the local address of the socket.
339 sub sockname($)
340 {
341         my ($self) = @_;
342         my ($port, $addr) = sockaddr_in $self->{sock}->sockname();
343         return "@{[inet_ntoa $addr]}:$port";
344 }
345
346 # $sock->peername() returns the remote address of the socket.
347 sub peername($)
348 {
349         my ($self) = @_;
350         my ($port, $addr) = sockaddr_in $self->{sock}->peername();
351         return "@{[inet_ntoa $addr]}:$port";
352 }
353
354 # $sock->send($data) sends some data over the socket; on success, 1 is returned.
355 sub send($$)
356 {
357         my ($self, $data) = @_;
358         return 1
359                 if not length $data;
360         if(not eval { $self->{sock}->send($data); })
361         {
362                 warn "$@";
363                 return 0;
364         }
365         return 1;
366 }
367
368 # $sock->recv() receives as much as possible from the socket (or at most 32k). Returns "" if no data is available.
369 sub recv($)
370 {
371         my ($self) = @_;
372         my $data = "";
373         if(defined $self->{sock}->recv($data, 32768, 0))
374         {
375                 return $data;
376         }
377         elsif($!{EAGAIN})
378         {
379                 return "";
380         }
381         else
382         {
383                 return undef;
384         }
385 }
386
387 # $sock->fds() returns the socket file descriptor.
388 sub fds($)
389 {
390         my ($self) = @_;
391         return fileno $self->{sock};
392 }
393
394
395
396
397
398
399
400 # Line-based buffered connectionless FIFO channel.
401 # Whatever is sent to it using send() is echoed back when using recv().
402 package Channel::FIFO;
403 use strict;
404 use warnings;
405
406 # Constructor:
407 #   my $chan = new Channel::FIFO();
408 sub new($)
409 {
410         my ($class) = @_;
411         my $you = {
412                 buffer => []
413         };
414         return
415                 bless $you, 'Channel::FIFO';
416 }
417
418 sub join_commands($@)
419 {
420         my ($self, @data) = @_;
421         return @data;
422 }
423
424 sub send($$$)
425 {
426         my ($self, $line, $nothrottle) = @_;
427         push @{$self->{buffer}}, $line;
428 }
429
430 sub quote($$)
431 {
432         my ($self, $data) = @_;
433         return $data;
434 }
435
436 sub recv($)
437 {
438         my ($self) = @_;
439         my $r = $self->{buffer};
440         $self->{buffer} = [];
441         return @$r;
442 }
443
444 sub fds($)
445 {
446         my ($self) = @_;
447         return ();
448 }
449
450
451
452
453
454
455
456 # QW rcon protocol channel.
457 # Wraps around a UDP based Connection and sends commands as rcon commands as
458 # well as receives rcon replies. The quote and join_commands methods are using
459 # DarkPlaces engine specific rcon protocol extensions.
460 package Channel::QW;
461 use strict;
462 use warnings;
463 use Digest::HMAC;
464 use Digest::MD4;
465
466 # Constructor:
467 #   my $chan = new Channel::QW($connection, "password");
468 sub new($$$)
469 {
470         my ($class, $conn, $password, $secure, $timeout) = @_;
471         my $you = {
472                 connector => $conn,
473                 password => $password,
474                 recvbuf => "",
475                 secure => $secure,
476                 timeout => $timeout,
477         };
478         return
479                 bless $you, 'Channel::QW';
480 }
481
482 # Note: multiple commands in one rcon packet is a DarkPlaces extension.
483 sub join_commands($@)
484 {
485         my ($self, @data) = @_;
486         return join "\0", @data;
487 }
488
489 sub send($$$)
490 {
491         my ($self, $line, $nothrottle) = @_;
492         utf8::encode $line
493                 if $color_utf8_enable;
494         if($self->{secure} > 1)
495         {
496                 $self->{connector}->send("\377\377\377\377getchallenge");
497                 my $c = $self->recvchallenge();
498                 return 0 if not defined $c;
499                 my $key = Digest::HMAC::hmac("$c $line", $self->{password}, \&Digest::MD4::md4);
500                 return $self->{connector}->send("\377\377\377\377srcon HMAC-MD4 CHALLENGE $key $c $line");
501         }
502         elsif($self->{secure})
503         {
504                 my $t = sprintf "%ld.%06d", time(), int rand 1000000;
505                 my $key = Digest::HMAC::hmac("$t $line", $self->{password}, \&Digest::MD4::md4);
506                 return $self->{connector}->send("\377\377\377\377srcon HMAC-MD4 TIME $key $t $line");
507         }
508         else
509         {
510                 return $self->{connector}->send("\377\377\377\377rcon $self->{password} $line");
511         }
512 }
513
514 # Note: backslash and quotation mark escaping is a DarkPlaces extension.
515 sub quote($$)
516 {
517         my ($self, $data) = @_;
518         $data =~ s/[\000-\037]//g;
519         $data =~ s/([\\"])/\\$1/g;
520         $data =~ s/\$/\$\$/g;
521         return $data;
522 }
523
524 sub recvchallenge($)
525 {
526         my ($self) = @_;
527
528         my $sel = IO::Select->new($self->fds());
529         my $endtime_max = Time::HiRes::time() + $self->{timeout};
530         my $endtime = $endtime_max;
531
532         while((my $dt = $endtime - Time::HiRes::time()) > 0)
533         {
534                 if($sel->can_read($dt))
535                 {
536                         for(;;)
537                         {
538                                 my $s = $self->{connector}->recv();
539                                 die "read error\n"
540                                         if not defined $s;
541                                 length $s
542                                         or last;
543                                 if($s =~ /^\377\377\377\377challenge (.*?)(?:$|\0)/s)
544                                 {
545                                         return $1;
546                                 }
547                                 next
548                                         if $s !~ /^\377\377\377\377n(.*)$/s;
549                                 $self->{recvbuf} .= $1;
550                         }
551                 }
552         }
553         return undef;
554 }
555
556 sub recv($)
557 {
558         my ($self) = @_;
559         for(;;)
560         {
561                 my $s = $self->{connector}->recv();
562                 die "read error\n"
563                         if not defined $s;
564                 length $s
565                         or last;
566                 next
567                         if $s !~ /^\377\377\377\377n(.*)$/s;
568                 $self->{recvbuf} .= $1;
569         }
570         my @out = ();
571         while($self->{recvbuf} =~ s/^(.*?)(?:\r\n?|\n)//)
572         {
573                 my $s = $1;
574                 utf8::decode $s
575                         if $color_utf8_enable;
576                 push @out, $s;
577         }
578         return @out;
579 }
580
581 sub fds($)
582 {
583         my ($self) = @_;
584         return $self->{connector}->fds();
585 }
586
587
588
589
590
591
592
593 # Line based protocol channel.
594 # Wraps around a TCP based Connection and sends commands as text lines
595 # (separated by CRLF). When reading responses from the Connection, any type of
596 # line ending is accepted.
597 # A flood control mechanism is implemented.
598 package Channel::Line;
599 use strict;
600 use warnings;
601 use Time::HiRes qw/time/;
602
603 # Constructor:
604 #   my $chan = new Channel::Line($connection);
605 sub new($$)
606 {
607         my ($class, $conn) = @_;
608         my $you = {
609                 connector => $conn,
610                 recvbuf => "",
611                 capacity => undef,
612                 linepersec => undef,
613                 maxlines => undef,
614                 lastsend => time()
615         };
616         return 
617                 bless $you, 'Channel::Line';
618 }
619
620 sub join_commands($@)
621 {
622         my ($self, @data) = @_;
623         return @data;
624 }
625
626 # Sets new flood control parameters:
627 #   $chan->throttle(maximum lines per second, maximum burst length allowed to
628 #     exceed the lines per second limit);
629 #   RFC 1459 describes these parameters to be 0.5 and 5 for the IRC protocol.
630 #   If the $nothrottle flag is set while sending, the line is sent anyway even
631 #   if flooding would take place.
632 sub throttle($$$)
633 {
634         my ($self, $linepersec, $maxlines) = @_;
635         $self->{linepersec} = $linepersec;
636         $self->{maxlines} = $maxlines;
637         $self->{capacity} = $maxlines;
638 }
639
640 sub send($$$)
641 {
642         my ($self, $line, $nothrottle) = @_;
643         utf8::encode $line
644                 if $color_utf8_enable;
645         my $t = time();
646         if(defined $self->{capacity})
647         {
648                 $self->{capacity} += ($t - $self->{lastsend}) * $self->{linepersec};
649                 $self->{lastsend} = $t;
650                 $self->{capacity} = $self->{maxlines}
651                         if $self->{capacity} > $self->{maxlines};
652                 if(!$nothrottle)
653                 {
654                         return -1
655                                 if $self->{capacity} < 0;
656                 }
657                 $self->{capacity} -= 1;
658         }
659         $line =~ s/\r|\n//g;
660         return $self->{connector}->send("$line\r\n");
661 }
662
663 sub quote($$)
664 {
665         my ($self, $data) = @_;
666         $data =~ s/\r\n?/\n/g;
667         $data =~ s/\n/*/g;
668         return $data;
669 }
670
671 sub recv($)
672 {
673         my ($self) = @_;
674         for(;;)
675         {
676                 my $s = $self->{connector}->recv();
677                 die "read error\n"
678                         if not defined $s;
679                 length $s
680                         or last;
681                 $self->{recvbuf} .= $s;
682         }
683         my @out = ();
684         while($self->{recvbuf} =~ s/^(.*?)(?:\r\n?|\n)//)
685         {
686                 my $s = $1;
687                 utf8::decode $s
688                         if $color_utf8_enable;
689                 push @out, $s;
690         }
691         return @out;
692 }
693
694 sub fds($)
695 {
696         my ($self) = @_;
697         return $self->{connector}->fds();
698 }
699
700
701
702
703
704
705 # main program... a gateway between IRC and DarkPlaces servers
706 package main;
707
708 use strict;
709 use warnings;
710 use IO::Select;
711 use Digest::SHA;
712 use Digest::HMAC;
713 use Time::HiRes qw/time/;
714
715 our @handlers = (); # list of [channel, expression, sub to handle result]
716 our @tasks = (); # list of [time, sub]
717 our %channels = ();
718 our %store = (
719         irc_nick => "",
720         playernick_byid_0 => "(console)",
721 );
722 our %config = (
723         irc_server => undef,
724         irc_nick => undef,
725         irc_nick_alternates => "",
726         irc_user => undef,
727         irc_channel => undef,
728         irc_ping_delay => 120,
729         irc_trigger => "",
730
731         irc_nickserv_password => "",
732         irc_nickserv_identify => 'PRIVMSG NickServ :IDENTIFY %2$s',
733         irc_nickserv_ghost => 'PRIVMSG NickServ :GHOST %1$s %2$s',
734         irc_nickserv_ghost_attempts => 3,
735
736         irc_quakenet_authname => "",
737         irc_quakenet_password => "",
738         irc_quakenet_authusers => "",
739         irc_quakenet_getchallenge => 'PRIVMSG Q@CServe.quakenet.org :CHALLENGE',
740         irc_quakenet_challengeauth => 'PRIVMSG Q@CServe.quakenet.org :CHALLENGEAUTH',
741         irc_quakenet_challengeprefix => ':Q!TheQBot@CServe.quakenet.org NOTICE [^:]+ :CHALLENGE',
742
743         irc_announce_slotsfree => 1,
744         irc_announce_mapchange => 'always',
745
746         dp_server => undef,
747         dp_secure => 1,
748         dp_secure_challengetimeout => 1,
749         dp_listen => "", 
750         dp_password => undef,
751         dp_status_delay => 30,
752         dp_server_from_wan => "",
753         dp_listen_from_server => "", 
754         dp_utf8_enable => $color_utf8_enable,
755         dp_timinglog => "",
756         irc_local => "",
757
758         irc_admin_password => "",
759         irc_admin_timeout => 3600,
760         irc_admin_quote_re => "",
761
762         irc_reconnect_delay => 300,
763         irc_commands => "",
764
765         plugins => "",
766 );
767
768 sub pickip($$)
769 {
770         my ($wan, $lan) = @_;
771         # $wan shall override $lan
772         return $lan
773                 if not length $wan;
774         return $wan
775                 if $wan =~ /:\d+$/; # full override
776         return $wan
777                 if $lan !~ /:(\d+)$/;
778         return "$wan:$1";
779 }
780
781
782
783 # Xonotic specific parsing of some server messages
784
785 sub xon_slotsstring()
786 {
787         my $slotsstr = "";
788         if(defined $store{slots_max})
789         {
790                 my $slots = $store{slots_max} - $store{slots_active};
791                 my $slots_s = ($slots == 1) ? '' : 's';
792                 $slotsstr = " ($slots free slot$slots_s)";
793                 my $s = pickip($config{dp_server_from_wan}, $config{dp_server});
794                 $slotsstr .= "; join now: \002xonotic +connect $s"
795                         if $slots >= 1 and not $store{lms_blocked};
796         }
797         return $slotsstr;
798 }
799
800
801
802 # Do we have a config file? If yes, read and parse it (syntax: key = value
803 # pairs, separated by newlines), if not, complain.
804 die "Usage: $0 configfile\n"
805         unless @ARGV == 1;
806
807 open my $fh, "<", $ARGV[0]
808         or die "open $ARGV[0]: $!";
809 while(<$fh>)
810 {
811         chomp;
812         /^#/ and next;
813         /^(.*?)\s*=(?:\s*(.*))?$/ or next;
814         warn "Undefined config item: $1"
815                 unless exists $config{$1};
816         $config{$1} = defined $2 ? $2 : "";
817 }
818 close $fh;
819 my @missing = grep { !defined $config{$_} } keys %config;
820 die "The following config items are missing: @missing"
821         if @missing;
822
823 $color_utf8_enable = $config{dp_utf8_enable};
824
825
826 # Create a channel for error messages and other internal status messages...
827
828 $channels{system} = new Channel::FIFO();
829
830 # for example, quit messages caused by signals (if SIGTERM or SIGINT is first
831 # received, try to shut down cleanly, and if such a signal is received a second
832 # time, just exit)
833 my $quitting = 0;
834 $SIG{INT} = sub {
835         exit 1 if $quitting++;
836         $channels{system}->send("quit SIGINT");
837 };
838 $SIG{TERM} = sub {
839         exit 1 if $quitting++;
840         $channels{system}->send("quit SIGTERM");
841 };
842
843
844
845 # Create the two channels to gateway between...
846
847 $channels{irc} = new Channel::Line(new Connection::Socket(tcp => $config{irc_local} => $config{irc_server} => 6667));
848 $channels{dp} = new Channel::QW(my $dpsock = new Connection::Socket(udp => $config{dp_listen} => $config{dp_server} => 26000), $config{dp_password}, $config{dp_secure}, $config{dp_secure_challengetimeout});
849 $config{dp_listen} = $dpsock->sockname();
850 $config{dp_server} = $dpsock->peername();
851 print "Listening on $config{dp_listen}\n";
852
853 $channels{irc}->throttle(0.5, 5);
854
855
856 # Utility routine to write to a channel by name, also outputting what's been written and some status
857 sub out($$@)
858 {
859         my $chanstr = shift;
860         my $nothrottle = shift;
861         my $chan = $channels{$chanstr};
862         if(!$chan)
863         {
864                 print "UNDEFINED: $chanstr, ignoring message\n";
865                 return;
866         }
867         @_ = $chan->join_commands(@_);
868         for(@_)
869         {
870                 my $result = $chan->send($_, $nothrottle);
871                 if($result > 0)
872                 {
873                         print "           $chanstr << $_\n";
874                 }
875                 elsif($result < 0)
876                 {
877                         print "FLOOD:     $chanstr << $_\n";
878                 }
879                 else
880                 {
881                         print "ERROR:     $chanstr << $_\n";
882                         $channels{system}->send("error $chanstr", 0);
883                 }
884         }
885 }
886
887
888
889 # Schedule a task for later execution by the main loop; usage: schedule sub {
890 # task... }, $time; When a scheduled task is run, a reference to the task's own
891 # sub is passed as first argument; that way, the task is able to re-schedule
892 # itself so it gets periodically executed.
893 sub schedule($$)
894 {
895         my ($sub, $time) = @_;
896         push @tasks, [time() + $time, $sub];
897 }
898
899 # On IRC error, delete some data store variables of the connection, and
900 # reconnect to the IRC server soon (but only if someone is actually playing)
901 sub irc_error()
902 {
903         # prevent multiple instances of this timer
904         return if $store{irc_error_active};
905         $store{irc_error_active} = 1;
906
907         delete $channels{irc};
908         schedule sub {
909                 my ($timer) = @_;
910                 if(!defined $store{slots_active})
911                 {
912                         # DP is not running, then delay IRC reconnecting
913                         #use Data::Dumper; print Dumper \$timer;
914                         schedule $timer => 1;
915                         return;
916                         # this will keep irc_error_active
917                 }
918                 $channels{irc} = new Channel::Line(new Connection::Socket(tcp => $config{irc_local} => $config{irc_server} => 6667));
919                 delete $store{$_} for grep { /^irc_/ } keys %store;
920                 $store{irc_nick} = "";
921                 schedule sub {
922                         my ($timer) = @_;
923                         out dp => 0, 'sv_cmd banlist', 'status 1', 'log_dest_udp';
924                         $store{status_waiting} = -1;
925                 } => 1;
926                 # this will clear irc_error_active
927         } => $config{irc_reconnect_delay};
928         return 0;
929 }
930
931 sub uniq(@)
932 {
933         my @out = ();
934         my %found = ();
935         for(@_)
936         {
937                 next if $found{$_}++;
938                 push @out, $_;
939         }
940         return @out;
941 }
942
943 # IRC joining (if this is called as response to a nick name collision, $is433 is set);
944 # among other stuff, it performs NickServ or Quakenet authentication. This is to be called
945 # until the channel has been joined for every message that may be "interesting" (basically,
946 # IRC 001 hello messages, 443 nick collision messages and some notices by services).
947 sub irc_joinstage($)
948 {
949         my($is433) = @_;
950
951         return 0
952                 if $store{irc_joined_channel};
953         
954                 #use Data::Dumper; print Dumper \%store;
955
956         if($is433)
957         {
958                 if(length $store{irc_nick})
959                 {
960                         # we already have another nick, but couldn't change to the new one
961                         # try ghosting and then get the nick again
962                         if(length $config{irc_nickserv_password})
963                         {
964                                 if(++$store{irc_nickserv_ghost_attempts} <= $config{irc_nickserv_ghost_attempts})
965                                 {
966                                         $store{irc_nick_requested} = $config{irc_nick};
967                                         out irc => 1, sprintf($config{irc_nickserv_ghost}, $config{irc_nick}, $config{irc_nickserv_password});
968                                         schedule sub {
969                                                 out irc => 1, "NICK $config{irc_nick}";
970                                         } => 1;
971                                         return; # we'll get here again for the NICK success message, or for a 433 failure
972                                 }
973                                 # otherwise, we failed to ghost and will continue with the wrong
974                                 # nick... also, no need to try to identify here
975                         }
976                         # otherwise, we can't handle this and will continue with our wrong nick
977                 }
978                 else
979                 {
980                         # we failed to get an initial nickname
981                         # change ours a bit and try again
982
983                         my @alternates = uniq ($config{irc_nick}, grep { $_ ne "" } split /\s+/, $config{irc_nick_alternates});
984                         my $nextnick = undef;
985                         for(0..@alternates-2)
986                         {
987                                 if($store{irc_nick_requested} eq $alternates[$_])
988                                 {
989                                         $nextnick = $alternates[$_+1];
990                                 }
991                         }
992                         if($store{irc_nick_requested} eq $alternates[@alternates-1]) # this will only happen once
993                         {
994                                 $store{irc_nick_requested} = $alternates[0];
995                                 # but don't set nextnick, so we edit it
996                         }
997                         if(defined $nextnick)
998                         {
999                                 $store{irc_nick_requested} = $nextnick;
1000                         }
1001                         else
1002                         {
1003                                 for(;;)
1004                                 {
1005                                         if(length $store{irc_nick_requested} < 9)
1006                                         {
1007                                                 $store{irc_nick_requested} .= '_';
1008                                         }
1009                                         else
1010                                         {
1011                                                 substr $store{irc_nick_requested}, int(rand length $store{irc_nick_requested}), 1, chr(97 + int rand 26);
1012                                         }
1013                                         last unless grep { $_ eq $store{irc_nick_requested} } @alternates;
1014                                 }
1015                         }
1016                         out irc => 1, "NICK $store{irc_nick_requested}";
1017                         return; # when it fails, we'll get here again, and when it succeeds, we will continue
1018                 }
1019         }
1020
1021         # we got a 001 or a NICK message, so $store{irc_nick} has been updated
1022         if(length $config{irc_nickserv_password})
1023         {
1024                 if($store{irc_nick} eq $config{irc_nick})
1025                 {
1026                         # identify
1027                         out irc => 1, sprintf($config{irc_nickserv_identify}, $config{irc_nick}, $config{irc_nickserv_password});
1028                 }
1029                 else
1030                 {
1031                         # ghost
1032                         if(++$store{irc_nickserv_ghost_attempts} <= $config{irc_nickserv_ghost_attempts})
1033                         {
1034                                 $store{irc_nick_requested} = $config{irc_nick};
1035                                 out irc => 1, sprintf($config{irc_nickserv_ghost}, $config{irc_nick}, $config{irc_nickserv_password});
1036                                 schedule sub {
1037                                         out irc => 1, "NICK $config{irc_nick}";
1038                                 } => 1;
1039                                 return; # we'll get here again for the NICK success message, or for a 433 failure
1040                         }
1041                         # otherwise, we failed to ghost and will continue with the wrong
1042                         # nick... also, no need to try to identify here
1043                 }
1044         }
1045
1046         # we are on Quakenet. Try to authenticate.
1047         if(length $config{irc_quakenet_password} and length $config{irc_quakenet_authname})
1048         {
1049                 if(defined $store{irc_quakenet_challenge})
1050                 {
1051                         if($store{irc_quakenet_challenge} =~ /^([0-9a-f]*)\b.*\bHMAC-SHA-256\b/)
1052                         {
1053                                 my $challenge = $1;
1054                                 my $hash1 = Digest::SHA::sha256_hex(substr $config{irc_quakenet_password}, 0, 10);
1055                                 my $key = Digest::SHA::sha256_hex("@{[lc $config{irc_quakenet_authname}]}:$hash1");
1056                                 my $digest = Digest::HMAC::hmac_hex($challenge, $key, \&Digest::SHA::sha256);
1057                                 out irc => 1, "$config{irc_quakenet_challengeauth} $config{irc_quakenet_authname} $digest HMAC-SHA-256";
1058                         }
1059                 }
1060                 else
1061                 {
1062                         out irc => 1, $config{irc_quakenet_getchallenge};
1063                         return;
1064                         # we get here again when Q asks us
1065                 }
1066         }
1067
1068         for(split / *; */, $store{irc_commands})
1069         {
1070                 s/\$nick/$store{irc_nick}/g;
1071                 out irc => 1, $_;
1072         }
1073         
1074         # if we get here, we are on IRC
1075         $store{irc_joined_channel} = 1;
1076         schedule sub {
1077                 # wait 1 sec to let stuff calm down
1078                 out irc => 1, "JOIN $config{irc_channel}";
1079         } => 1;
1080         return 0;
1081 }
1082
1083 my $RE_FAIL = qr/$ $/;
1084 my $RE_SUCCEED = qr//;
1085 sub cond($)
1086 {
1087         return $_[0] ? $RE_FAIL : $RE_SUCCEED;
1088 }
1089
1090
1091 # List of all handlers on the various sockets. Additional handlers can be added by a plugin.
1092 @handlers = (
1093         # detect a server restart and set it up again
1094         [ dp => q{ *(?:Warning: Could not expand \$|Unknown command ")(?:rcon2irc_[a-z0-9_]*)[" ]*} => sub {
1095                 out dp => 0,
1096                         'alias rcon2irc_eval "$*"',
1097                         'log_dest_udp',
1098                         'sv_logscores_console 0',
1099                         'sv_logscores_bots 1',
1100                         'sv_eventlog 1',
1101                         'sv_eventlog_console 1',
1102                         'alias rcon2irc_say_as "set say_as_restorenick \"$sv_adminnick\"; sv_adminnick \"$1^3\"; say \"^7$2\"; rcon2irc_say_as_restore"',
1103                         'alias rcon2irc_say_as_restore "set sv_adminnick \"$say_as_restorenick\""',
1104                         'alias rcon2irc_quit "echo \"quitting rcon2irc $1: log_dest_udp is $log_dest_udp\""'; # note: \\\\\\" ->perl \\\" ->console \"
1105                 return 0;
1106         } ],
1107
1108         # detect missing entry in log_dest_udp and fix it
1109         [ dp => q{"log_dest_udp" is "([^"]*)" \["[^"]*"\]} => sub {
1110                 my ($dest) = @_;
1111                 my @dests = split ' ', $dest;
1112                 return 0 if grep { $_ eq pickip($config{dp_listen_from_server}, $config{dp_listen}) } @dests;
1113                 out dp => 0, 'log_dest_udp "' . join(" ", @dests, pickip($config{dp_listen_from_server}, $config{dp_listen})) . '"';
1114                 return 0;
1115         } ],
1116
1117         # retrieve list of banned hosts
1118         [ dp => q{#(\d+): (\S+) is still banned for (\S+) seconds} => sub {
1119                 return 0 unless $store{status_waiting} < 0;
1120                 my ($id, $ip, $time) = @_;
1121                 $store{bans_new} = [] if $id == 0;
1122                 $store{bans_new}[$id] = { ip => $ip, 'time' => $time };
1123                 return 0;
1124         } ],
1125
1126         # retrieve hostname from status replies
1127         [ dp => q{host:     (.*)} => sub {
1128                 return 0 unless $store{status_waiting} < 0;
1129                 my ($name) = @_;
1130                 $store{dp_hostname} = $name;
1131                 $store{bans} = $store{bans_new};
1132                 return 0;
1133         } ],
1134
1135         # retrieve version from status replies
1136         [ dp => q{version:  (.*)} => sub {
1137                 return 0 unless $store{status_waiting} < 0;
1138                 my ($version) = @_;
1139                 $store{dp_version} = $version;
1140                 return 0;
1141         } ],
1142
1143         # retrieve player names
1144         [ dp => q{players:  (\d+) active \((\d+) max\)} => sub {
1145                 return 0 unless $store{status_waiting} < 0;
1146                 my ($active, $max) = @_;
1147                 my $full = ($active >= $max);
1148                 $store{slots_max} = $max;
1149                 $store{slots_active} = $active;
1150                 $store{status_waiting} = $active;
1151                 $store{playerslots_active_new} = [];
1152                 if($store{status_waiting} == 0)
1153                 {
1154                         $store{playerslots_active} = $store{playerslots_active_new};
1155                 }
1156                 if($full != ($store{slots_full} || 0))
1157                 {
1158                         $store{slots_full} = $full;
1159                         return 0 if $store{lms_blocked};
1160                         return 0 if !$config{irc_announce_slotsfree};
1161                         if($full)
1162                         {
1163                                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION is full!\001";
1164                         }
1165                         else
1166                         {
1167                                 my $slotsstr = xon_slotsstring();
1168                                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION can be joined again$slotsstr!\001";
1169                         }
1170                 }
1171                 return 0;
1172         } ],
1173
1174         # retrieve player names
1175         [ dp => q{\^\d(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(-?\d+)\s+\#(\d+)\s+\^\d(.*)} => sub {
1176                 return 0 unless $store{status_waiting} > 0;
1177                 my ($ip, $pl, $ping, $time, $frags, $no, $name) = ($1, $2, $3, $4, $5, $6, $7);
1178                 $store{"playerslot_$no"} = { ip => $ip, pl => $pl, ping => $ping, 'time' => $time, frags => $frags, no => $no, name => $name };
1179                 push @{$store{playerslots_active_new}}, $no;
1180                 if(--$store{status_waiting} == 0)
1181                 {
1182                         $store{playerslots_active} = $store{playerslots_active_new};
1183                 }
1184                 return 0;
1185         } ],
1186
1187         # IRC admin commands
1188         [ irc => q{:(([^! ]*)![^ ]*) (?i:PRIVMSG) [^&#%]\S* :(.*)} => sub {
1189                 return 0 unless ($config{irc_admin_password} ne '' || $store{irc_quakenet_users});
1190
1191                 my ($hostmask, $nick, $command) = @_;
1192                 my $dpnick = color_dpfix $nick;
1193
1194                 if($command eq "login $config{irc_admin_password}")
1195                 {
1196                         $store{logins}{$hostmask} = time() + $config{irc_admin_timeout};
1197                         out irc => 0, "PRIVMSG $nick :my wish is your command";
1198                         return -1;
1199                 }
1200
1201                 if($command =~ /^login /)
1202                 {
1203                         out irc => 0, "PRIVMSG $nick :invalid password";
1204                         return -1;
1205                 }
1206
1207                 if(($store{logins}{$hostmask} || 0) < time())
1208                 {
1209                         out irc => 0, "PRIVMSG $nick :authentication required";
1210                         return -1;
1211                 }
1212
1213                 if($command =~ /^status(?: (.*))?$/)
1214                 {
1215                         my ($match) = $1;
1216                         my $found = 0;
1217                         my $foundany = 0;
1218                         for my $slot(@{$store{playerslots_active} || []})
1219                         {
1220                                 my $s = $store{"playerslot_$slot"};
1221                                 next unless $s;
1222                                 if(not defined $match or index(color_dp2none($s->{name}), $match) >= 0)
1223                                 {
1224                                         out irc => 0, sprintf 'PRIVMSG %s :%-21s %2i %4i %8s %4i #%-3u %s', $nick, $s->{ip}, $s->{pl}, $s->{ping}, $s->{time}, $s->{frags}, $slot, color_dp2irc $s->{name};
1225                                         ++$found;
1226                                 }
1227                                 ++$foundany;
1228                         }
1229                         if(!$found)
1230                         {
1231                                 if(!$foundany)
1232                                 {
1233                                         out irc => 0, "PRIVMSG $nick :the server is empty";
1234                                 }
1235                                 else
1236                                 {
1237                                         out irc => 0, "PRIVMSG $nick :no nicknames match";
1238                                 }
1239                         }
1240                         return 0;
1241                 }
1242
1243                 if($command =~ /^kick # (\d+) (.*)$/)
1244                 {
1245                         my ($id, $reason) = ($1, $2);
1246                         my $dpreason = color_irc2dp $reason;
1247                         $dpreason =~ s/^(~?)(.*)/$1irc $dpnick: $2/g;
1248                         $dpreason =~ s/(["\\])/\\$1/g;
1249                         out dp => 0, "kick # $id $dpreason";
1250                         my $slotnik = "playerslot_$id";
1251                         out irc => 0, "PRIVMSG $nick :kicked #$id (@{[color_dp2irc $store{$slotnik}{name}]}\017 @ $store{$slotnik}{ip}) ($reason)";
1252                         return 0;
1253                 }
1254
1255                 if($command =~ /^kickban # (\d+) (\d+) (\d+) (.*)$/)
1256                 {
1257                         my ($id, $bantime, $mask, $reason) = ($1, $2, $3, $4);
1258                         my $dpreason = color_irc2dp $reason;
1259                         $dpreason =~ s/^(~?)(.*)/$1irc $dpnick: $2/g;
1260                         $dpreason =~ s/(["\\])/\\$1/g;
1261                         out dp => 0, "kickban # $id $bantime $mask $dpreason";
1262                         my $slotnik = "playerslot_$id";
1263                         out irc => 0, "PRIVMSG $nick :kickbanned #$id (@{[color_dp2irc $store{$slotnik}{name}]}\017 @ $store{$slotnik}{ip}), netmask $mask, for $bantime seconds ($reason)";
1264                         return 0;
1265                 }
1266
1267                 if($command eq "bans")
1268                 {
1269                         my $banlist =
1270                                 join ", ",
1271                                 map { "$_ ($store{bans}[$_]{ip}, $store{bans}[$_]{time}s)" }
1272                                 0..@{$store{bans} || []}-1;
1273                         $banlist = "no bans"
1274                                 if $banlist eq "";
1275                         out irc => 0, "PRIVMSG $nick :$banlist";
1276                         return 0;
1277                 }
1278
1279                 if($command =~ /^unban (\d+)$/)
1280                 {
1281                         my ($id) = ($1);
1282                         out dp => 0, "unban $id";
1283                         out irc => 0, "PRIVMSG $nick :removed ban $id ($store{bans}[$id]{ip})";
1284                         return 0;
1285                 }
1286
1287                 if($command =~ /^mute (\d+)$/)
1288                 {
1289                         my $id = $1;
1290                         out dp => 0, "mute $id";
1291                         my $slotnik = "playerslot_$id";
1292                         out irc => 0, "PRIVMSG $nick :muted $id (@{[color_dp2irc $store{$slotnik}{name}]}\017 @ $store{$slotnik}{ip})";
1293                         return 0;
1294                 }
1295
1296                 if($command =~ /^unmute (\d+)$/)
1297                 {
1298                         my ($id) = ($1);
1299                         out dp => 0, "unmute $id";
1300                         my $slotnik = "playerslot_$id";
1301                         out irc => 0, "PRIVMSG $nick :unmuted $id (@{[color_dp2irc $store{$slotnik}{name}]}\017 @ $store{$slotnik}{ip})";
1302                         return 0;
1303                 }
1304
1305                 if($command =~ /^quote (.*)$/)
1306                 {
1307                         my ($cmd) = ($1);
1308                         if($cmd =~ /^(??{$config{irc_admin_quote_re}})$/si)
1309                         {
1310                                 out irc => 0, $cmd;
1311                                 out irc => 0, "PRIVMSG $nick :executed your command";
1312                         }
1313                         else
1314                         {
1315                                 out irc => 0, "PRIVMSG $nick :permission denied";
1316                         }
1317                         return 0;
1318                 }
1319
1320                 out irc => 0, "PRIVMSG $nick :unknown command (supported: status [substring], kick # id reason, kickban # id bantime mask reason, bans, unban banid, mute id, unmute id)";
1321
1322                 return -1;
1323         } ],
1324
1325         # LMS: detect "no more lives" message
1326         [ dp => q{\^4.*\^4 has no more lives left} => sub {
1327                 if(!$store{lms_blocked})
1328                 {
1329                         $store{lms_blocked} = 1;
1330                         if(!$store{slots_full})
1331                         {
1332                                 schedule sub {
1333                                         if($store{lms_blocked})
1334                                         {
1335                                                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION can't be joined until next round (a player has no more lives left)\001";
1336                                         }
1337                                 } => 1;
1338                         }
1339                 }
1340         } ],
1341
1342         # detect IRC errors and reconnect
1343         [ irc => q{ERROR .*} => \&irc_error ],
1344         [ irc => q{:[^ ]* 404 .*} => \&irc_error ], # cannot send to channel
1345         [ system => q{error irc} => \&irc_error ],
1346
1347         # IRC nick in use
1348         [ irc => q{:[^ ]* 433 .*} => sub {
1349                 return irc_joinstage(433);
1350         } ],
1351
1352         # IRC welcome
1353         [ irc => q{:[^ ]* 001 .*} => sub {
1354                 $store{irc_seen_welcome} = 1;
1355                 $store{irc_nick} = $store{irc_nick_requested};
1356                 
1357                 # If users for quakenet are listed, parse them into a hash and schedule a sub to query information
1358                 if ($config{irc_quakenet_authusers} ne '') {
1359                         $store{irc_quakenet_users} = { map { $_ => 1 } split / /, $config{irc_quakenet_authusers} };
1360         
1361                         schedule sub {
1362                                 my ($timer) = @_;
1363                                 out irc => 0, "PRIVMSG Q :users " . $config{irc_channel};
1364                                 schedule $timer => 300;;
1365                         } => 1;
1366                 }
1367
1368                 return irc_joinstage(0);
1369         } ],
1370
1371         # IRC my nickname changed
1372         [ irc => q{:(?i:(??{$store{irc_nick}}))![^ ]* (?i:NICK) :(.*)} => sub {
1373                 my ($n) = @_;
1374                 $store{irc_nick} = $n;
1375                 return irc_joinstage(0);
1376         } ],
1377
1378         # Quakenet: challenge from Q
1379         [ irc => q{(??{$config{irc_quakenet_challengeprefix}}) (.*)} => sub {
1380                 $store{irc_quakenet_challenge} = $1;
1381                 return irc_joinstage(0);
1382         } ],
1383         
1384         # Catch joins of people in a channel the bot is in and catch our own joins of a channel,
1385         # detect channel join message and note hostname length to get the maximum allowed line length
1386         [ irc => q{:(([^! ]*)![^ ]*) JOIN (#.+)} => sub {
1387                 my ($hostmask, $nick, $chan) = @_;
1388
1389                 if ($nick eq $store{irc_nick}) {
1390                         $store{irc_maxlen} = 510 - length($hostmask);
1391                         if($store{irc_joined_channel} == 1)
1392                         {
1393                                 $store{irc_joined_channel} = 2;
1394                         }
1395                         print "* detected maximum line length for channel messages: $store{irc_maxlen}\n";
1396                 }
1397
1398                 return 0 unless ($store{irc_quakenet_users});
1399                 
1400                 if ($nick eq $store{irc_nick}) {
1401                         out irc => 0, "PRIVMSG Q :users $chan"; # get auths for all users
1402                 } else {
1403                         $store{quakenet_hosts}->{$nick} = $hostmask;
1404                         out irc => 0, "PRIVMSG Q :whois $nick"; # get auth for single user
1405                 }
1406                 
1407                 return 0;
1408         } ],
1409         
1410         # Catch response of users request
1411         [ irc => q{:Q!TheQBot@CServe.quakenet.org NOTICE [^:]+ :[@\+\s]?(\S+)\s+(\S+)\s*(\S*)\s*\((.*)\)} => sub {
1412                 my ($nick, $username, $flags, $host) = @_;
1413                 return 0 unless ($store{irc_quakenet_users});
1414                 
1415                 $store{logins}{"$nick!$host"} = time() + 600 if ($store{irc_quakenet_users}->{$username});
1416                 
1417                 return 0;
1418         } ],
1419         
1420         # Catch response of whois request
1421         [ irc => q{:Q!TheQBot@CServe.quakenet.org NOTICE [^:]+ :-Information for user (.*) \(using account (.*)\):} => sub {
1422                 my ($nick, $username) = @_;
1423                 return 0 unless ($store{irc_quakenet_users});
1424                 
1425                 if ($store{irc_quakenet_users}->{$username}) {
1426                         my $hostmask = $store{quakenet_hosts}->{$nick};
1427                         $store{logins}{$hostmask} = time() + 600;
1428                 }
1429                 
1430                 return 0;
1431         } ],
1432
1433         # shut down everything on SIGINT
1434         [ system => q{quit (.*)} => sub {
1435                 my ($cause) = @_;
1436                 out irc => 1, "QUIT :$cause";
1437                 $store{quitcookie} = int rand 1000000000;
1438                 out dp => 0, "rcon2irc_quit $store{quitcookie}";
1439         } ],
1440
1441         # remove myself from the log destinations and exit everything
1442         [ dp => q{quitting rcon2irc (??{$store{quitcookie}}): log_dest_udp is (.*) *} => sub {
1443                 my ($dest) = @_;
1444                 my @dests = grep { $_ ne pickip($config{dp_listen_from_server}, $config{dp_listen}) } split ' ', $dest;
1445                 out dp => 0, 'log_dest_udp "' . join(" ", @dests) . '"';
1446                 exit 0;
1447                 return 0;
1448         } ],
1449
1450         # IRC PING
1451         [ irc => q{PING (.*)} => sub {
1452                 my ($data) = @_;
1453                 out irc => 1, "PONG $data";
1454                 return 1;
1455         } ],
1456
1457         # IRC PONG
1458         [ irc => q{:[^ ]* PONG .* :(.*)} => sub {
1459                 my ($data) = @_;
1460                 return 0
1461                         if not defined $store{irc_pingtime};
1462                 return 0
1463                         if $data ne $store{irc_pingtime};
1464                 print "* measured IRC line delay: @{[time() - $store{irc_pingtime}]}\n";
1465                 undef $store{irc_pingtime};
1466                 return 0;
1467         } ],
1468
1469         # chat: Xonotic server -> IRC channel
1470         [ dp => q{\001(.*?)\^7: (.*)} => sub {
1471                 my ($nick, $message) = map { color_dp2irc $_ } @_;
1472                 out irc => 0, "PRIVMSG $config{irc_channel} :<$nick\017> $message";
1473                 return 0;
1474         } ],
1475
1476         # chat: Xonotic server -> IRC channel, nick set
1477         [ dp => q{:join:(\d+):(\d+):([^:]*):(.*)} => sub {
1478                 my ($id, $slot, $ip, $nick) = @_;
1479                 $store{"playernickraw_byid_$id"} = $nick;
1480                 $nick = color_dp2irc $nick;
1481                 $store{"playernick_byid_$id"} = $nick;
1482                 $store{"playerip_byid_$id"} = $ip;
1483                 $store{"playerslot_byid_$id"} = $slot;
1484                 $store{"playerid_byslot_$slot"} = $id;
1485                 return 0;
1486         } ],
1487
1488         # chat: Xonotic server -> IRC channel, nick change/set
1489         [ dp => q{:name:(\d+):(.*)} => sub {
1490                 my ($id, $nick) = @_;
1491                 $store{"playernickraw_byid_$id"} = $nick;
1492                 $nick = color_dp2irc $nick;
1493                 my $oldnick = $store{"playernick_byid_$id"};
1494                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 is now known as $nick";
1495                 $store{"playernick_byid_$id"} = $nick;
1496                 return 0;
1497         } ],
1498
1499         # chat: Xonotic server -> IRC channel, vote call
1500         [ dp => q{:vote:vcall:(\d+):(.*)} => sub {
1501                 my ($id, $command) = @_;
1502                 $command = color_dp2irc $command;
1503                 my $oldnick = $id ? $store{"playernick_byid_$id"} : "(console)";
1504                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 calls a vote for \"$command\017\"";
1505                 return 0;
1506         } ],
1507
1508         # chat: Xonotic server -> IRC channel, vote stop
1509         [ dp => q{:vote:vstop:(\d+)} => sub {
1510                 my ($id) = @_;
1511                 my $oldnick = $id ? $store{"playernick_byid_$id"} : "(console)";
1512                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 stopped the vote";
1513                 return 0;
1514         } ],
1515
1516         # chat: Xonotic server -> IRC channel, master login
1517         [ dp => q{:vote:vlogin:(\d+)} => sub {
1518                 my ($id) = @_;
1519                 my $oldnick = $id ? $store{"playernick_byid_$id"} : "(console)";
1520                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 logged in as master";
1521                 return 0;
1522         } ],
1523
1524         # chat: Xonotic server -> IRC channel, master do
1525         [ dp => q{:vote:vdo:(\d+):(.*)} => sub {
1526                 my ($id, $command) = @_;
1527                 $command = color_dp2irc $command;
1528                 my $oldnick = $id ? $store{"playernick_byid_$id"} : "(console)";
1529                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 used his master status to do \"$command\017\"";
1530                 return 0;
1531         } ],
1532
1533         # chat: Xonotic server -> IRC channel, result
1534         [ dp => q{:vote:v(yes|no|timeout):(\d+):(\d+):(\d+):(\d+):(-?\d+)} => sub {
1535                 my ($result, $yes, $no, $abstain, $not, $min) = @_;
1536                 my $spam = "$yes:$no" . (($min >= 0) ? " ($min needed)" : "") . ", $abstain didn't care, $not didn't vote";
1537                 out irc => 0, "PRIVMSG $config{irc_channel} :* the vote ended with $result: $spam";
1538                 return 0;
1539         } ],
1540
1541         # chat: IRC channel -> Xonotic server
1542         [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$config{irc_channel}})) :(?i:(??{$store{irc_nick}}))(?: |: ?|, ?)(.*)} => sub {
1543                 my ($nick, $message) = @_;
1544                 $nick = color_dpfix $nick;
1545                         # allow the nickname to contain colors in DP format! Therefore, NO color_irc2dp on the nickname!
1546                 $message = color_irc2dp $message;
1547                 $message =~ s/(["\\])/\\$1/g;
1548                 out dp => 0, "rcon2irc_say_as \"$nick on IRC\" \"$message\"";
1549                 return 0;
1550         } ],
1551
1552         (
1553                 length $config{irc_trigger}
1554                         ?
1555                                 [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$config{irc_channel}})) :(?i:(??{$config{irc_trigger}}))(?: |: ?|, ?)(.*)} => sub {
1556                                         my ($nick, $message) = @_;
1557                                         $nick = color_dpfix $nick;
1558                                                 # allow the nickname to contain colors in DP format! Therefore, NO color_irc2dp on the nickname!
1559                                         $message = color_irc2dp $message;
1560                                         $message =~ s/(["\\])/\\$1/g;
1561                                         out dp => 0, "rcon2irc_say_as \"$nick on IRC\" \"$message\"";
1562                                         return 0;
1563                                 } ]
1564                         :
1565                                 ()
1566         ),
1567
1568         # irc: CTCP VERSION reply
1569         [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$store{irc_nick}})) :\001VERSION( .*)?\001} => sub {
1570                 my ($nick) = @_;
1571                 my $ver = $store{dp_version} or return 0;
1572                 $ver .= ", rcon2irc $VERSION";
1573                 out irc => 0, "NOTICE $nick :\001VERSION $ver\001";
1574         } ],
1575
1576         # on game start, notify the channel
1577         [ dp => q{:gamestart:(.*):[0-9.]*} => sub {
1578                 my ($map) = @_;
1579                 $store{playing} = 1;
1580                 $store{map} = $map;
1581                 $store{map_starttime} = time();
1582                 if ($config{irc_announce_mapchange} eq 'always' || ($config{irc_announce_mapchange} eq 'notempty' && $store{slots_active} > 0)) {
1583                         my $slotsstr = xon_slotsstring();
1584                         out irc => 0, "PRIVMSG $config{irc_channel} :\00304" . $map . "\017 has begun$slotsstr";
1585                 }
1586                 delete $store{lms_blocked};
1587                 return 0;
1588         } ],
1589
1590         # on game over, clear the current map
1591         [ dp => q{:gameover} => sub {
1592                 $store{playing} = 0;
1593                 return 0;
1594         } ],
1595
1596         # scores: Xonotic server -> IRC channel (start)
1597         [ dp => q{:scores:(.*):(\d+)} => sub {
1598                 my ($map, $time) = @_;
1599                 $store{scores} = {};
1600                 $store{scores}{map} = $map;
1601                 $store{scores}{time} = $time;
1602                 $store{scores}{players} = [];
1603                 delete $store{lms_blocked};
1604                 return 0;
1605         } ],
1606
1607         # scores: Xonotic server -> IRC channel, legacy format
1608         [ dp => q{:player:(-?\d+):(\d+):(\d+):(\d+):(\d+):(.*)} => sub {
1609                 my ($frags, $deaths, $time, $team, $id, $name) = @_;
1610                 return if not exists $store{scores};
1611                 push @{$store{scores}{players}}, [$frags, $team, $name]
1612                         unless $frags <= -666; # no spectators
1613                 return 0;
1614         } ],
1615
1616         # scores: Xonotic server -> IRC channel (CTF), legacy format
1617         [ dp => q{:teamscores:(\d+:-?\d*(?::\d+:-?\d*)*)} => sub {
1618                 my ($teams) = @_;
1619                 return if not exists $store{scores};
1620                 $store{scores}{teams} = {split /:/, $teams};
1621                 return 0;
1622         } ],
1623
1624         # scores: Xonotic server -> IRC channel, new format
1625         [ dp => q{:player:see-labels:(-?\d+)[-0-9,]*:(\d+):(\d+):(\d+):(.*)} => sub {
1626                 my ($frags, $time, $team, $id, $name) = @_;
1627                 return if not exists $store{scores};
1628                 push @{$store{scores}{players}}, [$frags, $team, $name];
1629                 return 0;
1630         } ],
1631
1632         # scores: Xonotic server -> IRC channel (CTF), new format
1633         [ dp => q{:teamscores:see-labels:(-?\d+)[-0-9,]*:(\d+)} => sub {
1634                 my ($frags, $team) = @_;
1635                 return if not exists $store{scores};
1636                 $store{scores}{teams}{$team} = $frags;
1637                 return 0;
1638         } ],
1639
1640         # scores: Xonotic server -> IRC channel
1641         [ dp => q{:end} => sub {
1642                 return if not exists $store{scores};
1643                 my $s = $store{scores};
1644                 delete $store{scores};
1645                 my $teams_matter = defined $s->{teams};
1646
1647                 my @t = ();
1648                 my @p = ();
1649
1650                 if($teams_matter)
1651                 {
1652                         # put players into teams
1653                         my %t = ();
1654                         for(@{$s->{players}})
1655                         {
1656                                 my $thisteam = ($t{$_->[1]} ||= {score => 0, team => $_->[1], players => []});
1657                                 push @{$thisteam->{players}}, [$_->[0], $_->[1], $_->[2]];
1658                                 if($s->{teams})
1659                                 {
1660                                         $thisteam->{score} = $s->{teams}{$_->[1]};
1661                                 }
1662                                 else
1663                                 {
1664                                         $thisteam->{score} += $_->[0];
1665                                 }
1666                         }
1667
1668                         # sort by team score
1669                         @t = sort { $b->{score} <=> $a->{score} } values %t;
1670
1671                         # sort by player score
1672                         @p = ();
1673                         for(@t)
1674                         {
1675                                 @{$_->{players}} = sort { $b->[0] <=> $a->[0] } @{$_->{players}};
1676                                 push @p, @{$_->{players}};
1677                         }
1678                 }
1679                 else
1680                 {
1681                         @p = sort { $b->[0] <=> $a->[0] } @{$s->{players}};
1682                 }
1683
1684                 # no display for empty server
1685                 return 0
1686                         if !@p;
1687
1688                 # make message fit somehow
1689                 for my $maxnamelen(reverse 3..64)
1690                 {
1691                         my $scores_string = "PRIVMSG $config{irc_channel} :\00304" . $s->{map} . "\017 ended:";
1692                         if($teams_matter)
1693                         {
1694                                 my $sep = ' ';
1695                                 for(@t)
1696                                 {
1697                                         $scores_string .= $sep . "\003" . $color_team2irc_table{$_->{team}}. "\002\002" . $_->{score} . "\017";
1698                                         $sep = ':';
1699                                 }
1700                         }
1701                         my $sep = '';
1702                         for(@p)
1703                         {
1704                                 my ($frags, $team, $name) = @$_;
1705                                 $name = color_dpfix substr($name, 0, $maxnamelen);
1706                                 if($teams_matter)
1707                                 {
1708                                         $name = "\003" . $color_team2irc_table{$team} . " " . color_dp2none $name;
1709                                 }
1710                                 else
1711                                 {
1712                                         $name = " " . color_dp2irc $name;
1713                                 }
1714                                 $scores_string .= "$sep$name\017 $frags";
1715                                 $sep = ',';
1716                         }
1717                         if(length($scores_string) <= ($store{irc_maxlen} || 256))
1718                         {
1719                                 out irc => 0, $scores_string;
1720                                 return 0;
1721                         }
1722                 }
1723                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION would have LIKED to put the scores here, but they wouldn't fit :(\001";
1724                 return 0;
1725         } ],
1726
1727         # complain when system load gets too high
1728         [ dp => q{timing:   (([0-9.]*)% CPU, ([0-9.]*)% lost, offset avg ([0-9.]*)ms, max ([0-9.]*)ms, sdev ([0-9.]*)ms)} => sub {
1729                 my ($all, $cpu, $lost, $avg, $max, $sdev) = @_;
1730                 return 0 # don't complain when just on the voting screen
1731                         if !$store{playing};
1732                 if(length $config{dp_timinglog})
1733                 {
1734                         open my $fh, '>>', $config{dp_timinglog}
1735                                 or warn "open >> $config{dp_timinglog}: $!";
1736                         print $fh "@{[time]} $cpu $lost $avg $max $sdev $store{slots_active}\n"
1737                                 or warn "print >> $config{dp_timinglog}: $!";
1738                         close $fh
1739                                 or warn "close >> $config{dp_timinglog}: $!";
1740                 }
1741                 return 0 # don't complain if it was less than 0.5%
1742                         if $lost < 0.5;
1743                 return 0 # don't complain if nobody is looking
1744                         if $store{slots_active} == 0;
1745                 return 0 # don't complain in the first two minutes
1746                         if time() - $store{map_starttime} < 120;
1747                 return 0 # don't complain if it was already at least half as bad in this round
1748                         if $store{map_starttime} == $store{timingerror_map_starttime} and $lost <= 2 * $store{timingerror_lost};
1749                 $store{timingerror_map_starttime} = $store{map_starttime};
1750                 $store{timingerror_lost} = $lost;
1751                 out dp => 0, 'rcon2irc_say_as server "There are currently some severe system load problems. The admins have been notified."';
1752                 out irc => 1, "PRIVMSG $config{irc_channel} :\001ACTION has big trouble on $store{map} after @{[int(time() - $store{map_starttime})]}s: $all\001";
1753                 #out irc => 1, "PRIVMSG OpBaI :\001ACTION has big trouble on $store{map} after @{[int(time() - $store{map_starttime})]}s: $all\001";
1754                 return 0;
1755         } ],
1756 );
1757
1758
1759
1760 # Load plugins and add them to the handler list in the front.
1761 for my $p(split ' ', $config{plugins})
1762 {
1763         my @h = eval { do $p; }
1764                 or die "Invalid plugin $p: $@";
1765         for(reverse @h)
1766         {
1767                 ref $_ eq 'ARRAY' or die "Invalid plugin $p: did not return a list of arrays";
1768                 @$_ == 3 or die "Invalid plugin $p: did not return a list of three-element arrays";
1769                 !ref $_->[0] && !ref $_->[1] && ref $_->[2] eq 'CODE' or die "Invalid plugin $p: did not return a list of string-string-sub arrays";
1770                 unshift @handlers, $_;
1771         }
1772 }
1773
1774
1775
1776 # verify that the server is up by letting it echo back a string that causes
1777 # re-initialization of the required aliases
1778 out dp => 0, 'echo "Unknown command \"rcon2irc_eval\""'; # assume the server has been restarted
1779
1780
1781
1782 # regularily, query the server status and if it still is connected to us using
1783 # the log_dest_udp feature. If not, we will detect the response to this rcon
1784 # command and re-initialize the server's connection to us (either by log_dest_udp
1785 # not containing our own IP:port, or by rcon2irc_eval not being a defined command).
1786 schedule sub {
1787         my ($timer) = @_;
1788         out dp => 0, 'sv_cmd banlist', 'status 1', 'log_dest_udp', 'rcon2irc_eval set dummy 1';
1789         $store{status_waiting} = -1;
1790         schedule $timer => (exists $store{dp_hostname} ? $config{dp_status_delay} : 1);;
1791 } => 1;
1792
1793
1794
1795 # Continue with connecting to IRC as soon as we get our first status reply from
1796 # the DP server (which contains the server's hostname that we'll use as
1797 # realname for IRC).
1798 schedule sub {
1799         my ($timer) = @_;
1800
1801         # log on to IRC when needed
1802         if(exists $store{dp_hostname} && !exists $store{irc_seen_welcome})
1803         {
1804                 $store{irc_nick_requested} = $config{irc_nick};
1805                 out irc => 1, "NICK $config{irc_nick}", "USER $config{irc_user} localhost localhost :$store{dp_hostname}";
1806                 $store{irc_logged_in} = 1;
1807                 undef $store{irc_maxlen};
1808                 undef $store{irc_pingtime};
1809         }
1810
1811         schedule $timer => 1;;
1812 } => 1;
1813
1814
1815
1816 # Regularily ping the IRC server to detect if the connection is down. If it is,
1817 # schedule an IRC error that will cause reconnection later.
1818 schedule sub {
1819         my ($timer) = @_;
1820
1821         if($store{irc_logged_in})
1822         {
1823                 if(defined $store{irc_pingtime})
1824                 {
1825                         # IRC connection apparently broke
1826                         # so... KILL IT WITH FIRE
1827                         $channels{system}->send("error irc", 0);
1828                 }
1829                 else
1830                 {
1831                         # everything is fine, send a new ping
1832                         $store{irc_pingtime} = time();
1833                         out irc => 1, "PING $store{irc_pingtime}";
1834                 }
1835         }
1836
1837         schedule $timer => $config{irc_ping_delay};;
1838 } => 1;
1839
1840
1841
1842 # Main loop.
1843 for(;;)
1844 {
1845         # Build up an IO::Select object for all our channels.
1846         my $s = IO::Select->new();
1847         for my $chan(values %channels)
1848         {
1849                 $s->add($_) for $chan->fds();
1850         }
1851
1852         # wait for something to happen on our sockets, or wait 2 seconds without anything happening there
1853         $s->can_read(2);
1854         my @errors = $s->has_exception(0);
1855
1856         # on every channel, look for incoming messages
1857         CHANNEL:
1858         for my $chanstr(keys %channels)
1859         {
1860                 my $chan = $channels{$chanstr};
1861                 my @chanfds = $chan->fds();
1862
1863                 for my $chanfd(@chanfds)
1864                 {
1865                         if(grep { $_ == $chanfd } @errors)
1866                         {
1867                                 # STOP! This channel errored!
1868                                 $channels{system}->send("error $chanstr", 0);
1869                                 next CHANNEL;
1870                         }
1871                 }
1872
1873                 eval
1874                 {
1875                         for my $line($chan->recv())
1876                         {
1877                                 # found one! Check if it matches the regular expression of one of
1878                                 # our handlers...
1879                                 my $handled = 0;
1880                                 my $private = 0;
1881                                 for my $h(@handlers)
1882                                 {
1883                                         my ($chanstr_wanted, $re, $sub) = @$h;
1884                                         next
1885                                                 if $chanstr_wanted ne $chanstr;
1886                                         use re 'eval';
1887                                         my @matches = ($line =~ /^$re$/s);
1888                                         no re 'eval';
1889                                         next
1890                                                 unless @matches;
1891                                         # and if it is a match, handle it.
1892                                         ++$handled;
1893                                         my $result = $sub->(@matches);
1894                                         $private = 1
1895                                                 if $result < 0;
1896                                         last
1897                                                 if $result;
1898                                 }
1899                                 # print the message, together with info on whether it has been handled or not
1900                                 if($private)
1901                                 {
1902                                         print "           $chanstr >> (private)\n";
1903                                 }
1904                                 elsif($handled)
1905                                 {
1906                                         print "           $chanstr >> $line\n";
1907                                 }
1908                                 else
1909                                 {
1910                                         print "unhandled: $chanstr >> $line\n";
1911                                 }
1912                         }
1913                         1;
1914                 } or do {
1915                         if($@ eq "read error\n")
1916                         {
1917                                 $channels{system}->send("error $chanstr", 0);
1918                                 next CHANNEL;
1919                         }
1920                         else
1921                         {
1922                                 # re-throw
1923                                 die $@;
1924                         }
1925                 };
1926         }
1927
1928         # handle scheduled tasks...
1929         my @t = @tasks;
1930         my $t = time();
1931         # by emptying the list of tasks...
1932         @tasks = ();
1933         for(@t)
1934         {
1935                 my ($time, $sub) = @$_;
1936                 if($t >= $time)
1937                 {
1938                         # calling them if they are schedled for the "past"...
1939                         $sub->($sub);
1940                 }
1941                 else
1942                 {
1943                         # or re-adding them to the task list if they still are scheduled for the "future"
1944                         push @tasks, [$time, $sub];
1945                 }
1946         }
1947 }